$ sudo teach IT
Модуль 8 · Функции

Урок 8.7 — Аннотации типов и null-безопасность

Строгая типизация C#, var, nullable reference types и защита от null.

Одна из самых частых ошибок в программировании — обращение к null там, где ожидается объект. C# с версии 8.0 ввёл мощные средства для борьбы с этой проблемой: nullable reference types, null-conditional операторы и анализатор null-безопасности. В этом уроке ты научишься писать код, в котором null-ошибки практически невозможны.

🏷️

Строгая типизация в C#

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
💡 Строгая типизация — это плюс C#. Она предотвращает целые классы ошибок на этапе компиляции, до того как программа попадёт к пользователю.
🔄

Ключевое слово var

var — это не «тип-невидимка», а синтаксический сахар. Компилятор самостоятельно определяет тип переменной на основе выражения справа. Тип фиксируется на этапе компиляции и не может измениться.

C# · 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 — с помощью символа ?.

C# · string? vs string
// 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, double, bool) nullable всегда был: int? может быть null, а int — нет. Теперь то же самое и для ссылочных!
🔗

?. (null-conditional) и ?? (null-coalescing)

C# предлагает два мощных оператора для безопасной работы с null. Они позволяют писать лаконичный и безопасный код без громоздких if-проверок.

?. Null-conditional (оператор Элвиса)

Позволяет безопасно обращаться к членам объекта. Если объект null — вся цепочка прерывается и возвращается null.

C# · ?.
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-coalescing (объединение с null)

Возвращает левый операнд, если он не null, иначе — правый. Идеально для значений по умолчанию.

C# · ??
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.

C# · Способы проверки на 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 — он лучше читается. Для присвоения значения по умолчанию — ??. Для безопасного доступа к цепочкам — ?..

C# · 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.

C# · Атрибуты null-безопасности
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;
    }
}
💡 Эти атрибуты не изменяют поведение программы. Они служат подсказками для компилятора и IDE, чтобы выдавать более точные предупреждения.
❓

Мини-тест

Вопрос 1: Что означает string? в C# 8+?

A) Обязательно не-null строка
B) Строка, которая может быть null
C) Строка с обязательной проверкой
D) Динамическая строка

Правильный ответ: B

Вопрос 2: Что делает оператор ??

A) Проверяет тип переменной
B) Сокращает цепочку null-проверок
C) Возвращает левый операнд, если он не null, иначе правый
D) Выбрасывает исключение если null

Правильный ответ: C

Вопрос 3: Что выведет код?

string? s = null;
Console.WriteLine(s?.Length ?? -1);
A) 0
B) null
C) -1
D) Ошибка NullReferenceException

Правильный ответ: C

Вопрос 4: Верно ли, что var изменяет тип переменной во время выполнения?

A) Да, как в JavaScript
B) Да, при каждом присвоении
C) Нет, тип фиксируется на этапе компиляции
D) var — это динамический тип

Правильный ответ: C

Вопрос 5: Какой синтаксис для безопасного вызова метода, если объект может быть null?

A) obj!!.Method()
B) obj?.Method()
C) obj!.Method()
D) obj??Method()

Правильный ответ: B

🧪

Задание

Система поиска пользователей с null-безопасностью

Создай консольное приложение с классами User и UserDatabase:

  1. Класс User со свойствами: Name (string — non-nullable), Email (string? — может быть null), Age (int).
  2. Класс UserDatabase со словарём Dictionary<int, User>? _users.
  3. Метод FindUser(int id) возвращает User? (может не найти).
  4. Метод GetEmailOrPlaceholder(int id) — возвращает email или "Нет email".
  5. Метод PrintUserNameSafe(int id) — выводит имя, если пользователь найден, через ?..
  6. В Main() добавь несколько пользователей, попробуй найти существующего и несуществующего, используй все изученные операторы.

Цель

Отработать nullable reference types, ?., ??, is not null на реальных сценариях.

Сложность

Лёгкая

📌

Что важно запомнить

1️⃣
C# — строго типизированный язык. Тип каждой переменной известен на этапе компиляции.
2️⃣
var — не динамический тип. Компилятор выводит тип из правой части. Используй для очевидных случаев.
3️⃣
Nullable reference types: string? — может быть null, string — не должен быть null. Компилятор помогает соблюдать контракт.
4️⃣
Операторы: ?. — безопасный доступ (ничего не делает при null), ?? — значение по умолчанию, ??= — присвоить если null.
5️⃣
Проверка на null: используй is null и is not null для читаемости. Атрибуты [NotNull], [MaybeNull] помогают компилятору.

Тест: 8.7: Аннотации типов и null-безопасность

3 вопроса

Nullable

Premium