cheesemacfly
cheesemacfly

Reputation: 11762

Debugger stepping in if statement and lambda expression

Using the following code as an example:

if (true)
{
    string foo = null;
    List<string> bar = new List<string>
    {
        "test"
    };
    bar.Any(t => t == foo);
}

If I run this program in a regular way (without a break point or any other interruption), everything works without exception or error (as you would expect it).

Now if I put a break point on the if statement and move the cursor to the curly brace as described in the following picture (using my mouse, not using F10, so skipping the if(true) statement):

enter image description here

I get an exception of type System.NullReferenceException when the debugger executes the statement string foo = null

It seems to be linked to the fact that the variable foo is used in the lambda expression inside the if statement. I have tested and reproduced this on Visual Studio 2012 and 2013 (pro and ultimate).

Any idea on why this could be happening?

Upvotes: 7

Views: 910

Answers (2)

xvorsx
xvorsx

Reputation: 2442

Eric's answer and comments already describe why it can happens in general. I'd like to highlight whats going on in this particular case.

Here is a generated IL:

.method private hidebysig static void Main(string[] args) cil managed
{
    .entrypoint
    .maxstack 3
    .locals init (
        [0] class [mscorlib]System.Collections.Generic.List`1<string> bar,
        [1] class [mscorlib]System.Collections.Generic.List`1<string> <>g__initLocal0,
        [2] class StackOverflow.Program/<>c__DisplayClass2 CS$<>8__locals3,
        [3] bool CS$4$0000)
    L_0000: nop 
    L_0001: ldc.i4.0 
    L_0002: stloc.3 
    L_0003: newobj instance void StackOverflow.Program/<>c__DisplayClass2::.ctor()
    L_0008: stloc.2 
    L_0009: nop 
    L_000a: ldloc.2 
    L_000b: ldnull 
    L_000c: stfld string StackOverflow.Program/<>c__DisplayClass2::foo
    L_0011: newobj instance void [mscorlib]System.Collections.Generic.List`1<string>::.ctor()
    L_0016: stloc.1 
    L_0017: ldloc.1 
    L_0018: ldstr "test"
    L_001d: callvirt instance void [mscorlib]System.Collections.Generic.List`1<string>::Add(!0)
    L_0022: nop 
    L_0023: ldloc.1 
    L_0024: stloc.0 
    L_0025: ldloc.0 
    L_0026: ldloc.2 
    L_0027: ldftn instance bool StackOverflow.Program/<>c__DisplayClass2::<Main>b__1(string)
    L_002d: newobj instance void [mscorlib]System.Func`2<string, bool>::.ctor(object, native int)
    L_0032: call bool [System.Core]System.Linq.Enumerable::Any<string>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>, class [mscorlib]System.Func`2<!!0, bool>)
    L_0037: pop 
    L_0038: nop 
    L_0039: ret 
}

Note L_0003 line. It calls ctor for auto-generated c__DisplayClass2 class, which hold foo field, since you use it in a lambda. So NullReferenceException happens because your skip class initialization, but then you assigning instance's field foo on a line L_000c.

Too bad, there is no easy way to debug on IL level to verify this, but we can debug JITed program (Debug -> Disassembly)

Here is your first breakpoint:

enter image description here

And then after cursor move:

enter image description here

One of these skiped call instructures must be call to ctor from L_0003.

Upvotes: 8

Eric Lippert
Eric Lippert

Reputation: 660159

The comments which conjecture that you are skipping the generation of the closure are correct. C# programs are not guaranteed to have any particular behavior when you move the instruction pointer. If it hurts when you do that, don't do it.

actually that is a small lie. There are guarantees. For example, you are guaranteed that doing so in a verifiable program will not corrupt the internal data structures of the clr. You are guaranteed that doing so will not misalign the stack. and so on. But no guarantees are expressed or implied wrt your data structures! You move the instruction pointer at your peril.

Upvotes: 9

Related Questions