Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unintended consequences when changing next line of execution in Visual Studio [duplicate]

Why does this go BOOM?

using System;
using System.Linq;

namespace Test
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            try
            {
                // 1. Hit F10 to step into debugging.
                string[] one = {"1"}; //2. Drag arrow to make this next statement executed
                // 3. Hit f5.
                Enumerable.Range(1,1)
                    .Where(x => one.Contains(x.ToString()));
            }
            catch (Exception exception)
            {
                Console.Write("BOOM!");
            }
        }
    }
}
like image 951
Robino Avatar asked Jun 12 '15 17:06

Robino


1 Answers

Looking at the ILDASM output, there might be an explanation here...

  .locals init ([0] class Test.Program/'<>c__DisplayClass1' 'CS$<>8__locals2',
           [1] class [mscorlib]System.Exception exception,
           [2] string[] CS$0$0000)
  IL_0000:  nop
  .try
  {
    IL_0001:  newobj     instance void Test.Program/'<>c__DisplayClass1'::.ctor()
    IL_0006:  stloc.0
    IL_0007:  nop
    IL_0008:  ldloc.0
    IL_0009:  ldc.i4.1
    IL_000a:  newarr     [mscorlib]System.String
    IL_000f:  stloc.2
    IL_0010:  ldloc.2
    IL_0011:  ldc.i4.0
    IL_0012:  ldstr      "1"
    IL_0017:  stelem.ref
    IL_0018:  ldloc.2
    IL_0019:  stfld      string[] Test.Program/'<>c__DisplayClass1'::one
    IL_001e:  ldc.i4.1
    IL_001f:  ldc.i4.1
    IL_0020:  call       class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32,
                                                                                                                                    int32)
    IL_0025:  ldloc.0
    IL_0026:  ldftn      instance bool Test.Program/'<>c__DisplayClass1'::'<Main>b__0'(int32)
    IL_002c:  newobj     instance void class [mscorlib]System.Func`2<int32,bool>::.ctor(object,
                                                                                        native int)
    IL_0031:  call       class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0> [System.Core]System.Linq.Enumerable::Where<int32>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>,
                                                                                                                                         class [mscorlib]System.Func`2<!!0,bool>)
    IL_0036:  pop
    IL_0037:  nop
    IL_0038:  leave.s    IL_004a
  }  // end .try
  catch [mscorlib]System.Exception 
  {

When you drag the execution cursor, you run the risk of corrupting the call stack. This is because dragging the cursor literally skips those lines. When running in the debugger, after hitting F10, the cursor stops at the start of the Main routine, before the try. If you drag the cursor to the creation of the array, you are skipping this fun line:

IL_0001: newobj instance void Test.Program/'<>c__DisplayClass1'::.ctor()

Which creates an instance of the Program class. The program class is then used later here:

IL_0019: stfld string[] Test.Program/'<>c__DisplayClass1'::one

Which because you skipped it, didn't create that object, so you get a NullReferenceException when running.

Why people can't reproduce this on VS2012, I'm not sure. Maybe the compiler is outputting different IL, but this is far as I can come up with using VS2013 Ultimate and C#4.5.

Interestingly enough, when you comment out the try/catch, the start of the program in IL looks like this:

.locals init ([0] class Test.Program/'<>c__DisplayClass1' 'CS$<>8__locals2',
           [1] string[] CS$0$0000)
  IL_0000:  newobj     instance void Test.Program/'<>c__DisplayClass1'::.ctor()
  IL_0005:  stloc.0

Which you can see the first line in the routine creates the Program object. Why the compiler decided to put that line inside the try/catch is beyond me.

EDIT

Digging a little deeper, changing your program to this:

    private static void Main(string[] args)
    {
        string[] one;

        try
        {
            // 1. Hit F10 to step into debugging.
            one = new string[] { "1" }; //2. Drag arrow to this
            // 3. Hit f5.
            Enumerable.Range(1, 1)
                .Where(x => one.Contains(x.ToString()));
        }
        catch (Exception exception)
        {
            Console.Write("BOOM!");
        }
    }

Results in working code. Examining the IL, you can see that the instance creation was moved outside the try:

  .locals init ([0] class [mscorlib]System.Exception exception,
           [1] class [mscorlib]System.Func`2<int32,bool> 'CS$<>9__CachedAnonymousMethodDelegate1',
           [2] class Test.Program/'<>c__DisplayClass2' 'CS$<>8__locals3',
           [3] string[] CS$0$0000)
  IL_0000:  ldnull
  IL_0001:  stloc.1
  IL_0002:  newobj     instance void Test.Program/'<>c__DisplayClass2'::.ctor()
  IL_0007:  stloc.2
  IL_0008:  nop
  .try
  {

The compiler was nice enough to move the creation of the string array from outside the try to inside the try, so skipping that line still results in a valid object. The code works, so I'm guessing that the NullReferenceException really is the instance of the Program class.

like image 58
Ron Beyer Avatar answered Sep 28 '22 23:09

Ron Beyer