Dlaczego otrzymujemy ostrzeżenie o zerowym odwołaniu, gdy odwołanie zerowe wydaje się niemożliwe?

9

Po przeczytaniu tego pytania na temat HNQ, zacząłem czytać o Nullable Reference Type w C # 8 i przeprowadziłem kilka eksperymentów.

Mam świadomość, że 9 razy na 10, a nawet częściej, gdy ktoś mówi: „Znalazłem błąd kompilatora!” dzieje się tak zgodnie z projektem i własnym nieporozumieniem. A ponieważ zacząłem przyglądać się tej funkcji dopiero dzisiaj, najwyraźniej nie bardzo dobrze ją rozumiem. W ten sposób, spójrzmy na ten kod:

#nullable enable
class Program
{
    static void Main()
    {
        var s = "";
        var b = s == null; // If you comment this line out, the warning on the line below disappears
        var i = s.Length; // warning CS8602: Dereference of a possibly null reference
    }
}

Po przeczytaniu dokumentacji, do której się powiodłem, spodziewałbym się, że s == nulllinia ostrzeże mnie - w końcu swyraźnie nie można jej zerwać, więc porównywanie jej z nulltym nie ma sensu.

Zamiast tego otrzymuję ostrzeżenie w następnym wierszu, a ostrzeżenie mówi, że smożliwe jest odwołanie zerowe, chociaż dla człowieka jest oczywiste, że tak nie jest.

Ponad to, ostrzeżenie jest nie wyświetlany jeśli nie porównać sdo null.

Zrobiłem trochę googlingu i natknąłem się na problem GitHub , który okazał się dotyczyć czegoś zupełnie innego, ale w trakcie tego procesu przeprowadziłem rozmowę z współpracownikiem, która dała więcej wglądu w to zachowanie (np. „Czeki zerowe są często użytecznym sposobem mówiąca kompilatorowi, aby zresetował swoje wcześniejsze wnioskowanie na temat dopuszczalności zmiennej. ” ). Pozostało mi jednak główne pytanie bez odpowiedzi.

Zamiast tworzyć nowy problem na GitHub i potencjalnie zajmować czas niewiarygodnie zajętym współpracownikom projektu, udostępniam to społeczności.

Czy możesz mi wyjaśnić, co się dzieje i dlaczego? W szczególności dlaczego nie generuje się żadnych ostrzeżeń na s == nulllinii i dlaczego mamy, CS8602skoro nie wydaje się, że nullmożliwe jest odniesienie? Jeśli wnioskowanie o zerowanie nie jest kuloodporne, jak sugeruje połączony wątek GitHub, jak może pójść nie tak? Jakie byłyby tego przykłady?

Andrew Savinykh
źródło
Wydaje się, że sam kompilator ustawia zachowanie, że w tym momencie zmienna „s” może mieć wartość null. W każdym razie, jeśli kiedykolwiek użyję ciągów znaków lub obiektów, zawsze należy sprawdzić przed wywołaniem funkcji. „s? .Length” powinno załatwić sprawę, a samo ostrzeżenie powinno zniknąć.
chg
1
@chg, nie powinno być takiej potrzeby, ?ponieważ snie ma wartości zerowej. Nie staje się zerowalne, po prostu dlatego, że byliśmy wystarczająco głupi, aby go porównać null.
Andrew Savinykh
Podążałem za wcześniejszym pytaniem (przepraszam, nie mogę go znaleźć), w którym postawiono, że jeśli dodasz sprawdzenie, że wartość jest pusta, to kompilator przyjmie to jako „wskazówkę”, że wartość może być pusta, nawet jeśli wyraźnie nie jest przypadkiem.
stuartd
@stuartd, tak, wydaje się, że tak właśnie jest. Więc teraz pytanie brzmi: dlaczego to jest przydatne?
Andrew Savinykh
1
@ chg, cóż, tak mówię w pytaniu, prawda?
Andrew Savinykh

Odpowiedzi:

7

Jest to w rzeczywistości duplikat odpowiedzi, którą łączy @stuartd, więc nie będę tutaj wchodził w bardzo głębokie szczegóły. Ale sedno sprawy polega na tym, że nie jest to ani błąd językowy, ani błąd kompilatora, ale jego zamierzone zachowanie jest dokładnie takie, jak zaimplementowane. Śledzimy stan zerowy zmiennej. Gdy początkowo deklarujesz zmienną, stan ten to NotNull, ponieważ jawnie inicjujesz ją wartością inną niż null. Ale nie śledzimy, skąd pochodzi ten NotNull. Jest to na przykład kod równoważny:

