C# 在列表中添加委托(delegate)

标签 c#

<分区>

下面这个例子十次只打印 10。 你能告诉我后台发生了什么吗?

 public delegate void DoSomething();

static void Main(string[] args)
{
    List<DoSomething> lstOfDelegate = new List<DoSomething>();
    int iCnt = 0;
    while (iCnt < 10)
    {
        lstOfDelegate.Add(delegate { Console.WriteLine(iCnt); });
        iCnt++;
    }

    foreach (var item in lstOfDelegate)
    {
        item.Invoke();
    }
    Console.ReadLine();
}

根据我的说法,它应该打印 0 到 9,但它打印 10,10,10,10,10,10,10,10,10,10

最佳答案

逻辑

这是代表的一个常见问题,实际上很容易,当你说:

item.Invoke();

委托(delegate)将运行,并且该委托(delegate)是:

Console.WriteLine(iCnt);

iCnt 的值是多少?在那一刻?好吧,它是10因为你递增它直到它达到那个值。所以它打印10 .

因为您将调用其中的 10 个代表,并且他们都打印 10 , 你得到 10 10 次。

你可以让它使用 iCnt 的值在使用另一个变量创建委托(delegate)的迭代中:

while (iCnt < 10)
{
    var value = iCnt;
    lstOfDelegate.Add(delegate { Console.WriteLine(value); });
    iCnt++;
}

现在,该变量永远不会改变值,委托(delegate)应该按预期工作。


幕后花絮

幕后的实现是使用一个隐藏的匿名类,它有 iCnt作为一个领域。那么{ Console.WriteLine(iCnt); }被创建为该类的匿名方法。在运行时,会创建一个指向匿名类实例的委托(delegate),并带有指向匿名方法的指针。

我为等效的完整程序(如下)创建了代码,并使用 Roslyn 编译它以查看它生成的 .IL。

C#代码:

using System;
using System.Collections.Generic;

public class Program
{
    public delegate void DoSomething();

    public static void Main(string[] args)
    {
        List<DoSomething> lstOfDelegate = new List<DoSomething>();
        int iCnt = 0;
        while (iCnt < 10)
        {
            lstOfDelegate.Add(delegate { Console.WriteLine(iCnt); });
            iCnt++;
        }

        foreach (var item in lstOfDelegate)
        {
            item.Invoke();
        }
        Console.ReadLine();
    }
}

接下来是对 IL 的重新解释(此代码将不起作用,但显示了编译器编写代码的方式,添加了注释):

public class Program
{
    /* This is the delegate class, it uses runtime code, not IL */
    public class DoSomething : System.MulticastDelegate
    {
        public DoSomething(object object, int method) { /*...*/ }
        public override void Invoke() { /*...*/ }
        public override void BeginInvoke() { /*...*/ }
        Public override void EndInvoke() { /*...*/ }
    }

    /* This is the hidden anonymous class,
       the system refers to it as '<>c__DisplayClass1_0'.
       notice it contains iCnt. */
    [CompilerGenerated()]
    private class '<>c__DisplayClass1_0'
    {
        /* iCnt was moved to here by the compiler. */
        public int iCnt;
        public '<>c__DisplayClass1_0'() { /* Default constructor */ }
        /* This is the method the delegate invokes.*/
        internal '<Main>b__0'()
        {
            Console.WriteLine(this.iCnt);
        }
    }

    public static void Main(string[] args)
    {
        '<>c__DisplayClass1_0' var0; // A reference to the anonymous class
        List<DoSomething> var1; // lstOfDelegate
        int var2; // temp variable for the increment
        bool var3; // temp variable for the while conditional
        List<DoSomething>.Enumerator var4; // enumerator, used by foreach
        DoSomething var5; // temp variable for the delegate
        
        // Instantiate the anonymous class
        // As you can see, there is only one instance,
        // so there is only one iCnt
        var0 = new '<>c__DisplayClass1_0'();

        // List<DoSomething> lstOfDelegate = new List<DoSomething>();
        var1 = new List<DoSomething>();

        // int iCnt = 0;
        var0.iCnt = 0;

        goto IL_003b; // while (iCnt < 10) {
IL_0016:

        // lstOfDelegate.Add(delegate { Console.WriteLine(iCnt); });
        var1.add(new DoSomething(var0, funcPtr('<>c__DisplayClass1_0'.'<Main>b__0')));

        // iCnt++;
        var2 = var0.iCnt;
        var0.iCnt = var2 + 1;

IL_003b:
        var3 = var0.iCnt < 10;
        if (var3) goto IL_0016; // }

        var4 = var1.GetEnumerator();
        goto IL_0067; // foreach (var item in lstOfDelegate) {

        try {

IL_0054:

        var5 = var4.Current;
        var5.Invoke();

IL_0067:
        if (var4.MoveNext()) goto IL_0054;

        } finally {

        var4.Dispose();
        
        }

        Console.ReadLine();

    }

