Урок 8.7 — Аннотации типов и null-безопасность
Строгая типизация C#, var, nullable reference types и защита от null.
Одна из самых частых ошибок в программировании — обращение к null там, где ожидается объект. C# с версии 8.0 ввёл мощные средства для борьбы с этой проблемой: nullable reference types, null-conditional операторы и анализатор null-безопасности. В этом уроке ты научишься писать код, в котором null-ошибки практически невозможны.
Строгая типизация в C#
C# — строго типизированный язык. Это значит, что тип каждой переменной, параметра, выражения известен на этапе компиляции. Если ты попытаешься использовать значение не по типу — компилятор выдаст ошибку.
int number = 42;
string text = "Привет";
// number = "текст"; // ❌ Ошибка компиляции: нельзя string в int
// text = 42; // ❌ Ошибка компиляции: нельзя int в string
// Неявное приведение типов:
double d = 10; // int → double (безопасно, не теряется)
int i = (int)3.14; // double → int (нужно явное приведение, теряется 0.14)
// Тип выражения определяется на этапе компиляции:
var sum = 10 + 20; // sum имеет тип int
var name = "Анна"; // name имеет тип string
Ключевое слово var
var — это не «тип-невидимка», а синтаксический сахар. Компилятор самостоятельно определяет тип переменной на основе выражения справа. Тип фиксируется на этапе компиляции и не может измениться.
var number = 42; // int
var price = 19.99m; // decimal
var name = "Анна"; // string
var isActive = true; // bool
var list = new List<int>(); // List<int>
var person = new Person(); // Person
// ❌ Нельзя использовать var без инициализации:
// var x; // ошибка!
// var y = null; // ошибка — тип не определить!
// ❌ Нельзя изменить тип:
var value = 10;
// value = "текст"; // ошибка — value уже int
Когда использовать var?
- Да: когда тип очевиден из правой части:
var dict = new Dictionary<string, int>(); - Да: для анонимных типов:
var user = new { Name = "Анна", Age = 25 }; - Нет: когда тип не очевиден:
var result = GetData();— лучше написать явно.
var не делает C# динамическим языком вроде JavaScript. Тип всё ещё жёсткий — просто компилятор выводит его за тебя.Nullable reference types: string? vs string (C# 8+)
До C# 8.0 ссылочные типы (string, object, классы) могли быть null без каких-либо предупреждений. Начиная с C# 8.0, ты можешь явно указать, может ли переменная быть null — с помощью символа ?.
// Enable nullable в .csproj или в файле:
// #nullable enable
string name = "Анна"; // НЕ может быть null — гарантия
string? maybeNull = null; // МОЖЕТ быть null — предупреждение, если не проверить
// name = null; // ⚠️ Предупреждение! string не должен быть null
// maybeNull = null; // ✅ Нормально, это ожидаемо
// При использовании nullable-переменной нужно проверить:
if (maybeNull != null)
{
Console.WriteLine(maybeNull.Length); // ✅ Безопасно
}
// Или через null-conditional:
Console.WriteLine(maybeNull?.Length); // ✅ Безопасно, вернёт null
Правила nullable reference types:
string— не-nullable: компилятор предупредит, если присвоить null.string?— nullable: явно допускает null, но требует проверки перед использованием.- Это же касается любых ссылочных типов:
Person?,List<int>?. - Null-безопасность включается на уровне проекта (в
.csproj) или директивой#nullable enable.
int? может быть null, а int — нет. Теперь то же самое и для ссылочных!?. (null-conditional) и ?? (null-coalescing)
C# предлагает два мощных оператора для безопасной работы с null. Они позволяют писать лаконичный и безопасный код без громоздких if-проверок.
Позволяет безопасно обращаться к членам объекта. Если объект null — вся цепочка прерывается и возвращается null.
Person? person = GetPerson();
// Без ?. — нужно писать проверку:
if (person != null)
{
Console.WriteLine(person.Name);
}
// С ?. — лаконично и безопасно:
Console.WriteLine(person?.Name); // если person null, то ничего не произойдёт
// Цепочка ?.:
string? cityName = person?.Address?.City?.Name;
// Если любой из них null → cityName = null
// Для вызова методов:
person?.PrintInfo(); // если person не null — вызовется
Возвращает левый операнд, если он не null, иначе — правый. Идеально для значений по умолчанию.
string? input = GetUserInput();
string result = input ?? "Значение по умолчанию";
// Если input null → используем "Значение по умолчанию"
// Комбинация ?. и ??:
int? length = person?.Name?.Length ?? 0;
// Если person или Name null → length = 0
// ??= (C# 8+): присвоить, если null
string? name = null;
name ??= "Гость"; // name теперь "Гость"
name ??= "Админ"; // name уже не null, ничего не изменится
Проверка на null: is null, is not null
В C# есть несколько способов проверить значение на null. Самый современный — использовать паттерн-матчинг с is null и is not null.
string? text = GetText();
// Стиль 1: классический (C# 1.0+)
if (text != null) { /* ... */ }
// Стиль 2: is null (C# 7.0+)
if (text is null) { /* ... */ }
// Стиль 3: is not null (C# 9.0+)
if (text is not null) { /* ... */ }
// Стиль 4: паттерн-матчинг (C# 9.0+)
if (text is string) { /* text не null и доступен */ }
// Стиль 5: null-coalescing
string result = text ?? "default";
// Стиль 6: null-conditional
int? length = text?.Length;
Рекомендация:
Для простой проверки используй is not null — он лучше читается. Для присвоения значения по умолчанию — ??. Для безопасного доступа к цепочкам — ?..
int nonNullable = 42; // int не может быть null
int? nullable = null; // int? может быть null
// int? — это на самом деле Nullable<int>
// У nullable типов есть свойства:
if (nullable.HasValue)
{
Console.WriteLine(nullable.Value); // безопасное получение значения
}
// Преобразование:
int regular = nullable ?? 0; // null → 0
int another = nullable.GetValueOrDefault(); // то же самое
Атрибуты для аннотаций
В C# есть атрибуты, которые помогают компилятору и анализаторам понимать семантику null. Они находятся в пространстве имён System.Diagnostics.CodeAnalysis.
using System.Diagnostics.CodeAnalysis;
public class UserService
{
// [NotNull] — метод не возвращает null
[return: NotNull]
public string GetUserName(int id)
{
return "Анна";
}
// [MaybeNull] — метод может вернуть null
[return: MaybeNull]
public string? FindUser(int id)
{
// может быть null
return null;
}
// [NotNullWhen(true)] — если метод вернул true, параметр не null
public bool TryGetUser(int id, [NotNullWhen(true)] out User? user)
{
if (id > 0)
{
user = new User();
return true;
}
user = null;
return false;
}
}
Мини-тест
Вопрос 1: Что означает string? в C# 8+?
Правильный ответ: B
Вопрос 2: Что делает оператор ??
Правильный ответ: C
Вопрос 3: Что выведет код?
string? s = null;
Console.WriteLine(s?.Length ?? -1);
Правильный ответ: C
Вопрос 4: Верно ли, что var изменяет тип переменной во время выполнения?
Правильный ответ: C
Вопрос 5: Какой синтаксис для безопасного вызова метода, если объект может быть null?
Правильный ответ: B
Задание
Система поиска пользователей с null-безопасностью
Создай консольное приложение с классами User и UserDatabase:
- Класс
Userсо свойствами:Name(string — non-nullable),Email(string? — может быть null),Age(int). - Класс
UserDatabaseсо словарёмDictionary<int, User>? _users. - Метод
FindUser(int id)возвращаетUser?(может не найти). - Метод
GetEmailOrPlaceholder(int id)— возвращает email или "Нет email". - Метод
PrintUserNameSafe(int id)— выводит имя, если пользователь найден, через?.. - В Main() добавь несколько пользователей, попробуй найти существующего и несуществующего, используй все изученные операторы.
Цель
Отработать nullable reference types, ?., ??, is not null на реальных сценариях.
Сложность
Лёгкая
Что важно запомнить
string? — может быть null, string — не должен быть null. Компилятор помогает соблюдать контракт.?. — безопасный доступ (ничего не делает при null), ?? — значение по умолчанию, ??= — присвоить если null.is null и is not null для читаемости. Атрибуты [NotNull], [MaybeNull] помогают компилятору.Тест: 8.7: Аннотации типов и null-безопасность
3 вопроса