#nullable enable
class Program
{
    static void Main()
    {
        M("");
    }
    static void M(string s)
    {
        var b = s == null;
        var i = s.Length; // warning CS8602: Dereference of a possibly null reference
    }
}

W obu przypadkach, gdy wyraźnie przetestować sna null. Bierzemy to jako wkład do analizy przepływu, tak jak Mads odpowiedział na to pytanie: https://stackoverflow.com/a/59328672/2672518 . W wyniku tej odpowiedzi otrzymasz ostrzeżenie o powrocie. W takim przypadku odpowiedzią jest, że pojawi się ostrzeżenie, że wyrejestrowałeś potencjalnie zerowe odwołanie.

Nie staje się zerowalne, po prostu dlatego, że byliśmy wystarczająco głupi, aby go porównać null.

Tak, to prawda. Do kompilatora. Jako ludzie możemy przyjrzeć się temu kodowi i oczywiście zrozumieć, że nie może on wygenerować wyjątku zerowego odwołania. Ale sposób, w jaki analiza nullable flow jest implementowana w kompilatorze, nie może. Omówiliśmy pewne ulepszenia w tej analizie, w których dodaliśmy dodatkowe stany w oparciu o to, skąd ta wartość pochodzi, ale zdecydowaliśmy, że zwiększyło to złożoność wdrożenia, nie dając dużego zysku, ponieważ są to jedyne miejsca, w których przydałoby się to w takich przypadkach, gdy użytkownik inicjuje zmienną o newwartości stałej lub stałej, a następnie sprawdza ją null.

333fred
źródło
Dziękuję Ci. Dotyczy to głównie podobieństw z drugim pytaniem. Chciałbym bardziej zająć się różnicami. Na przykład, dlaczego s == nullnie wyświetla ostrzeżenia?
Andrew Savinykh,
Właśnie zdałem sobie z tego sprawę #nullable enable; string s = "";s = null;kompiluje i działa (nadal generuje ostrzeżenie) jakie są korzyści z implementacji, która pozwala przypisać wartość NULL do „niewymiennego odwołania” w włączonym kontekście adnotacji zerowej?
Andrew Savinykh
Odpowiedź Mad'a koncentruje się na tym, że „[kompilator] nie śledzi związku między stanem odrębnych zmiennych”, w tym przykładzie nie mamy oddzielnych zmiennych, więc mam problem z zastosowaniem pozostałej odpowiedzi Mad'a w tym przypadku.
Andrew Savinykh,
Jest rzeczą oczywistą, ale pamiętajcie, że nie jestem tutaj, aby krytykować, ale uczyć się. Używam C # od pierwszego wydania w 2001 roku. Mimo że nie jestem nowy w języku, sposób, w jaki kompilator się zachowuje, był dla mnie zaskakujący. Celem tego pytania jest uzasadnienie, dlaczego takie zachowanie jest przydatne dla ludzi.
Andrew Savinykh
Istnieją ważne powody do sprawdzenia s == null. Być może jesteś na przykład metodą publiczną i chcesz przeprowadzić weryfikację parametrów. A może używasz biblioteki, która zawiera niepoprawne adnotacje, i dopóki nie naprawią tego błędu, musisz zająć się wartością zerową, jeśli nie została zadeklarowana. W obu przypadkach byłoby to złe doświadczenie, gdybyśmy go ostrzegali. Co do umożliwienia przypisania: adnotacje zmiennych lokalnych służą tylko do odczytu. W ogóle nie wpływają na środowisko uruchomieniowe. W rzeczywistości umieściliśmy wszystkie te ostrzeżenia w jednym kodzie błędu, abyś mógł je wyłączyć, jeśli chcesz zmniejszyć rezygnację z kodu.
333fred
0

Jeśli wnioskowanie o zerowaniu nie jest kuloodporne, [...] jak może się nie udać?

Z radością przyjąłem zerowane odniesienia do C # 8, gdy tylko były dostępne. Ponieważ byłem przyzwyczajony do używania notacji ReSharper [NotNull] (itp.), Zauważyłem pewne różnice między nimi.

