C# 订阅一个函数到 System.Action 订阅另一个 System.Action:为什么顺序很重要?

C# subscribe a function to System.Action that is subscribed to another System.Action: why does order matter?

我有3个类,组成一棵树。我正在使用 System.Actions 来传达事件。但我很困惑为什么我订阅的顺序很重要。检查这个伪代码:

class Leaf
{
    public System.Action OnTrigger;
    public void Go()
    {
        if (OnTrigger != null)
            OnTrigger();
    }
}

class Chunk
{
    public System.Action OnTrigger;
    public Leaf leaf = null;
    public Chunk()
    {
        leaf = new Leaf();
    }
}

class Tree
{
    void Hello()
    {
        Debug.Log("Hello");
    }
    void World()
    {
        Debug.Log("World");
    }

    public Tree()
    {
        var chunk2 = new Chunk();
        chunk2.OnTrigger += Hello; // OK: will be called
        chunk2.leaf.OnTrigger += chunk2.OnTrigger;
        chunk2.OnTrigger += World; // NOT: not be called
        chunk2.leaf.Go();
    }
}

此致,

马萨

这与订单本身无关。

它与您所做的作业有关。

    chunk2.OnTrigger += Hello; // this is **never** called
    chunk2.leaf.OnTrigger += chunk2.OnTrigger; // this is the only one called
    chunk2.OnTrigger += World; // this is **never** called

当您调用 chunk2.leaf.Go(); 时,唯一触发的事件是 chunk2.leaf.OnTriggerchunk2.OnTrigger 从未被调用。

通过检查委托的工作方式和编译时创建的 MSIL,这一点相对明显。首先让我们注意 System.Action 是一个具有 ICloneable 接口的 System.Delegate

当你编译这个程序时,你得到的几乎是这样的:

  IL_0015:  newobj     instance void [mscorlib]System.Action::.ctor(object,
                                                                    native int)
  IL_001a:  call       class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
                                                                                          class [mscorlib]System.Delegate)
  IL_001f:  castclass  [mscorlib]System.Action
  IL_0024:  stfld      class [mscorlib]System.Action ConsoleTests.Program6/Chunk::OnTrigger
  IL_0029:  ldloc.0
  IL_002a:  ldfld      class ConsoleTests.Program6/Leaf ConsoleTests.Program6/Chunk::leaf
  IL_002f:  dup
  IL_0030:  ldfld      class [mscorlib]System.Action ConsoleTests.Program6/Leaf::OnTrigger
  IL_0035:  ldloc.0
  IL_0036:  ldfld      class [mscorlib]System.Action ConsoleTests.Program6/Chunk::OnTrigger
  IL_003b:  call       class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
                                                                                          class [mscorlib]System.Delegate)
  IL_0040:  castclass  [mscorlib]System.Action
  IL_0045:  stfld      class [mscorlib]System.Action ConsoleTests.Program6/Leaf::OnTrigger
  IL_004a:  ldloc.0
  IL_004b:  dup
  IL_004c:  ldfld      class [mscorlib]System.Action ConsoleTests.Program6/Chunk::OnTrigger
  IL_0051:  ldnull
  IL_0052:  ldftn      void ConsoleTests.Program6::World()
  IL_0058:  newobj     instance void [mscorlib]System.Action::.ctor(object,
                                                                    native int)
  IL_005d:  call       class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
                                                                                          class [mscorlib]System.Delegate)
  IL_0062:  castclass  [mscorlib]System.Action
  IL_0067:  stfld      class [mscorlib]System.Action ConsoleTests.Program6/Chunk::OnTrigger
  IL_006c:  ldloc.0
  IL_006d:  ldfld      class ConsoleTests.Program6/Leaf ConsoleTests.Program6/Chunk::leaf
  IL_0072:  callvirt   instance void ConsoleTests.Program6/Leaf::Go()

请注意,当您对委托执行 += 时,编译器会将其转换为 Combine 调用,该调用将当前委托与传入的委托组合在一起。现在看IL_004b,这是一个dup调用。

From the MSDN: "Copies the current topmost value on the evaluation stack, and then pushes the copy onto the evaluation stack."

这会告诉运行时复制堆栈上的对象,因为这是 ICloneable,它会创建对象的 copy。现在,当您将另一个委托添加到它时,您并没有将它添加到两者,只是您正在操作的那个,让 Leaf 触发器保持原样。

几乎等价的代码是这样的:

public Tree()
{
    var chunk2 = new Chunk();
    chunk2.OnTrigger += Hello; // OK: will be called
    chunk2.leaf.OnTrigger = (Action)chunk2.OnTrigger.Clone();
    chunk2.OnTrigger += World; // NOT: not be called
    chunk2.leaf.Go();
}

这可能会使这里发生的事情更清楚一些。