2014-06-04 23 views
5

Dlaczego następująca awaria z NullReferenceException na oświadczeniu a.b.c = LazyInitBAndReturnValue(a);?Odnośniki użytkownika i kolejność przypisania do oceny

class A { 
    public B b; 
} 

class B { 
    public int c; 
    public int other, various, fields; 
} 

class Program { 

    private static int LazyInitBAndReturnValue(A a) 
    { 
     if (a.b == null) 
      a.b = new B(); 

     return 42; 
    } 

    static void Main(string[] args) 
    { 
     A a = new A(); 
     a.b.c = LazyInitBAndReturnValue(a); 
     a.b.other = LazyInitBAndReturnValue(a); 
     a.b.various = LazyInitBAndReturnValue(a); 
     a.b.fields = LazyInitBAndReturnValue(a); 
    } 
} 

wyrażenia przypisania są oceniane from right to left, więc do czasu jesteśmy przypisanie a.b.c, a.b nie powinna być zerowa. Co dziwne, kiedy debugger łamie się na wyjątku, to również pokazuje a.b jako zainicjalizowane wartością inną niż null.

Debugger on break

+0

'b' jest określany w' a.b.c' zanim zostanie zainicjowany w 'LazyInitB ...()'. – ja72

+0

Przeczytaj odpowiedzi przed komentarzem, zachowanie debuggera wydaje się pokazywać coś bardziej skomplikowanego niż to się dzieje. – SamStephens

Odpowiedz

2

ta jest szczegółowo w Section 7.13.1 w C# spec.

Przetwarzanie wykonywalna prostego przypisania postaci X = Y składa się z następujących etapów:

  • Jeśli x jest klasyfikowany jako zmienną:
    • x jest oceniona wyprodukować zmienną.
    • y jest oceniany i, w razie potrzeby, konwertowany na typ x poprzez niejawną konwersję (sekcja 6.1).
    • Jeśli zmienna podana przez x jest elementem tablicy typu referencyjnego, wykonywane jest sprawdzanie w czasie wykonywania, aby upewnić się, że wartość obliczona dla y jest zgodna z instancją tablicy, z której x jest elementem . Sprawdzenie powiedzie się, jeśli y jest zerowe lub jeśli istnieje niejawna referencja konwersji (sekcja 6.1.4) z faktycznego typu instancji , do której odwołuje się y do rzeczywistego typu elementu instancji tablicy zawierającej x. W przeciwnym razie generowany jest wyjątek System.ArrayTypeMismatchException.
    • Wartość wynikająca z oceny i konwersji y jest przechowywana w lokalizacji podanej przez ocenę x.
  • Jeśli x jest klasyfikowany jako właściwość lub indeksatora dostępu:
    • instancja ekspresji (jeśli X jest stały) a lista argumentu (jeśli X jest dostęp podziałowe) związany z X jest oceniany, a wyniki są używane w późniejszym wywołaniu selektora dostępu.
    • y jest oceniany i, w razie potrzeby, konwertowany na typ x poprzez niejawną konwersję (sekcja 6.1).
    • Ustawiony akcesor wartości x jest wywoływany z wartością obliczoną dla y jako jej argument wartości.

myślę dolny odcinek (jeśli x jest klasyfikowany jako własność lub indeksatora dostępu) stanowi wskazówkę, ale być może C# ekspert może wyjaśnić.

zestaw dostępowe są generowane, następnie y ocenia (powodowane przez przerwania), a następnie zestaw dostępowej jest wywoływana, która powoduje wyjątek zerowego odniesienia. Gdybym musiał zgadnąć, powiedziałbym, że accessor wskazuje na starą wartość b, która była zerowa.Po aktualizacji b nie aktualizuje on już utworzonego akcesora.

+1

Dziwne jest to, że jeśli faktycznie debugujesz kod, metoda 'LazyInitBAndReturnValue' faktycznie działa * przed * wyrzuceniem wyjątku. Myślę, że ocena a.b.c spowodowałaby wyrzucenie wyjątku przed uruchomieniem metody, ponieważ nie można było ocenić "c". –

+0

@CraigW. - Nienawidzę dodawać odpowiedzi * guess * na StackOverflow, ale może Eric Lippert natknie się na to pytanie i potwierdzi moje podejrzenia. "+ 1" na pytanie! –

+0

Kilka razy musiałem przeczytać twój ostatni akapit, ale teraz dostaję to, co mówisz. Do czasu, gdy przypisanie się stanie, 'b' faktycznie zawiera wartość, ale * wewnętrznie * CLR już zapisał odniesienie do' b' z * przed * metoda została uruchomiona. Kiedy po raz pierwszy spojrzałem na kod OP, pomyślałem: "Oczywiście, że dostaniesz NullRefEx", ale potem uruchomienie kodu sprawiło, że podrapałem się w głowę. To teraz (rodzaj) ma sens, ale z pewnością mogę zrozumieć zamieszanie OP. –

-1

Zdaję sobie sprawę, że to nie odpowiada na twoje pytanie, ale pozwolenie na coś poza klasą A, aby zainicjować członka należącego do klasy A, wydaje mi się przełamać hermetyzację. Jeśli B wymaga inicjalizacji przy pierwszym użyciu, "właścicielem" B powinien być ten, który to zrobi.

class A 
{ 
    private B _b; 
    public B b 
    { 
     get 
     { 
      _b = _b ?? new B(); 
      return _b; 
     } 
    } 
} 
+0

Nie sądzę, że to jest coś, co powinienem robić w prawdziwym życiu - co mam nadzieję, że nie. Kod ma kilka innych anty-wzorców. Jest to jednak interesujące zachowanie z perspektywy akademickiej. Głosowano z przegłosowaniem, ponieważ nie jest to odpowiedź na pytanie - byłoby to stosowne jako komentarz. – SamStephens

Powiązane problemy