在释放对象时,是否需要将自定义事件设置为null?

16
假设我们有两个对象,Broadcaster和Listener。 Broadcaster有一个名为Broadcast的事件,Listener已订阅该事件。 如果Listener在不取消订阅Broadcast事件的情况下被处理,由于包含它的事件委托引用,它将保留在内存中。
我好奇的是,如果Broadcaster在Listener取消订阅或Broadcaster设置Broadcast = null之前被处理,Broadcaster是否会被保留在内存中?
除了一个博主认为不设置事件为null将使源代码保留在内存中(在这里找到),我没有找到任何硬性答案来回答这个问题。
我想听听为什么或为什么不这样做的解释。
谢谢。
更新: 论坛线程,在此开发人员指出事件应设置为null,但Jon Skeet表示没有必要,但没有详细说明。

我猜你的意思是“Listener会被保留在内存中吗?” - Marc Gravell
我不是。我指的是广播器。问题是关于在订阅者仍然存在时事件源被销毁的情况。 - Dan Rigby
我看了这篇博客文章,他是100%错误的 - 这是一个单向链接。我会尝试为您证明... - Marc Gravell
这是指博客文章中的“disposing”部分吗?该部分讨论了一种特定情况,其中Broadcaster被明确注入到Listener中 - 他并没有试图证明这是默认行为。 - Jeff Sternal
我的评论是针对这一行的:“如果监听器类向事件注册,那么在幕后,每个类之间都会维护一个链接或引用,一旦监听器已经向事件注册。”这是错误的。 - Marc Gravell
2个回答

9
请注意,委托不会使发布者保持活动状态(它们只会使目标=订阅者保持活动状态),因此仅有的订阅数量是无法使广播器保持活动状态的。从这个角度来看,它是否被处理并不重要。当没有任何项引用广播器时(事件订阅对此无关紧要),它将符合收集条件。
本质上,委托是一个(列表)由MethodInfo和对象引用组成的一对;要调用的方法和要作为“arg0”(也称为this)调用的对象。它根本没有引用触发事件的对象。
这里有证据表明监听器不会使源保持活动状态;您应该看到“Source 1”被收集,即使我们仍然有匹配的已订阅的监听器。如预期的那样,“Listener 2”没有被收集,因为我们仍然有匹配的广播器。
class DataSource
{
    public DataSource(string name) { this.name = name; }
    private readonly string name;
    ~DataSource() { Console.WriteLine("Collected: " + name); }

    public event EventHandler SomeEvent;
}
class DataListener
{
    public DataListener(string name) { this.name = name; }
    private readonly string name;
    ~DataListener() { Console.WriteLine("Collected: " + name); }
    public void Subscribe(DataSource source)
    {
        source.SomeEvent += SomeMethodOnThisObject;
    }
    private void SomeMethodOnThisObject(object sender, EventArgs args) { }
}

static class Program
{
    static void Main()
    {
        DataSource source1 = new DataSource("Source 1"),
                source2 = new DataSource("Source 2");
        DataListener listener1 = new DataListener("Listener 1"),
                listener2 = new DataListener("Listener 2");
        listener1.Subscribe(source1);
        listener2.Subscribe(source2);
        // now we'll release one source and one listener, and force a collect
        source1 = null;
        listener2 = null;
        GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
        GC.WaitForPendingFinalizers(); // source 1 gets collected, ONLY

        Console.WriteLine("Done");
        Console.ReadLine();
        GC.KeepAlive(source2); // prevents collection due to optimisation
        GC.KeepAlive(listener1); // prevents collection due to optimisation
    }
}

是的,但我可以提出一个回应,即MethodInfo引用要调用的方法,该方法位于订阅者上,因此在广播器和订阅者之间创建了一个引用。解释可能是引用的方向(广播器->订阅者,而不是相反)。 - Dan Rigby
@Dan - 当然,它是从广播器到订阅者的方向,这正是为什么引用不会保持广播器活动的原因。 - Marc Gravell
在简单的情况下,如果事件发布者在“Dispose”上将其订阅者列表置空,那么这并不重要。另一方面,如果某个东西在被处理后仍然保留着对事件发布者的无用根引用,则任何未被置空的订阅者列表都将成为对这些订阅者的无用根引用。在处理时清除订阅者列表可以将本来可能导致大量内存泄漏的问题变成小问题。有时这是一件好事(如果它将会在45分钟后杀死程序的内存泄漏变成了在45天后才杀死它,并且该程序只需要运行60分钟)。 - supercat
有时这可能是一件坏事(如果它将会在45分钟后导致程序崩溃的内存泄漏转变成45天后才导致程序崩溃的内存泄漏,并且该程序需要运行46天;前者的内存泄漏可能比后者更快被捕捉到,而后者可能导致浪费更多的时间来运行一个几乎无法工作的程序)。 - supercat

5
不行。Broadcast事件中的委托目标引用了Listener对象。这将使得Listener对象保持活动状态。Listener对象没有任何对Broadcast对象的反向引用。
注意术语。释放Broadcast对象无济于事。它必须被垃圾回收,只有当对象没有剩余引用时才会发生。当这种情况发生时,委托对象也将自动被收集,因为委托对象内部维护了私有事件委托对象的委托目标列表的唯一引用。这也删除了委托对侦听器的引用。如果没有其他对侦听器的引用,它也将被收集。如果仍然存在,则只是不再接收事件通知。长话短说:您不必在Broadcast类中显式设置事件为null。
在监听器中并非完全相同,它由其订阅的事件引用。如果它被声明为不适合业务(已处理),但广播仍然存活,则应明确删除其事件订阅。SystemEvents类是其中的极端版本,其事件是静态的。在引用已处理的侦听器的委托上触发事件是您倾向于注意到的事情。
大多数实用的对象模型都会确保当父项消失时侦听器对象消失。Windows Forms将是一个很好的例子。然后无需显式取消订阅事件。

实际上,您可以从声明它的类中将事件设置为null。 - Alfred Myers
“Which is something the Broadcast class cannot do, it can't generate a delegate instance to unsubscribe.” 的意思是什么? - Alfred Myers

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