【问题标题】:If events are implemented as delegates in .NET, what is the point of the .event IL section?如果事件在 .NET 中实现为委托,那么 .event IL 部分的意义何在?
【发布时间】:2008-10-20 20:35:30
【问题描述】:

我在 Stack Overflow 上看到了一些关于委托、事件和这两个功能的 .NET 实现的非常好的问题。特别是一个问题,“How do C# Events work behind the scenes?”,产生了一个很好的答案,很好地解释了一些微妙的观点。

上述问题的答案说明了这一点:

当你声明一个类似字段的事件时 ...编译器生成方法 和一个私有字段(同类型 作为代表)。课堂内, 当您参考 ElementAddedEvent 你指的是这个领域。外部 类,你指的是 字段

从同一问题(“Field-like events”)链接的 MSDN 文章补充说:

引发事件的概念是 完全等同于调用 事件代表的代表—— 因此,没有特殊的语言 用于引发事件的构造。

为了进一步检查,我构建了一个测试项目,以查看事件和委托编译到的 IL:

public class TestClass
{
    public EventHandler handler;
    public event EventHandler FooEvent;

    public TestClass()
    { }
}

我希望委托字段 handler 和事件 FooEvent 编译为大致相同的 IL 代码,并使用一些额外的方法来包装对编译器生成的 FooEvent 字段的访问。但是生成的 IL 并不是我所期望的:

.class public auto ansi beforefieldinit TestClass
    extends [mscorlib]System.Object
{
    .event [mscorlib]System.EventHandler FooEvent
    {
        .addon instance void TestClass::add_FooEvent(class [mscorlib]System.EventHandler)
        .removeon instance void TestClass::remove_FooEvent(class [mscorlib]System.EventHandler)
    }

    .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
    {
        // Constructor IL hidden
    }

    .field private class [mscorlib]System.EventHandler FooEvent
    .field public class [mscorlib]System.EventHandler handler
}

由于事件只不过是使用编译器生成的addremove 方法的委托,我没想到会在IL 中看到事件被视为更多。但是添加和删除方法是在以.event 开头的部分中定义的,而不是像普通方法那样在.method 开始。

我的终极问题是:如果事件被简单地实现为具有访问器方法的委托,那么拥有.event IL 部分的意义何在?如果没有这个,它们不能通过使用.method 部分在 IL 中实现吗? .event 是否等同于 .method

