1. Вступ
Уявіть, що програмний код — це вистава, а змінні — актори. Кожного разу, коли змінна потрапляє у нову сцену (блок коду), вона може бути «живою» (має значення) або «привидом» (null). Але ось халепа: глядач — компілятор — дивиться виставу не наживо, а за сценарієм. Він бачить, де акторам видають ролі (присвоюють значення), а де вони зникають (стають null). Та зазирнути в реальність (виконання програми) він не може — лише здогадуватися з тексту сценарію!
Статичний аналіз потоку даних — це процес, за якого компілятор перевіряє код ще до запуску програми, щоб з’ясувати, чи не намагається хтось зіграти сцену з «падінням NullReferenceException».
Суть процесу
Починаючи з C# 8.0 та появи режиму Nullable Reference Types (NRT), компілятор аналізує, чи може змінна опинитися null у якійсь гілці виконання. І якщо так — він одразу повідомляє про це. Він не дозволить вам «використати імʼя актора, який раптом став привидом».
2. Як читати попередження компілятора
Основні категорії попереджень
Коли активовано режим NRT, компілятор особливо уважно стежить за змінними посилального типу (string, User, масиви, обʼєкти). Ось найпоширеніші типи попереджень:
- Possible null assignment — ви намагаєтеся присвоїти значення, яке може бути null, змінній, що null не допускає.
- Dereference of a possibly null reference — ви звертаєтеся до члена обʼєкта, але компілятор сумнівається, що той не дорівнює null.
- Possible null return — метод може повернути null, хоча за сигнатурою має повертати значення.
- Nullable object must have a value — ви використовуєте .Value у nullable-типу без перевірки.
- Unassigned non-nullable property — ви не надали значення полю/властивості, що не допускає null.
Зазвичай у IDE (наприклад, у Rider або Visual Studio) такі попередження підкреслюються хвилястою лінією, а коли наводите вказівник миші — з’являється пояснення.
Приклад типового попередження
#nullable enable
string? possibleNull = GetString();
// Ой-ой, компілятор попереджає: "Dereference of a possibly null reference"
int length = possibleNull.Length; // Попередження!
Компілятор не впевнений, що possibleNull не дорівнює null, а отже — звернення до .Length може призвести до винятку.
Якщо метод виглядає так:
string? GetString() { ... }
А ви пишете:
string nonNullable = GetString();
Тоді отримаєте попередження: можливе небезпечне присвоєння null змінній, що не допускає null.
3. Компілятор аналізує ваш код
Приклад 1: прямолінійне присвоєння
string? name = null;
Console.WriteLine(name.Length); // Попередження: можливе звернення до null
Тут усе очевидно: name дорівнює null, тож спроба звернення до властивості — потенційно небезпечна.
Приклад 2: перевірка на null
string? name = GetUserName();
if (name != null)
{
Console.WriteLine(name.Length); // Усе гаразд!
}
else
{
Console.WriteLine("Імʼя не вказано!");
}
Компілятор розуміє, що всередині блоку if (name != null) змінна вважається безпечною. Це і є flow analysis — аналіз потоку значень залежно від умов.
4. Компілятор не може зазирнути «усередину методу»
Він дивиться лише на сигнатуру: якщо зазначено, що метод повертає string?, він вірить, що може бути null, навіть коли всередині завжди повертається рядок.
Приклад 3: повернення значення з методу
string? GetMaybeName(bool useName)
{
if (useName)
return "Code Jedi";
else
return null;
}
void PrintLength()
{
string? name = GetMaybeName(false);
Console.WriteLine(name.Length); // Попередження!
}
Компілятор бачить, що GetMaybeName може повернути null, тому просить вас бути обережнішими.
5. Поширені сценарії та попередження
Присвоєння nullable у non-nullable змінну
string? maybeUser = GetUser();
string alwaysUser = maybeUser; // warning: possible null assignment
Щоб прибрати попередження:
string alwaysUser = maybeUser ?? "Гість";
Тепер завжди буде рядок — або з методу, або «Гість».
Забули перевірити .HasValue у nullable value-типу
int? age = GetAge();
int realAge = age.Value; // warning: можливе звернення до null
Можна так:
if (age.HasValue)
Console.WriteLine(age.Value);
Або:
int realAge = age ?? -1;
Автоматичні властивості без ініціалізації
class User
{
public string Name { get; set; } // warning: не ініціалізовано!
}
Варіанти розв’язання:
public string Name { get; set; } = "Без імені";
або
public string? Name { get; set; }
6. Складні випадки аналізу потоку
Аналіз у циклах
string? text = null;
while (text == null)
{
text = Console.ReadLine();
}
Console.WriteLine(text.Length); // усе гаразд: компілятор це зрозумів!
Різні шляхи присвоєння
string? name;
if (Random.Shared.Next() % 2 == 0)
name = "Alice";
else
name = null;
Console.WriteLine(name.Length); // warning: possible null dereference
Компілятор аналізує всі гілки. Якщо бодай в одній — null, він попереджає.
7. Як «заспокоїти» компілятор (і коли цього не варто робити)
Іноді ви впевнені, що значення не може бути null, але компілятор не вірить:
string? value = GetSomething();
if (value == null)
throw new Exception("Очікувалося значення!");
Console.WriteLine(value.Length); // Попередження зникає
Або ви користуєтеся оператором !:
string? value = GetSomething();
Console.WriteLine(value!.Length); // Без попереджень, але потенційно небезпечно
Це називають null-forgiving operator. Він нічого не перевіряє — лише «каже» компілятору: «заспокойся, усе під контролем». Але якщо ви помилитеся, під час виконання отримаєте виняток.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