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?
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?
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ć:
bool x;
if (M()) x = true;
Console.WriteLine(x);
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:
bool x;
if (N() * 0 == 0) x = true;
Console.WriteLine(x);
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 :
// Library written by BarCorp
public abstract class Bar
{
// Derived class is responsible for initializing x.
protected int x;
protected abstract void InitializeX();
public void M()
{
InitializeX();
Console.WriteLine(x);
}
}
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
public class Foo : Bar
{
protected override void InitializeX() { }
}
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ę.
bool x;
tego równoważnym bool x = false;
nawet wewnątrz metody ?
Kiedy tworzę ten sam bool w ramach mojej metody, bool check (zamiast w klasie), pojawia się błąd „użycie nieprzypisanej kontroli zmiennej lokalnej”. Czemu?
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 i default(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 :
Powodem, dla którego chcemy to uczynić nielegalnym, nie jest, jak wielu ludzi sądzi, ponieważ zmienna lokalna zostanie zainicjowana jako śmieci i chcemy Cię chronić przed śmieciami. W rzeczywistości automatycznie inicjalizujemy lokalne wartości domyślne. (Chociaż języki programowania C i C ++ tego nie robią i pozwolą na radosne odczytywanie śmieci z niezainicjowanego lokalnego). Raczej dzieje się tak dlatego, że istnienie takiej ścieżki kodu jest prawdopodobnie błędem i chcemy wrzucić cię do pit jakości; powinieneś ciężko pracować, aby napisać ten błąd.
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.
Dlaczego zmienne lokalne wymagają inicjalizacji, a pola nie?
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.
Dlaczego zmienne lokalne wymagają inicjalizacji?
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 pola nie wymagają inicjalizacji?
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.
A co z typami kolekcji?
Egzekwowanie przez C # inicjalizacji zmiennych lokalnych jest ograniczone, co często przyciąga programistów. Rozważ następujące cztery wiersze kodu:
string str;
var len1 = str.Length;
var array = new string[10];
var len2 = array[0].Length;
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.
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 czy set
zostanie wywołana przed get
, 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).
class Foo {
private string Boo;
public Foo() { /** bla bla bla **/ }
public string DoSomething() { return Boo; }
}
Właściwość Boo
może, ale nie musi, zostać zainicjowana w konstruktorze. Więc kiedy znajdzie return Boo;
, nie zakłada , że został zainicjowany. Po prostu tłumi błąd.
public string Foo() {
string Boo;
return Boo; // triggers error
}
Te { }
znaki określają zakres bloku kodu. Kompilator wędruje po gałęziach tych { }
bloków, śledząc rzeczy. Można łatwo stwierdzić, że Boo
nie został zainicjowany. Następnie wyzwalany jest błąd.
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.
public string Foo() {
string Boo;
/* bla bla bla */
if(Boo == null) {
return "";
}
return Boo;
}
Z instrukcji:
Kompilator C # nie zezwala na użycie niezainicjowanych zmiennych. Jeśli kompilator wykryje użycie zmiennej, która mogła nie zostać zainicjowana, generuje błąd kompilatora CS0165. Aby uzyskać więcej informacji, zobacz pola (przewodnik programowania w języku C #). Zwróć uwagę, że ten błąd jest generowany, gdy kompilator napotka konstrukcję, która może spowodować użycie nieprzypisanej zmiennej, nawet jeśli dany kod nie. Pozwala to uniknąć konieczności stosowania zbyt skomplikowanych reguł dla określonego przypisania.
Źródła: https://msdn.microsoft.com/en-us/library/4y7h161d.aspx