两种不同类型的将事件分配给事件处理程序的区别

3

我在 Stack Overflow 上看到了这段示例代码,其中一种做法被认为是不好的,而另一种则是好的。但我不明白为什么? 实际上,我遇到了那个著名的 RCW COM 对象错误,那篇文章说这可能是一个原因。

public class SomeClass
{
    private Interop.ComObjectWrapper comObject;
    private event ComEventHandler comEventHandler;

    public SomeClass()
    {
        comObject = new Interop.ComObjectWrapper();

        // NO - BAD!
        comObject.SomeEvent += new ComEventHandler(EventCallback);

        // YES - GOOD!
        comEventHandler = new ComEventHandler(EventCallback);
        comObject.SomeEvent += comEventHandler
    }

    public void EventCallback()
    {
        // DO WORK
    }

编辑:这里是源链接:COM对象已经与其底层RCW分离,无法使用


@Servy:他所建议的是否正确?如果是,请您能否详细解释一下?这只适用于COM事件处理程序还是普通的.NET对象也应该这样做? - Bohn
@BDotA 我可以肯定地告诉你,对于大多数事件处理程序来说,这种做法都没有意义;通常情况下,他所建议的做法都是不必要的膨胀。由于我没有进行任何COM工作,所以无法确定在那种情况下是否需要它。无论如何,我建议在评论中询问他详细说明。 - Servy
@Servy:我其实不确定,在字段中持有委托的引用是否能解决弱引用的问题。即使在comEventHandler字段中持有委托的引用,也不能防止整个对象图被垃圾回收。我们仍然应该为类型为SomeClass的整个对象添加额外的根来防止整个图形被GC。 - Sergey Teplyakov
私有成员comEventHandler是一个“事件”,这让人感到困惑。如果将comEventHandler设置为字段,代码会更易读和更简单。甚至可以将其设置为只读字段。 - Jeppe Stig Nielsen
1
@BDotA请查看我的答案以获取更多细节。 - Sergey Teplyakov
显示剩余7条评论
1个回答

4

我认为这两个代码片段是相同的,我们在这里没有任何关于强/弱引用的问题。

背景

首先,如果我们的Interop.ComObjectWrapper提供了CLR事件(即存储事件处理程序的委托),我们肯定会从ComObjectWrapper获得一个强引用到我们的对象。

任何委托都包含两部分:Target类型为object和指向特定方法的方法指针。如果Targetnull,则回调指向静态方法。

不可能有一个带有Target类型为WeakReference的委托。有所谓的弱事件模式,但它是在EventManager上实现的,而不是普通的委托。

将事件处理程序存储在字段中也无济于事。第一部分

内部事件实现意味着在订阅事件之后:

comObject.SomeEvent += EventCallback;

comObject对象隐式地持有对SomeClass对象的强引用。无论使用什么样的订阅技术以及ComObject是否是COM对象包装器,这都是真实的。

订阅事件会在生命周期方面隐式地增加两个对象之间的依赖关系。这就是为什么在.NET世界中,最常见的内存泄漏是由于订阅长时间存在的对象的事件而导致的。只要事件持有者在应用程序中可访问,事件订阅者就不会死亡。

将事件处理程序存储在字段中也没有帮助。第二部分

但是,即使我的假设不成立,ComObjectWrapper提供了某种弱事件模式的概念,将事件处理程序保存在字段中也没有任何帮助。

让我们回顾一下事件关键字的含义:

private event ComEventHandler comEventHandler;
... 
comEventHandler = new ComEventHandler(EventCallback);

将回调函数保存在当前字段中(基本上我们可以将私有事件视为简单的委托字段),不会改变现有行为。
我们已经知道,委托是一个简单的对象,用于存储对目标对象(即SomeClass对象)和方法(即public void EventCallBack())的引用。这意味着在字段中存储附加的委托会向SomeClass本身添加对SomeClass的附加引用。
基本上,在字段中存储事件处理程序在语义上等同于在SomeClass中存储附加引用: private SomeClass someClass; public SomeClaas() { //这与在comEventHandler字段中存储委托基本相同 someClass = this; }
在SomeClass中存储强引用不会延长当前对象的生命周期。这意味着如果ComObjectWrapper没有保持对SomeClass对象的强引用,则在comEventHandler中存储事件处理程序不会延长SomeClass的生命周期,并且不会防止SomeClass被垃圾回收。
结论
在私有字段中存储事件处理程序不会延长对象的生命周期,也不会防止其被垃圾回收。
这就是以下代码片段在对象生命周期方面没有区别的原因:
    // GOOD!
    comObject.SomeEvent += new ComEventHandler(EventCallback);

    // EVEN BETTER!
    comObject.SomeEvent += EventCallback;

    // NOT GOOD, BECAUSE WAN'T HELP!
    comEventHandler = new ComEventHandler(EventCallback);
    comObject.SomeEvent += comEventHandler

非常感谢 Sergey 的答案,我学到了新东西。那么回到主要的问题,你提到:“只要在应用程序中访问事件持有者,事件订阅者就不会死亡。”请问您能否建议一些调试技巧或解决此问题的解决方案,以定位问题并解决它? - Bohn
你应该查看此帖子中提到的其他评论 - https://dev59.com/DHI_5IYBdhLWcg3wAeLl。 为了检查对象生命周期是否有问题,您可以将此对象的实例添加到静态对象列表中: private static List<SomeObject> _neverDyingList = new List<SomeObject>();// 在方法中 _neverDyingList.Add(this);如果这不起作用,那就意味着您有其他问题,比如Appartment问题或其他问题。 - Sergey Teplyakov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接