Jeśli utworzę bool w mojej klasie, po prostu coś takiego bool check
, domyślnie ma wartość false.
Kiedy tworzę ten sam bool w mojej metodzie bool check
(zamiast w klasie), pojawia się błąd „użycie nieprzypisanej kontroli zmiennej lokalnej”. Czemu?
c#
language-design
local-variables
nachime
źródło
źródło
Odpowiedzi:
Odpowiedzi Yuvala i Davida są w zasadzie poprawne; Podsumowując:
Komentator odpowiedzi Davida pyta, dlaczego niemożliwe jest wykrycie użycia nieprzypisanego pola za pomocą analizy statycznej; jest to punkt, który chcę rozwinąć w tej odpowiedzi.
Po pierwsze, dla jakiejkolwiek zmiennej, lokalnej lub innej, w praktyce niemożliwe jest dokładne określenie , czy zmienna jest przypisana, czy nieprzypisana. Rozważać:
Pytanie „czy x jest przypisane?” jest równoważne z „czy M () zwraca prawdę?” Teraz załóżmy, że M () zwraca prawdę, jeśli Ostatnie twierdzenie Fermata jest prawdziwe dla wszystkich liczb całkowitych mniejszych niż jedenaście gajillionów, a fałsz w przeciwnym razie. Aby ustalić, czy x jest definitywnie przypisane, kompilator musi zasadniczo przedstawić dowód ostatniego twierdzenia Fermata. Kompilator nie jest taki inteligentny.
Więc kompilator zamiast tego dla lokalnych implementuje algorytm, który jest szybki i przeszacowuje, gdy lokalna nie jest ostatecznie przypisana. Oznacza to, że ma kilka fałszywych alarmów, gdzie mówi „Nie mogę udowodnić, że ten lokalny jest przypisany”, mimo że ty i ja wiemy, że tak. Na przykład:
Załóżmy, że N () zwraca liczbę całkowitą. Ty i ja wiemy, że N () * 0 będzie równe 0, ale kompilator tego nie wie. (Uwaga: C # 2.0 kompilator nie wiem, ale usunąłem że optymalizacja, a specyfikacja nie powiedzieć , że kompilator wie, że).
W porządku, więc co wiemy do tej pory? Uzyskanie dokładnej odpowiedzi przez mieszkańców jest niepraktyczne, ale możemy tanio przecenić nieprzypisane wartości i uzyskać całkiem niezły wynik, który zawiera błędy po stronie „spraw, abyś naprawił swój niejasny program”. Dobre. Dlaczego nie zrobić tego samego dla pól? To znaczy stworzyć określony moduł sprawdzania zadań, który przecenia tanio?
Cóż, na ile jest sposobów inicjalizacji lokalnego? Można to przypisać w tekście metody. Można go przypisać wewnątrz lambda w tekście metody; że lambda może nigdy nie zostać wywołana, więc te przypisania nie są istotne. Lub można go przekazać jako „out” do innej metody, w którym to momencie możemy założyć, że jest przypisany, gdy metoda zwraca normalnie. Są to bardzo wyraźne punkty, w których przypisywany jest lokalny, i znajdują się tam w ten sam sposób, w jaki deklarowany jest lokalny . Określenie konkretnego przypisania dla lokalnych wymaga jedynie analizy lokalnej . Metody są zwykle krótkie - o wiele mniej niż milion wierszy kodu w metodzie - więc analiza całej metody jest dość szybka.
A co z polami? Pola można oczywiście zainicjować w konstruktorze. Lub inicjator pola. Lub konstruktor może wywołać metodę instancji, która inicjuje pola. Lub konstruktor może wywołać metodę wirtualną , która inicjalizuje pola. Lub konstruktor może wywołać metodę z innej klasy , która może znajdować się w bibliotece , która inicjuje pola. Pola statyczne można inicjować w konstruktorach statycznych. Pola statyczne mogą być inicjowane przez inne konstruktory statyczne.
Zasadniczo inicjator dla pola może znajdować się w dowolnym miejscu w całym programie , w tym wewnątrz metod wirtualnych, które zostaną zadeklarowane w bibliotekach, które nie zostały jeszcze napisane :
Czy kompilowanie tej biblioteki jest błędem? Jeśli tak, w jaki sposób BarCorp ma naprawić błąd? Przypisując wartość domyślną do x? Ale to już robi kompilator.
Załóżmy, że ta biblioteka jest legalna. Jeśli FooCorp pisze
Jest to błąd? Jak kompilator ma to rozgryźć? Jedynym sposobem jest wykonanie całej analizy programu, która śledzi statyczne inicjowanie każdego pola na każdej możliwej ścieżce w programie , w tym ścieżki, które obejmują wybór metod wirtualnych w czasie wykonywania . Ten problem może być arbitralnie trudny ; może obejmować symulowane wykonanie milionów ścieżek sterowania. Analiza lokalnych przepływów sterowania zajmuje mikrosekundy i zależy od rozmiaru metody. Analiza globalnych przepływów kontrolnych może zająć wiele godzin, ponieważ zależy to od złożoności każdej metody w programie i wszystkich bibliotek .
Dlaczego więc nie przeprowadzić tańszej analizy, która nie musi analizować całego programu, a po prostu jeszcze bardziej zawyżać? Cóż, zaproponuj algorytm, który działa i nie utrudnia napisania poprawnego programu, który faktycznie się kompiluje, a zespół projektowy może to rozważyć. Nie znam żadnego takiego algorytmu.
Teraz komentator sugeruje „wymagaj, aby konstruktor zainicjował wszystkie pola”. To nie jest zły pomysł. W rzeczywistości jest to niezły pomysł, że C # ma już tę funkcję dla struktur . Konstruktor struktury jest wymagany do ostatecznego przypisania wszystkich pól do czasu normalnego powrotu ctora; domyślny konstruktor inicjuje wszystkie pola do ich wartości domyślnych.
A co z zajęciami? Cóż, jak wiesz, że konstruktor został zainicjowany pole ? Ctor mógłby wywołać metodę wirtualną w celu zainicjowania pól, a teraz jesteśmy z powrotem w tej samej pozycji, w której byliśmy wcześniej. Struktury nie mają klas pochodnych; klasy mogą. Czy biblioteka zawierająca klasę abstrakcyjną musi zawierać konstruktora, który inicjuje wszystkie jej pola? Skąd klasa abstrakcyjna wie, jakie wartości powinny mieć pola?
John sugeruje po prostu zakaz wywoływania metod w ctor przed zainicjalizowaniem pól. Podsumowując, nasze opcje to:
Zespół projektowy wybrał trzecią opcję.
źródło
bool x;
tego równoważnymbool x = false;
nawet wewnątrz metody ?Ponieważ kompilator stara się zapobiec popełnieniu błędu.
Czy inicjalizacja zmiennej
false
zmienia cokolwiek w tej konkretnej ścieżce wykonania? Prawdopodobnie nie, rozważanie idefault(bool)
tak jest fałszywe, ale zmusza cię do bycia świadomym, że tak się dzieje. Środowisko .NET uniemożliwia dostęp do „pamięci śmieci”, ponieważ zainicjuje każdą wartość do wartości domyślnej. Mimo to wyobraź sobie, że był to typ referencyjny i przekazujesz niezainicjowaną (zerową) wartość do metody oczekującej wartości różnej od null i otrzymujesz NRE w czasie wykonywania. Kompilator po prostu próbuje temu zapobiec, akceptując fakt, że czasami może to skutkowaćbool b = false
wydaniem instrukcji.Eric Lippert mówi o tym w poście na blogu :
Dlaczego nie dotyczy to pola klasowego? Cóż, zakładam, że linia musiała gdzieś zostać narysowana, a inicjalizacja zmiennych lokalnych jest o wiele łatwiejsza do zdiagnozowania i uzyskania poprawności, w przeciwieństwie do pól klas. Kompilator mógłby to zrobić, ale pomyśl o wszystkich możliwych sprawdzeniach, które musiałby wykonać (gdzie niektóre z nich są niezależne od samego kodu klasy), aby ocenić, czy każde pole w klasie zostało zainicjowane. Nie jestem projektantem kompilatorów, ale jestem pewien, że byłoby to zdecydowanie trudniejsze, ponieważ jest wiele przypadków, które są brane pod uwagę i również muszą być wykonane w odpowiednim czasie . Dla każdej funkcji, którą musisz zaprojektować, napisać, przetestować i wdrożyć, a wartość jej wdrożenia w przeciwieństwie do włożonego wysiłku byłaby niegodna i skomplikowana.
źródło
Krótka odpowiedź jest taka, że kod uzyskujący dostęp do niezainicjowanych zmiennych lokalnych może zostać wykryty przez kompilator w niezawodny sposób, przy użyciu analizy statycznej. Ale tak nie jest w przypadku pól. Zatem kompilator wymusza pierwszy przypadek, ale nie drugi.
To nic więcej niż decyzja projektowa języka C #, jak wyjaśnił Eric Lippert . Środowisko CLR i .NET tego nie wymaga. Na przykład VB.NET skompiluje się dobrze z niezainicjowanymi zmiennymi lokalnymi, aw rzeczywistości CLR inicjalizuje wszystkie niezainicjowane zmienne do wartości domyślnych.
To samo mogłoby się zdarzyć z C #, ale projektanci języka zdecydowali się tego nie robić. Powodem jest to, że zainicjalizowane zmienne są ogromnym źródłem błędów, a więc wymuszając inicjalizację, kompilator pomaga ograniczyć przypadkowe błędy.
Dlaczego więc ta obowiązkowa jawna inicjalizacja nie ma miejsca w przypadku pól w klasie? Po prostu dlatego, że ta jawna inicjalizacja może wystąpić podczas konstrukcji, przez właściwość wywoływaną przez inicjator obiektu lub nawet przez metodę wywoływaną długo po zdarzeniu. Kompilator nie może użyć analizy statycznej, aby określić, czy każda możliwa ścieżka w kodzie prowadzi do jawnej inicjalizacji zmiennej przed nami. Złe popełnienie błędu byłoby denerwujące, ponieważ deweloper mógłby pozostać z prawidłowym kodem, który nie będzie się kompilował. Więc C # w ogóle go nie wymusza, a środowisko CLR jest pozostawione do automatycznego inicjowania pól do wartości domyślnej, jeśli nie jest to jawnie ustawione.
Egzekwowanie przez C # inicjalizacji zmiennych lokalnych jest ograniczone, co często przyciąga programistów. Rozważ następujące cztery wiersze kodu:
Drugi wiersz kodu nie zostanie skompilowany, ponieważ próbuje odczytać niezainicjowaną zmienną łańcuchową. Czwarta linia kodu kompiluje się jednak dobrze, tak jak
array
została zainicjowana, ale tylko z wartościami domyślnymi. Ponieważ domyślną wartością łańcucha jest null, otrzymujemy wyjątek w czasie wykonywania. Każdy, kto spędził tutaj czas na przepełnieniu stosu, będzie wiedział, że ta jawna / niejawna niespójność inicjalizacji prowadzi do wielu błędów „Dlaczego otrzymuję komunikat„ Odwołanie do obiektu nie jest ustawione na wystąpienie obiektu ”? pytania.źródło
public interface I1 { string str {get;set;} }
i metodęint f(I1 value) { return value.str.Length; }
. Jeśli istnieje w bibliotece, kompilator nie może wiedzieć, z czym ta biblioteka będzie połączona, a zatem czyset
zostanie wywołana przedget
, Pole zapasowe może nie zostać jawnie zainicjowane, ale musi skompilować taki kod.f
. Zostanie wygenerowany podczas kompilacji konstruktorów. Jeśli zostawisz konstruktor z polem prawdopodobnie niezainicjowanym, byłby to błąd. Mogą również istnieć ograniczenia dotyczące wywoływania metod klas i metod pobierających przed zainicjowaniem wszystkich pól.Dobre odpowiedzi powyżej, ale pomyślałem, że opublikuję znacznie prostszą / krótszą odpowiedź, aby ludzie byli leniwi, aby przeczytać długą (jak ja).
Klasa
Właściwość
Boo
może, ale nie musi, zostać zainicjowana w konstruktorze. Więc kiedy znajdziereturn Boo;
, nie zakłada , że został zainicjowany. Po prostu tłumi błąd.Funkcjonować
Te
{ }
znaki określają zakres bloku kodu. Kompilator wędruje po gałęziach tych{ }
bloków, śledząc rzeczy. Można łatwo stwierdzić, żeBoo
nie został zainicjowany. Następnie wyzwalany jest błąd.Dlaczego błąd istnieje?
Błąd został wprowadzony w celu zmniejszenia liczby wierszy kodu wymaganych do zapewnienia bezpieczeństwa kodu źródłowego. Bez błędu powyższe wyglądałoby tak.
Z instrukcji:
Źródła: https://msdn.microsoft.com/en-us/library/4y7h161d.aspx
źródło