Dlaczego zmienne lokalne wymagają inicjalizacji, a pola nie?


140

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?


Komentarze nie służą do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
Martijn Pieters

14
Pytanie jest niejasne. Czy „ponieważ tak mówi specyfikacja” byłoby akceptowalną odpowiedzią?
Eric Lippert

4
Ponieważ tak to zostało zrobione w Javie, kiedy to skopiowali. : P
Alvin Thompson

Odpowiedzi:


177

Odpowiedzi Yuvala i Davida są w zasadzie poprawne; Podsumowując:

  • Użycie nieprzypisanej zmiennej lokalnej jest prawdopodobnym błędem, który kompilator może wykryć niewielkim kosztem.
  • Użycie nieprzypisanego pola lub elementu tablicy jest mniej prawdopodobnym błędem i trudniej jest wykryć warunek w kompilatorze. Dlatego kompilator nie próbuje wykryć użycia niezainicjowanej zmiennej dla pól i zamiast tego opiera się na inicjalizacji do wartości domyślnej, aby określić zachowanie programu.

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:

  • Uczyń powszechne, bezpieczne, często używane idiomy programistyczne jako nielegalne.
  • Wykonaj kosztowną analizę całego programu, która spowoduje, że kompilacja zajmie wiele godzin w celu wyszukania błędów, których prawdopodobnie nie ma.
  • Polegaj na automatycznej inicjalizacji do wartości domyślnych.

Zespół projektowy wybrał trzecią opcję.


1
Świetna odpowiedź, jak zwykle. Mam jednak pytanie: dlaczego nie przypisać automatycznie wartości domyślnych również zmiennym lokalnym? Innymi słowy, dlaczego nie uczynić bool x;tego równoważnym bool x = false; nawet wewnątrz metody ?
durron597

8
@ durron597: Ponieważ doświadczenie pokazuje, że zapomnienie o przypisaniu wartości do lokalnego jest prawdopodobnie błędem. Jeśli jest to prawdopodobnie błąd i jest tani i łatwy do wykrycia, istnieje dobry bodziec, aby uczynić to zachowanie nielegalnym lub ostrzeżeniem.
Eric Lippert

27

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 falsezmienia 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 = falsewydaniem 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.


„wyobraź sobie, że to był typ referencyjny i przekazałbyś ten niezainicjowany obiekt do metody oczekującej zainicjalizowanej” Czy chodziło Ci o: ”wyobraź sobie, że był to typ referencyjny i przekazałeś wartość domyślną (null) zamiast referencji obiekt"?
Deduplicator

@Deduplicator Yes. Metoda oczekująca wartości różnej od null. Edytował tę część. Mam nadzieję, że teraz jest to jaśniejsze.
Yuval Itzchakov

Nie sądzę, że to z powodu narysowanej linii. Każda klasa ma mieć konstruktora, przynajmniej domyślnego konstruktora. Więc kiedy trzymasz się domyślnego konstruktora, otrzymasz wartości domyślne (cicha przezroczystość). Definiując konstruktor, oczekuje się lub powinno się wiedzieć, co w nim robimy i jakie pola mają zostać zainicjowane w jaki sposób, w tym znajomość wartości domyślnych.
Peter

Wręcz przeciwnie: pole w metodzie może mieć zadeklarowane i przypisane wartości w różnych ścieżkach wykonania. Mogą istnieć wyjątki, które można łatwo nadzorować, dopóki nie przejrzysz dokumentacji frameworka, którego możesz użyć, lub nawet innych części kodu, których możesz nie utrzymywać. Może to wprowadzić bardzo złożoną ścieżkę wykonania. Dlatego kompilatorzy podpowiadają.
Peter

@Peter Tak naprawdę nie zrozumiałem twojego drugiego komentarza. Jeśli chodzi o pierwsze, nie ma wymogu inicjowania żadnych pól wewnątrz konstruktora. To powszechna praktyka . Zadaniem kompilatora nie jest wymuszanie takiej praktyki. Nie można polegać na żadnej implementacji konstruktora działającego i powiedzieć „w porządku, wszystkie pola są gotowe”. W swojej odpowiedzi Eric wiele rozwinął na temat sposobów inicjalizacji pola klasy i pokazuje, jak bardzo dużo czasu zajęłoby obliczenie wszystkich logicznych sposobów inicjalizacji.
Yuval Itzchakov

25

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 arrayzostał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.


„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”. Nie jestem przekonany, że to prawda. Czy możesz zamieścić przykład programu odpornego na analizę statyczną?
John Kugelman

@JohnKugelman, rozważ prosty przypadek 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 setzostanie wywołana przed get, Pole zapasowe może nie zostać jawnie zainicjowane, ale musi skompilować taki kod.
David Arno

To prawda, ale nie spodziewałbym się, że błąd zostanie wygenerowany podczas kompilacji 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.
John Kugelman

@JohnKugelman: Zamieszczę odpowiedź omawiając podniesiony przez Ciebie problem.
Eric Lippert

4
To niesprawiedliwe. Próbujemy się tutaj nie zgodzić!
John Kugelman

10

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

class Foo {
    private string Boo;
    public Foo() { /** bla bla bla **/ }
    public string DoSomething() { return Boo; }
}

Właściwość Boomoż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.

Funkcjonować

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 Boonie 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.

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

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.