【问题讨论】:

    标签: c# .net events delegates cil


    【解决方案1】:

    我不确定这是否令人惊讶...比较属性与字段的相同(因为属性在与事件相同的函数之前:通过访问器进行封装​​):

    .field public string Foo // public field
    .property instance string Bar // public property
    {
        .get instance string MyType::get_Bar()
        .set instance void MyType::set_Bar(string)
    }
    

    另外 - 事件提及任何关于字段的内容;他们定义访问器(添加/删除)。委托支持者是一个实现细节;恰好类似字段的事件将字段声明为支持成员 - 就​​像自动实现的属性将字段声明为支持成员一样。其他实现是可能的(并且非常常见,尤其是在表单等方面)。

    其他常见实现:

    稀疏事件(控件等)- EventHandlerList(或类似的):

    // only one instance field no matter how many events;
    // very useful if we expect most events to be unsubscribed
    private EventHandlerList events = new EventHandlerList();
    protected EventHandlerList Events {
        get { return events; } // usually lazy
    }
    
    // this code repeated per event
    private static readonly object FooEvent = new object();
    public event EventHandler Foo
    {
        add { Events.AddHandler(FooEvent, value); }
        remove { Events.RemoveHandler(FooEvent, value); }
    }
    protected virtual void OnFoo()
    {
        EventHandler handler = Events[FooEvent] as EventHandler;
        if (handler != null) handler(this, EventArgs.Empty);
    }
    

    (以上几乎是获胜形式事件的支柱)

    外观(虽然这会让“发送者”有点困惑;一些中间代码通常很有帮助):

    private Bar wrappedObject; // via ctor
    public event EventHandler SomeEvent
    {
        add { wrappedObject.SomeOtherEvent += value; }
        remove { wrappedObject.SomeOtherEvent -= value; }
    }
    

    (上面也可以用来有效地重命名一个事件)

    【讨论】:

    • 您能否提供更多关于其他常见实现的详细信息?我假设您在谈论使用字段作为支持成员之外的其他实现,而不是使用任何类型的委托作为支持成员之外的其他实现。
    • 感谢您的更新。如何指定我希望使用 EventHandlerList 而不是委托字段作为支持成员来实现事件?如果 event 关键字的默认行为是使用委托字段,我是否必须完全绕过 C# 并直接编写 IL?
    • 我已经在上面写过了。默认如果您不指定添加/删除 是使用委托字段。如果您指定添加/删除(如上),您可以做您喜欢的事情。可能也可以通过 postsharp 做到这一点,但我不确定我会打扰......
    【解决方案2】:

    事件与委托不同。事件封装了为事件添加/删除处理程序。处理程序由委托表示。

    可以只为每个事件编写 AddClickHandler/RemoveClickHandler 等 - 但这会比较痛苦,并且不会让 VS 等工具轻松地将事件与其他任何事件分开。 p>

    这真的就像属性一样 - 您可以编写 GetSize/SetSize 等(就像在 Java 中所做的那样),但是通过分离属性,可以使用语法快捷方式和更好的工具支持。

    【讨论】:

    • 你可以对委托做同样的事情,他们支持 += 组合(即使是空委托)
    • 当然,缺点是您不能使用事件重构或构建更高级别的代码。就像 MS 现在非常热门的“基于异步事件”的东西一样——一般地编写代码很痛苦,因为你不能引用事件委托并传递它的类型......
    【解决方案3】:

    拥有一对添加、删除、方法的事件的要点是封装

    大多数时候事件按原样使用,但其他时候您不想将附加到事件的委托存储在字段中,或者您想对添加或删除事件方法进行额外处理。

    例如,实现memory efficient events 的一种方法是将委托存储在字典中而不是私有字段中,因为字段总是被分配,而字典仅在添加项目时才会增长。这个模型类似于 Winforms 和 WPF 使用的有效利用内存的模型(winforms 和 WPF 使用键控字典来存储委托而不是列表)

    【讨论】:

    • 恕我直言,如果没有 remove 方法,事件将被更好地封装——只是一个“add”方法,它返回一个 MethodInvoker,当被调用时,它将删除事件。这将允许 Remove 处理程序从 Add 处理程序接收任意信息。例如,如果订阅是通过链表管理的,在仅给定委托的删除方法中查找特定订阅将需要线性搜索,但如果通过 MethodInvoker 完成删除,则委托可以包含对适当列表节点的引用。
    • @supercat,这将是一种更好的方法,但更昂贵。委托是相当消耗内存的。由于每个添加的事件有 2 个委托,您的方法至少会使事件模型的内存消耗增加一倍。由于事件膨胀导致的内存消耗是 .Net 应用程序中的一个真正问题,它不需要成为一个更大的问题。
    • @Pop:如果委托会使用太多内存,则可以返回一个实现 IUnsubscribe 的对象,其一种方法可以取消订阅事件。选择使用链表还是其他方式来保存事件是调用速度、订阅速度和取消订阅速度之间的权衡。让对象负责执行事件取消订阅的一个优点是,此类对象的终结器可用于取消订阅事件。如果事件委托指向保存终结器的对象,这将不起作用,但是...
    • @Pop: ...如果将接收事件的对象包装在一个持有取消订阅者的对象中,并且所有用户引用都持有外部对象,则订阅将保持活动状态,但仅在,所有对被包装对象的引用都消失了。在任何情况下,让订阅行为返回一个对象,然后该对象将用于取消订阅,这将允许使用更广泛的技术来处理事件订阅,这反过来可以在上述因素之间实现更有利的权衡。
    • @supercat,这样的对象 (IUnsubscribe) 将消耗与委托类似的内存量(32/64 位用于引用存储在调用方的对象,+ 64/128 位对象标头 + 32/64 位对事件对象的引用,+ 更多内存以指示需要删除的委托。
    猜你喜欢
    • 2021-04-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-10-09
    • 1970-01-01
    • 1970-01-01
    • 2019-05-06
    相关资源
    最近更新 更多