9

我有这段代码

public class Publisher
{
    public event EventHandler SomeEvent;
}

public class Subscriber
{
    public static int Count;

    public Subscriber(Publisher publisher)
    {
        publisher.SomeEvent += new EventHandler(publisher_SomeEvent);
    }

    ~Subscriber()
    {
        Subscriber.Count++;
    }

    private void publisher_SomeEvent(object sender, EventArgs e)
    {
        // TODO
    }
}

在我的应用程序的主要方法中,我有

static void Main(string[] args)
{
    Publisher publisher = new Publisher();

    for (int i = 0; i < 10; i++)
    {
        Subscriber subscriber = new Subscriber(publisher);
        subscriber = null;
    }

    GC.Collect();
    GC.WaitForPendingFinalizers();

    Console.WriteLine(Subscriber.Count.ToString());
}

如果我运行它,我将有 0 作为输出。如果我从代码中删除事件订阅,我会得到预期的结果——即 10。

GC.Collect()被调用时,gc 被强制启动垃圾收集。因为 Subscriber在其中定义了Finalize,GC 将暂停收集,直到finalizequeue为空——也就是说,所有 Subscription 实例都会调用它的Finalize()方法(如果我的假设是错误的,请纠正我)。在下一行GC.WaitForPendingFinalizers()被调用,这将有效地暂停执行,直到终结器队列为空。现在,因为我们有 0 作为输出,我相信Finalize()没有被调用,这让我相信 GC 没有将订阅者实例标记为要收集,因此Finalizer()方法没有被调用。

所以我有2个问题

  1. 我的假设是否正确并且事件订阅会阻止 GC 标记要收集的订阅者实例?
  2. 如果是这样,是因为发布者持有对订阅者的引用吗?(垃圾收集器和事件处理程序

我唯一的猜测是,由于有 10 个 Subscriber 实例引用同一个发布者实例,当 GC 收集发生时,它看到还有其他对发布者的引用,因此无法收集,结果所有订阅与发布者一起的实例正在移动到下一代,因此在代码执行到达Console.WriteLine(Subscriber.Count.ToString())时不会发生垃圾收集,也不会调用Finalize( )

我是对的还是我在这里遗漏了什么?

4

2 回答 2

4

您错误地识别了真正发生的事情,这是 C# 中一个非常常见的陷阱。您需要运行测试程序的发布版本并在没有调试器的情况下运行它(按 Ctrl+F5)。它将在您的用户机器上运行的方式。现在请注意,无论您是否订阅该事件已经完全无关紧要,您将始终获得 10。

问题是,当您使用调试器时,不会收集发布者对象。我在这个答案中详细解释了原因。

稍微扩展一下,这里有循环引用。订阅者对象引用发布者对象。并且 Publisher 对象具有对 Subscriber 对象的引用。循环引用不足以使对象保持活动状态。谢天谢地,如果是这样的话,垃圾收集就不会很有效。必须在其他地方引用发布者对象才能保持活动状态,局部变量不够好。

于 2013-06-18T13:21:52.590 回答
3

这两个问题的答案都是肯定的。

于 2013-06-18T08:57:25.843 回答