    public Program() { /* Default constructor */ }
}

注意事项:

  • 如您所见,添加到列表中的所有代表实际上都是相同的。
  • 在循环中添加新变量时,它会将语义更改为每次迭代都有一个新变量。然后编译器生成代码来实例化一个新的 '<>c__DisplayClass1_0'在每次迭代中,而不是在 Main 开始时一次.
  • DoSomething 方法的实现不在 IL 中。
  • 您会注意到循环是反向的,这取决于编译器。
  • funcPtr代表 OpCode ldftn ,它得到一个 int ,运行时将其用作指向方法的指针。 C# 中没有直接等效项。
  • .NET中的try-finally block 是声明式的,我把它加在了它所属的位置上。在 .NET 中,进入 try block 并不是实际指令。
  • 您还可以在代码中看到 foreach 循环的结构。
  • 生成的代码始终使用全名限定,为了便于阅读,我将其删除。

希望阅读上面的代码可以消除任何疑问。为了以防万一,我在下面添加了 Roslyn 生成的 IL:

.class private auto ansi '<Module>'
{
} // end of class <Module>

.class public auto ansi beforefieldinit Program
    extends [mscorlib]System.Object
{
    // Nested Types
    .class nested public auto ansi sealed DoSomething
        extends [mscorlib]System.MulticastDelegate
    {
        // Methods
        .method public hidebysig specialname rtspecialname 
            instance void .ctor (
                object 'object',
                native int 'method'
            ) runtime managed 
        {
        } // end of method DoSomething::.ctor

        .method public hidebysig newslot virtual 
            instance void Invoke () runtime managed 
        {
        } // end of method DoSomething::Invoke

        .method public hidebysig newslot virtual 
            instance class [mscorlib]System.IAsyncResult BeginInvoke (
                class [mscorlib]System.AsyncCallback callback,
                object 'object'
            ) runtime managed 
        {
        } // end of method DoSomething::BeginInvoke

        .method public hidebysig newslot virtual 
            instance void EndInvoke (
                class [mscorlib]System.IAsyncResult result
            ) runtime managed 
        {
        } // end of method DoSomething::EndInvoke

    } // end of class DoSomething

    .class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass1_0'
        extends [mscorlib]System.Object
    {
        .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
            01 00 00 00
        )
        // Fields
        .field public int32 iCnt

        // Methods
        .method public hidebysig specialname rtspecialname 
            instance void .ctor () cil managed 
        {
            // Method begins at RVA 0x20f4
            // Code size 8 (0x8)
            .maxstack 8

            IL_0000: ldarg.0
            IL_0001: call instance void [mscorlib]System.Object::.ctor()
            IL_0006: nop
            IL_0007: ret
        } // end of method '<>c__DisplayClass1_0'::.ctor

        .method assembly hidebysig 
            instance void '<Main>b__0' () cil managed 
        {
            // Method begins at RVA 0x20fd
            // Code size 14 (0xe)
            .maxstack 8

            IL_0000: nop
            IL_0001: ldarg.0
            IL_0002: ldfld int32 Program/'<>c__DisplayClass1_0'::iCnt
            IL_0007: call void [mscorlib]System.Console::WriteLine(int32)
            IL_000c: nop
            IL_000d: ret
        } // end of method '<>c__DisplayClass1_0'::'<Main>b__0'

    } // end of class <>c__DisplayClass1_0


    // Methods
    .method public hidebysig static 
        void Main (
            string[] args
        ) cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 136 (0x88)
        .maxstack 3
        .locals init (
            [0] class Program/'<>c__DisplayClass1_0',
            [1] class [mscorlib]System.Collections.Generic.List`1<class Program/DoSomething>,
            [2] int32,
            [3] bool,
            [4] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class Program/DoSomething>,
            [5] class Program/DoSomething
        )

        IL_0000: newobj instance void Program/'<>c__DisplayClass1_0'::.ctor()
        IL_0005: stloc.0
        IL_0006: nop
        IL_0007: newobj instance void class [mscorlib]System.Collections.Generic.List`1<class Program/DoSomething>::.ctor()
        IL_000c: stloc.1
        IL_000d: ldloc.0
        IL_000e: ldc.i4.0
        IL_000f: stfld int32 Program/'<>c__DisplayClass1_0'::iCnt
        IL_0014: br.s IL_003b
        IL_0016: nop
        IL_0017: ldloc.1
        IL_0018: ldloc.0
        IL_0019: ldftn instance void Program/'<>c__DisplayClass1_0'::'<Main>b__0'()
        IL_001f: newobj instance void Program/DoSomething::.ctor(object, native int)
        IL_0024: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<class Program/DoSomething>::Add(!0)
        IL_0029: nop
        IL_002a: ldloc.0
        IL_002b: ldfld int32 Program/'<>c__DisplayClass1_0'::iCnt
        IL_0030: stloc.2
        IL_0031: ldloc.0
        IL_0032: ldloc.2
        IL_0033: ldc.i4.1
        IL_0034: add
        IL_0035: stfld int32 Program/'<>c__DisplayClass1_0'::iCnt
        IL_003a: nop
        IL_003b: ldloc.0
        IL_003c: ldfld int32 Program/'<>c__DisplayClass1_0'::iCnt
        IL_0041: ldc.i4.s 10
        IL_0043: clt
        IL_0045: stloc.3
        IL_0046: ldloc.3
        IL_0047: brtrue.s IL_0016
        IL_0049: nop
        IL_004a: ldloc.1
        IL_004b: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<class Program/DoSomething>::GetEnumerator()
        IL_0050: stloc.s 4
        IL_0052: br.s IL_0067
        IL_0054: ldloca.s 4
        IL_0056: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class Program/DoSomething>::get_Current()
        IL_005b: stloc.s 5
        IL_005d: nop
        IL_005e: ldloc.s 5
        IL_0060: callvirt instance void Program/DoSomething::Invoke()
        IL_0065: nop
        IL_0066: nop
        IL_0067: ldloca.s 4
        IL_0069: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class Program/DoSomething>::MoveNext()
        IL_006e: brtrue.s IL_0054
        IL_0070: leave.s IL_0081
        IL_0072: ldloca.s 4
        IL_0074: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class Program/DoSomething>
        IL_007a: callvirt instance void [mscorlib]System.IDisposable::Dispose()
        IL_007f: nop
        IL_0080: endfinally
        IL_0081: call string [mscorlib]System.Console::ReadLine()
        IL_0086: pop
        IL_0087: ret

        Try IL_0052-IL_0072 Finally IL_0072-IL_0081
    } // end of method Program::Main

    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x20f4
        // Code size 8 (0x8)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [mscorlib]System.Object::.ctor()
        IL_0006: nop
        IL_0007: ret
    } // end of method Program::.ctor

} // end of class Program

关于C# 在列表中添加委托(delegate),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40070787/

相关文章:

c# - *.datasource 应该在版本控制中被忽略吗?

c# - 加载前检查文件创建者以防止篡改/损坏数据

c# - 如何在 LINQ 中应用 if 条件?

c# - 如何在 WPF 中创建类似 Excel 的电子表格 - C#

c# - 使用序列化检查对象的状态变化?

c# - 操作失败,对具有 IID 的接口(interface)的 COM 组件上的 QueryInterface 调用失败,因为库未注册

c# - 无法为对象类重载运算符 '+'

c# - 当一个属性快速更改多次时,会发送多少个 PropertyChanged 事件?

c# - 希望 Autofac 不注册任何具有多个实现的接口(interface)

c# - 如何选择与对象列表中的属性值共享属性值的所有对象?