Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Member references and assignment order of evaluation

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

like image 975
Matt Kline Avatar asked Nov 11 '22 06:11

Matt Kline


1 Answers

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.

like image 128
Mike Christensen Avatar answered Dec 15 '22 09:12

Mike Christensen