2014-06-04 5 views
5

Почему следующая авария с NullReferenceException в заявлении a.b.c = LazyInitBAndReturnValue(a);?Ссылки на участников и порядок их назначения

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); 
    } 
} 

Назначение выражения вычисляется from right to left, так что к тому времени, когда мы назначаем a.b.c, a.b не должен быть пустым. Как ни странно, когда отладчик разбивается на исключение, он также показывает a.b как инициализированное ненулевым значением.

Debugger on break

+0

'b' ссылается на' a.b.c' перед его инициализацией в 'LazyInitB ...()'. – ja72

+0

Прочитайте ответы перед комментарием, поведение отладчика, похоже, показывает нечто более сложное, чем это происходит. – SamStephens

ответ

2

Это подробно описано в Section 7.13.1 на языке C# спецификации.

Обработка во время выполнения простого присвоения вида х = у состоит из следующих этапов:

  • Если х классифицируется как переменная:
    • х оценивали с производят переменную.
    • y оценивается и, при необходимости, преобразуется в тип x через неявное преобразование (раздел 6.1).
    • Если переменная, заданная x, является элементом массива ссылочного типа, выполняется проверка времени выполнения, чтобы гарантировать, что значение , вычисленное для y, совместимо с экземпляром массива, из которого x является элементом . Проверка завершается успешно, если y имеет значение NULL или если неявное обращение (раздел 6.1.4) существует из фактического типа экземпляра , на который ссылается y, на фактический тип элемента экземпляра массива , содержащего x. В противном случае возникает ошибка System.ArrayTypeMismatchException: .
    • Значение, полученное в результате оценки и преобразования y, сохраняется в местоположении, заданном оценкой x.
  • Если х классифицируются как свойства или индексатора доступ к:
    • выражение экземпляра (если й не является статическим) и список аргументов (если х доступ индексатора), связанным с й оцениваются и результаты используются в последующем наборе аксессуаров.
    • y оценивается и, при необходимости, преобразуется в тип x через неявное преобразование (раздел 6.1).
    • Аксессор доступа к объекту x вызывается со значением, вычисленным для y в качестве аргумента его значения.

Я думаю, что нижняя часть (если х классифицируется как собственность или индексатор доступ к) дает подсказку, но, возможно, C# эксперт может прояснить.

множества аксессор генерируется, а затем y оценивается (срабатывание вашей точки останова), то множества аксессор вызывается, что приводит к нулевой ссылке исключения. Если бы я должен был догадаться, я бы сказал, что аксессуар указывает на старое значение b, которое было null.Когда вы обновляете b, он не обновляет аксессор, который он уже создал.

+1

Странно, что если вы действительно отлаживаете код, метод 'LazyInitBAndReturnValue' фактически запускает * перед *, генерируется исключение. Я думаю, что оценка a.b.c приведет к тому, что исключение будет выбрано до запуска метода, потому что 'c' не может быть оценен. –

+0

@CraigW. - Ненавижу добавлять * угадывать * ответы на StackOverflow, но, возможно, Эрик Липперт споткнется на этот вопрос и подтвердит или опровергнет мои подозрения. '+ 1' за вопрос, хотя! –

+0

Я должен был прочитать ваш последний параграф пару раз, но теперь я получаю то, что вы говорите. К тому времени, когда задание происходит, 'b' фактически содержит значение, но * внутренне *, CLR уже сохранил ссылку на' b' from * before *, метод был запущен. Когда я впервые взглянул на код OP, я подумал: «Конечно, вы получите NullRefEx», но затем запуск кода заставил меня почесать голову. Это сейчас (вроде) имеет смысл, но я могу, конечно, понять путаницу OP. –

-1

Я понимаю, что это не отвечает на ваш вопрос, но позволяя чему-то вне класса А инициализировать член, принадлежащий классу А, таким образом, мне кажется, что он разрушает инкапсуляцию. Если B необходимо инициализировать при первом использовании, «владелец» B должен быть тем, кто должен это сделать.

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

Я не думаю, что это что-то, что @Matt Kline хочет делать в реальной жизни - по крайней мере, я надеюсь, что нет. Код содержит несколько других шаблонов. Однако это интересное поведение с научной точки зрения. Проголосовали за это, потому что это не ответ на вопрос - это было бы уместно в качестве комментария. – SamStephens

Смежные вопросы