5

Why does the following crash with a NullReferenceException on the statement 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);
    }
}

Assignment expressions are evaluated from right to left, so by the time we are assigning to a.b.c, a.b should not be null. Oddly enough, when the debugger breaks on the exception, it too shows a.b as initialized to a non-null value.

Debugger on break

SamStephens
  • 5,721
  • 6
  • 36
  • 44
Matt Kline
  • 10,149
  • 7
  • 50
  • 87

2 Answers2

2

This is detailed in Section 7.13.1 of the C# spec.

The run-time processing of a simple assignment of the form x = y consists of the following steps:

  • If x is classified as a variable:
    • x is evaluated to produce the variable.
    • y is evaluated and, if required, converted to the type of x through an implicit conversion (Section 6.1).
    • If the variable given by x is an array element of a reference-type, a run-time check is performed to ensure that the value computed for y is compatible with the array instance of which x is an element. The check succeeds if y is null, or if an implicit reference conversion (Section 6.1.4) exists from the actual type of the instance referenced by y to the actual element type of the array instance containing x. Otherwise, a System.ArrayTypeMismatchException is thrown.
    • The value resulting from the evaluation and conversion of y is stored into the location given by the evaluation of x.
  • If x is classified as a property or indexer access:
    • The instance expression (if x is not static) and the argument list (if x is an indexer access) associated with x are evaluated, and the results are used in the subsequent set accessor invocation.
    • y is evaluated and, if required, converted to the type of x through an implicit conversion (Section 6.1).
    • The set accessor of x is invoked with the value computed for y as its value argument.

I think the bottom section (if x is classified as a property or indexer access) provides a hint, but perhaps a C# expert can clarify.

A set accessor is generated first, then y is evaluated (triggering your breakpoint), then the set accessor is invoked, which causes a null reference exception. If I had to guess, I'd say the accessor points to the old value of b, which was null. When you update b, it doesn't update the accessor that it already created.

Mike Christensen
  • 88,082
  • 50
  • 208
  • 326
  • 1
    The odd thing is that if you actually debug the code the method `LazyInitBAndReturnValue` actually runs *before* the exception is thrown. I would think the evaluation of a.b.c would cause the exception to be thrown before the method runs because `c` could not be evaluated. – Craig W. Jun 04 '14 at 22:25
  • @CraigW. - I hate adding *guess* answers on StackOverflow, but maybe Eric Lippert will stumble across this question and confirm or disprove my suspicion. `+1` for the question though! – Mike Christensen Jun 04 '14 at 22:41
  • I had to read your last paragraph a couple of times but now I get what you're saying. By the time the assignment happens `b` actually contains a value but *internally* the CLR has already stored a reference to `b` from *before* the method was run. When I first looked at the OP's code I thought, "Of course you're going to get a NullRefEx" but then running the code made me scratch my head. It now (sort of) makes sense but I can certainly understand the OP's confusion. – Craig W. Jun 04 '14 at 22:54
  • If it's going to evaluate to get a set accessor, shouldn't the set accessor be to `c`, rather than `b`, and therefore NullRefEx before evaluation of `y` begins? I'm fascinated now I know more about the behavior. I also hope Eric sees this question :-) – SamStephens Jun 04 '14 at 23:15
-1

I realize this doesn't answer your question, but allowing something outside of class A to initialize a member belonging to class A in this fashion seems to me to break encapsulation. If B needs to be initialized on first use the "owner" of B should be the one to do that.

class A
{
    private B _b;
    public B b
    {
        get
        {
            _b = _b ?? new B();
            return _b;
        }
    }
}
Craig W.
  • 17,838
  • 6
  • 49
  • 82
  • I don't think that this is something @Matt Kline wants to do in real life - at least I hope not. The code has several other anti-patterns. However it's an interesting behavior from an academic perspective. Down-voted because this is not an answer to the question - it would have been appropriate as a comment. – SamStephens Jun 05 '14 at 04:59