Mogę rozsądnie zgadnąć, co się tutaj dzieje, ale to wszystko jest trochę skomplikowane :) Obejmuje stan zerowy i śledzenie zerowe opisane w specyfikacji roboczej . Zasadniczo w miejscu, w którym chcemy powrócić, kompilator ostrzeże, jeśli stan wyrażenia to „może null” zamiast „not null”.
Ta odpowiedź ma nieco narracyjną formę, a nie tylko „oto wnioski” ... Mam nadzieję, że w ten sposób będzie bardziej przydatna.
Uproszczę nieco ten przykład, pozbywając się pól i rozważę metodę z jednym z tych dwóch podpisów:
public static string M(string? text)
public static string M(string text)
W poniższych implementacjach podałem każdej metodzie inny numer, aby móc jednoznacznie odwoływać się do konkretnych przykładów. Pozwala także na obecność wszystkich implementacji w tym samym programie.
W każdym z opisanych poniżej przypadków zrobimy różne rzeczy, ale w końcu spróbujemy wrócić text
- więc text
ważny jest stan zerowy .
Bezwarunkowy zwrot
Najpierw spróbujmy zwrócić go bezpośrednio:
public static string M1(string? text) => text; // Warning
public static string M2(string text) => text; // No warning
Do tej pory takie proste. Stan zerowania parametru na początku metody to „może null”, jeśli jest typu, string?
i „nie jest null”, jeśli jest typu string
.
Prosty warunkowy zwrot
Teraz sprawdźmy, czy w samym if
warunku instrukcji nie ma wartości null . (Chciałbym użyć operatora warunkowego, który, jak sądzę, będzie miał taki sam efekt, ale chciałem pozostać bardziej wierny temu pytaniu.)
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
}
}
Świetnie, więc wygląda na to, że w if
instrukcji, w której sam warunek sprawdza zerowość, stan zmiennej w każdej gałęzi if
instrukcji może być inny: w else
bloku stan nie jest „null” w obu częściach kodu. W szczególności w M3 stan zmienia się z „może zerowy” na „nie zerowy”.
Warunkowy powrót z lokalną zmienną
Teraz spróbujmy podnieść ten warunek do zmiennej lokalnej:
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
}
}
Zarówno M5, jak i M6 wydają ostrzeżenia. Więc nie tylko nie uzyskujemy pozytywnego efektu zmiany stanu z „może zerowy” na „nie zerowy” w M5 (tak jak w M3) ... otrzymujemy odwrotnie efekt w M6, skąd stan przechodzi z „ not null ”na„ może null ”. To mnie naprawdę zaskoczyło.
Wygląda na to, że dowiedzieliśmy się, że:
- Logika wokół „jak została obliczona zmienna lokalna” nie jest używana do propagowania informacji o stanie. Więcej o tym później.
- Wprowadzenie porównania zerowego może ostrzec kompilator, że coś, co wcześniej uważało, że nie jest zerowe, może w końcu być zerowe.
Bezwarunkowy zwrot po zignorowanym porównaniu
Spójrzmy na drugi z tych punktów, wprowadzając porównanie przed bezwarunkowym powrotem. (Więc całkowicie ignorujemy wynik porównania.):
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
}
Zauważ, że M8 wydaje się, że powinien być równoważny M2 - oba mają parametr inny niż null, który bezwarunkowo zwracają - ale wprowadzenie porównania z null zmienia stan z „not null” na „może null”. Możemy uzyskać dalsze dowody na to, próbując wywnioskować, text
zanim warunek:
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
}
Zwróć uwagę, że return
instrukcja nie ma teraz ostrzeżenia: stan po wykonaniu text.Length
ma wartość „nie jest zerowa” (ponieważ jeśli wykonamy to wyrażenie pomyślnie, nie może być zerowy). Tak więc text
parametr zaczyna się od „not null” ze względu na jego typ, staje się „może null” ze względu na porównanie wartości null, a następnie ponownie staje się „not null” text2.Length
.
Jakie porównania wpływają na stan?
To porównanie text is null
... jaki wpływ mają podobne porównania? Oto cztery kolejne metody, wszystkie zaczynające się od niepozwalającego na podanie parametru ciągu:
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
}
Dlatego, mimo że x is object
jest obecnie zalecana alternatywa x != null
, nie mają one ten sam efekt: tylko porównanie z wartością null (z dowolnego is
, ==
lub !=
) zmienia stan z „NOT NULL” do „Może null”.
Dlaczego podniesienie warunku ma wpływ?
Wracając do naszego pierwszego punktu wcześniejszego, dlaczego M5 i M6 nie biorą pod uwagę stanu, który doprowadził do zmiennej lokalnej? Nie zaskakuje mnie to tak bardzo, jak wydaje się zaskakiwać innych. Wbudowanie tego rodzaju logiki w kompilator i specyfikację jest bardzo pracochłonne i przynosi stosunkowo niewielkie korzyści. Oto kolejny przykład, który nie ma nic wspólnego z zerowalnością, w której wprowadzenie czegoś ma wpływ:
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
}
Choć my wiemy, że alwaysTrue
zawsze będzie prawdziwe, to nie spełnia wymogów określonych w specyfikacji, które sprawiają, że kod po if
oświadczeniu nieosiągalnym, czyli to, czego potrzebujemy.
Oto kolejny przykład dotyczący określonego przypisania:
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);
}
Choć my wiemy, że kod wejdzie dokładnie jeden z tych if
organów rachunku, nie ma nic w specyfikacji do pracy, która na zewnątrz. Narzędzia do analizy statycznej mogą być w stanie to zrobić, ale próba wprowadzenia tego do specyfikacji języka byłaby złym pomysłem, IMO - dobrze, że narzędzia do analizy statycznej mają wszystkie rodzaje heurystyki, które mogą ewoluować w czasie, ale nie tak bardzo dla specyfikacji języka.