Я можу зробити обґрунтовану здогадку щодо того, що тут відбувається, але це все трохи складніше :) Це включає стан нуля та відстеження нуля, описане в проекті специфікації . Принципово, в тому місці, куди ми хочемо повернутися, компілятор попередить, якщо стан виразу "можливо нульовий" замість "не нульовий".
Ця відповідь є дещо оповідною формою, а не просто "ось висновки" ... Я сподіваюся, що це корисніше саме так.
Я збираюся трохи спростити приклад, позбувшись полів, і розгляну метод з одним із цих двох підписів:
public static string M(string? text)
public static string M(string text)
У нижчезазначених реалізаціях я дав кожному методу різну кількість, тому я можу однозначно посилатися на конкретні приклади. Це також дозволяє всі реалізації реалізувати в одній програмі.
У кожному з описаних нижче випадків ми будемо робити різні речі, але в кінцевому підсумку намагаємося повернутися text
- так це text
важливий нульовий стан .
Безумовне повернення
Спочатку спробуємо повернути його безпосередньо:
public static string M1(string? text) => text; // Warning
public static string M2(string text) => text; // No warning
Поки так просто. Стан нульового параметра параметра на початку методу - "можливо нульовий", якщо він має тип string?
і "не нульовий", якщо він типу string
.
Просте умовне повернення
Тепер перевіримо наявність нуля в самій if
умові заяви. (Я використовував би умовного оператора, який, я вважаю, матиме такий же ефект, але я хотів залишатися вірніше питання.)
public static string M3(string? text)
{
if (text is null)
{
return "";
}
else
{
return text; // No warning
}
}
public static string M4(string text)
{
if (text is null)
{
return "";
}
else
{
return text; // No warning
}
}
Чудово, так це виглядає як в if
операторі, де сама умова перевіряє недійсність, стан змінної в межах кожної гілки if
висловлення може бути різним: всередині else
блоку стан "не нульовий" в обох фрагментах коду. Так, зокрема, у M3 стан змінюється з "можливо null" на "not null".
Умовне повернення з локальною змінною
Тепер спробуємо приєднати цю умову до локальної змінної:
public static string M5(string? text)
{
bool isNull = text is null;
if (isNull)
{
return "";
}
else
{
return text; // Warning
}
}
public static string M6(string text)
{
bool isNull = text is null;
if (isNull)
{
return "";
}
else
{
return text; // Warning
}
}
І M5, і M6 видають попередження. Таким чином, ми не тільки отримуємо позитивний ефект зміни стану з "можливо нульового" на "не нульового" в М5 (як це робилося в М3) ... ми отримуємо протилежний ефект у М6, звідки йде стан " не null "до" можливо null ". Це дійсно мене здивувало.
Так виглядає, що ми дізналися про це:
- Логіка навколо "як була обчислена локальна змінна" не використовується для поширення інформації про стан. Детальніше про це пізніше.
- Введення нульового порівняння може попередити компілятора про те, що те, що раніше вважалося не нульовим, може бути зрештою нульовим.
Безумовне повернення після ігнорованого порівняння
Давайте розглянемо другий з цих пунктів, вводячи порівняння перед безумовною віддачею. (Отже, ми повністю ігноруємо результат порівняння.):
public static string M7(string? text)
{
bool ignored = text is null;
return text; // Warning
}
public static string M8(string text)
{
bool ignored = text is null;
return text; // Warning
}
Зауважте, як M8 відчуває, що він повинен бути еквівалентний M2 - обидва мають ненульовий параметр, який вони повертають беззастережно - але введення порівняння з null змінює стан з "not null" на "можливо null". Ми можемо отримати додаткові докази цього, спробувавши скинути за мету text
перед умовою:
public static string M9(string text)
{
int length1 = text.Length; // No warning
bool ignored = text is null;
int length2 = text.Length; // Warning
return text; // No warning
}
Зауважте, як у return
оператора зараз немає попередження: стан після його виконання text.Length
є "не нульовим" (тому що якщо ми виконаємо це вираз успішно, воно не може бути нульовим). Тож text
параметр починається як "not null" через свій тип, стає "можливо null" через порівняння з null, потім знову стає "not null" після text2.Length
.
Які порівняння впливають на стан?
Отже, це порівняння text is null
... на що впливають подібні порівняння? Ось ще чотири способи, починаючи з ненульового параметра рядка:
public static string M10(string text)
{
bool ignored = text == null;
return text; // Warning
}
public static string M11(string text)
{
bool ignored = text is object;
return text; // No warning
}
public static string M12(string text)
{
bool ignored = text is { };
return text; // No warning
}
public static string M13(string text)
{
bool ignored = text != null;
return text; // Warning
}
Так що , хоча x is object
в даний час є рекомендованим альтернативою x != null
, вони не мають такий же ефект: тільки порівняння з нулем (з будь-яким is
, ==
або !=
) змінює стан з «Не нуль» до «може бути нулем».
Чому підйом стану має вплив?
Повертаючись до нашого першого пункту раніше, чому M5 та M6 не враховують умову, яка призвела до локальної змінної? Це мене не так дивує, як здається, дивує інших. Впровадження такої логіки у компілятор та специфікацію - це велика робота та порівняно невелика користь. Ось ще один приклад, що не має нічого спільного з заміною, коли вбудовування чогось має ефект:
public static int X1()
{
if (true)
{
return 1;
}
}
public static int X2()
{
bool alwaysTrue = true;
if (alwaysTrue)
{
return 1;
}
// Error: not all code paths return a value
}
Хоча ми знаємо, що alwaysTrue
це завжди буде правдою, він не задовольняє вимогам специфікації, які роблять код після if
оператора недоступним, що саме нам і потрібно.
Ось ще один приклад щодо певного призначення:
public static void X3()
{
string x;
bool condition = DateTime.UtcNow.Year == 2020;
if (condition)
{
x = "It's 2020.";
}
if (!condition)
{
x = "It's not 2020.";
}
// Error: x is not definitely assigned
Console.WriteLine(x);
}
Незважаючи на те, що ми знаємо, що код буде входити саме в один із цих if
органів тверджень, в специфікації немає нічого, що б це виправити. Засоби статичного аналізу цілком можуть це зробити, але намагатися вкласти це в специфікацію мови було б поганою ідеєю, ІМО - це чудово, щоб інструменти статичного аналізу мали всі види евристики, які можуть розвиватися з часом, але не так сильно для мовної специфікації.