【问题标题】:Debugger stepping in if statement and lambda expression调试器步入 if 语句和 lambda 表达式
【发布时间】:2014-08-04 14:10:59
【问题描述】:

以如下代码为例:

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

如果我以常规方式运行该程序(没有断点或任何其他中断),则一切正常,没有异常或错误(如您所料)。

现在,如果我在 if 语句上放置一个断点并将光标移动到大括号,如下图所示(使用我的鼠标,而不是使用 F10,因此跳过 if(true) 语句):

当调试器执行string foo = null 语句时,我得到了System.NullReferenceException 类型的异常

这似乎与foo 变量在if 语句内的lambda 表达式中使用的事实有关。我已经在 Visual Studio 2012 和 2013(专业版和终极版)上测试并复制了它。

知道为什么会发生这种情况吗?

【问题讨论】:

  • 您应该检查生成 IL,但很可能您的变量被捕获到创建的类中以获取 lambda 表达式所需的闭包,并且该类实例尚不存在(或者您在跳过时跳过了它的初始化if(true)检查。
  • 无法验证,但是当您跳过 if 语句时,这看起来像 foo 被提升到的底层对象(因为它被 lambda 捕获)没有被分配.
  • 当你说它似乎与变量 foo 在 lambda 中使用这一事实有关时,这是否意味着如果你以后使用 foo 它停止失败?但这一切似乎都很奇怪,因为它不应该真的在简单的分配上抛出异常......
  • @Chris “简单”赋值并不是那么简单:它秘密地赋值给一个只有编译器可访问类型的隐藏对象的字段。其实很复杂!
  • 另一个观察结果(证实了“隐藏对象未分配”理论):如果将 foo 的声明移到 if 语句之外,错误就会消失,可能是因为我们现在需要在if 语句之前分配对象。似乎这些对象的所有块级分配都与 if 语句相关联,至少就调试器所知而言。一如既往的教训:C# 中的闭包没有什么“简单”的。

标签: c# lambda visual-studio-debugging


【解决方案1】:

推测您正在跳过闭包生成的 cmets 是正确的。不保证 C# 程序在移动指令指针时具有任何特定行为。如果这样做时会感到疼痛,请不要这样做。

其实这是一个小小的谎言。有保证。例如,您可以保证在可验证程序中这样做不会破坏 clr 的内部数据结构。您可以保证这样做不会使堆栈错位。等等。但对您的数据结构没有任何明示或暗示的保证!移动指令指针后果自负。

【讨论】:

  • 我想我应该更认真地对待[...]drag the arrow. This may have unintended consequences 警告 :) 感谢您的回答
【解决方案2】:

Eric 的回答和 cmets 已经描述了为什么它通常会发生。 我想强调一下在这种特殊情况下发生了什么。

这是一个生成的 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 
}

注意L_0003 行。它为自动生成的 c__DisplayClass2 类调用 ctor,该类包含 foo 字段,因为您在 lambda 中使用它。所以NullReferenceException 发生是因为你的跳过类初始化,但是你在L_000c 的一行上分配了实例的字段foo。

太糟糕了,没有简单的方法可以在 IL 级别进行调试来验证这一点,但我们可以调试 JITed 程序(调试 -> 反汇编)

这是你的第一个断点:

然后光标移动后:

其中一个跳过的调用结构必须从L_0003 调用ctor。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-01-25
    相关资源
    最近更新 更多