Kompilator C # można oszukać, ale ma tendencję do bycia ostrożnym (zwykle nie zawsze).

Dla odniesienia dla przyszłych gości są to scenariusze, w których kompilator jest dość zdezorientowany (zakładam, że wszystkie te przypadki są zgodne z projektem):

  • Null wybaczający null . Często używane, aby uniknąć ostrzeżenia o dereferencji, ale nie dopuszczać do zerowania obiektu. Wygląda na to, że chcesz trzymać stopę w dwóch butach.
    string s = null!; //No warning

  • Analiza powierzchni . W przeciwieństwie do ReSharper (który robi to za pomocą adnotacji kodu ), kompilator C # nadal nie obsługuje pełnego zakresu atrybutów do obsługi zerowalnych odwołań.
    void DoSomethingWith(string? s)
    {    
        ThrowIfNull(s);
        var split = s.Split(' '); //Dereference warning
    }

Pozwala jednak użyć jakiegoś konstruktu do sprawdzenia nullability, który także pozbywa się ostrzeżenia:

    public static void DoSomethingWith(string? s)
    {
        Debug.Assert(s != null, nameof(s) + " != null");
        var split = s.Split(' ');  //No warning
    }

lub (wciąż całkiem fajne) atrybuty (znajdź je wszystkie tutaj ):

    public static bool IsNullOrEmpty([NotNullWhen(false)] string? value)
    {
        ...
    }

  • Analiza podatnego kodu . To właśnie pokazałeś. Kompilator musi przyjąć założenia, aby działać, a czasem mogą wydawać się sprzeczne z intuicją (przynajmniej dla ludzi).
    void DoSomethingWith(string s)
    {    
        var b = s == null;
        var i = s.Length; // Dereference warning
    }

  • Problemy z lekami generycznymi . Zapytano tutaj i wyjaśniono bardzo dobrze tutaj (ten sam artykuł, co poprzednio, akapit „Problem z T?”). Generyczne są skomplikowane, ponieważ muszą uszczęśliwić zarówno referencje, jak i wartości. Główną różnicą jest to, że chociaż string?jest tylko ciągiem, to int?staje się Nullable<int>i zmusza kompilator do obsługi ich na zasadniczo różne sposoby. Również tutaj kompilator wybiera bezpieczną ścieżkę, zmuszając cię do określenia tego, czego oczekujesz:
    public interface IResult<out T> : IResult
    {
        T? Data { get; } //Warning/Error: A nullable type parameter must be known to be a value type or non-nullable reference type.
    }

Rozwiązane dające ograniczenia:

    public interface IResult<out T> : IResult where T : class { T? Data { get; }}
    public interface IResult<T> : IResult where T : struct { T? Data { get; }}

Ale jeśli nie użyjemy ograniczeń i nie usuniemy „?” z danych nadal jesteśmy w stanie umieścić w nim wartości zerowe za pomocą słowa kluczowego „default”:

    [Pure]
    public static Result<T> Failure(string description, T data = default)
        => new Result<T>(ResultOutcome.Failure, data, description); 
        // data here is definitely null. No warning though.

Ten ostatni wydaje mi się trudniejszy, ponieważ pozwala napisać niebezpieczny kod.

Mam nadzieję, że to komuś pomoże.

Alvin Sartor
źródło
Sugeruję, aby przeczytać dokumenty dotyczące atrybutów zerowalnych przeczytać: docs.microsoft.com/en-us/dotnet/csharp/nullable-attributes . Rozwiążą niektóre z twoich problemów, szczególnie z sekcją analizy powierzchni i ogólnymi.
333fred
Dzięki za link @ 333fred. Chociaż istnieją atrybuty, którymi można się bawić, atrybut, który rozwiązuje problem, który opublikowałem (coś, co mówi mi, że ThrowIfNull(s);zapewnia, że snie jest zerowy), nie istnieje. Również artykuł wyjaśnia, jak obsługiwać generyczne niepozwalające na zerowanie, podczas gdy ja pokazywałem, jak można „oszukać” kompilator, mając wartość zerową, ale bez ostrzeżenia o tym.
Alvin Sartor
W rzeczywistości atrybut istnieje. Złożyłem błąd w dokumentach, aby go dodać. Szukasz DoesNotReturnIf(bool).
333fred
@ 333fred faktycznie szukam czegoś podobnego DoesNotReturnIfNull(nullable).
Alvin Sartor