C# 为什么在未取消订阅事件时,这不会导致内存泄漏

C# 为什么在未取消订阅事件时,这不会导致内存泄漏,c#,.net,memory-leaks,event-handling,windbg,C#,.net,Memory Leaks,Event Handling,Windbg,我试图理解事件是如何导致内存泄漏的。我在stackoverflow问题上找到了一个很好的解释,但在Windg中查看对象时,我对结果感到困惑。首先,我有一个简单的类,如下所示 class Person { public string LastName { get; set; } public string FirstName { get; set; } public event EventHandler UponWakingUp;

我试图理解事件是如何导致内存泄漏的。我在stackoverflow问题上找到了一个很好的解释,但在Windg中查看对象时,我对结果感到困惑。首先,我有一个简单的类,如下所示

class Person
    {
        public string LastName { get; set; }
        public string FirstName { get; set; }

        public event EventHandler UponWakingUp;
        public Person()  {  }

        public void Wakeup()
        {
            Console.WriteLine("Waking up");
            if (UponWakingUp != null)
                UponWakingUp(null, EventArgs.Empty);
        }
    }
public partial class Form1 : Form
    {
        Person John = new Person() { LastName = "Doe", FirstName = "John" };

        public Form1()
        {
            InitializeComponent();

            John.UponWakingUp += new EventHandler(John_UponWakingUp);
        }

        void John_UponWakingUp(object sender, EventArgs e)
        {
            Console.WriteLine("John is waking up");
        }

        private void button1_Click(object sender, EventArgs e)
        {
            John = null;
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
            MessageBox.Show("done");
         }
    }
现在我在windows窗体应用程序中使用这个类,如下所示

class Person
    {
        public string LastName { get; set; }
        public string FirstName { get; set; }

        public event EventHandler UponWakingUp;
        public Person()  {  }

        public void Wakeup()
        {
            Console.WriteLine("Waking up");
            if (UponWakingUp != null)
                UponWakingUp(null, EventArgs.Empty);
        }
    }
public partial class Form1 : Form
    {
        Person John = new Person() { LastName = "Doe", FirstName = "John" };

        public Form1()
        {
            InitializeComponent();

            John.UponWakingUp += new EventHandler(John_UponWakingUp);
        }

        void John_UponWakingUp(object sender, EventArgs e)
        {
            Console.WriteLine("John is waking up");
        }

        private void button1_Click(object sender, EventArgs e)
        {
            John = null;
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
            MessageBox.Show("done");
         }
    }
如您所见,我实例化了Person类并订阅了UponWakingUp事件。我在这张表格上有一个按钮。当用户单击此按钮时,我将此Person实例设置为null,而不取消订阅事件。然后我调用GC.Collect以确保执行了Garbade收集。我在这里显示一个消息框,这样我就可以附加Windbg来查看Form1类的参考帮助,在这个类中我看不到任何对该事件的参考(Windbg输出如下所示,尽管Form1的数据太长,但我显示的是与我的问题相关的)。此类具有对Person类的引用,但为null。基本上,这对我来说并不像是内存泄漏,因为Form1没有任何对Person类的引用,即使它没有取消订阅该活动

我的问题是,这是否会导致内存泄漏。若否,原因为何

0:005> !do 0158d334   
Name:        WindowsFormsApplication1.Form1  
MethodTable: 00366390  
EEClass:     00361718  
Size:        332(0x14c) bytes  
File:        c:\Sandbox\\WindowsFormsApplication1\WindowsFormsApplication1\bin\Debug\WindowsFormsApplication1.exe  
Fields:  
      MT    Field   Offset                 Type VT     Attr    Value Name  
619af744  40001e0        4        System.Object  0 instance 00000000 __identity  
60fc6c58  40002c3        8 ...ponentModel.ISite  0 instance 00000000 site  
619af744  4001534      b80        System.Object  0   static 0158dad0 EVENT_MAXIMIZEDBOUNDSCHANGED  
**00366b70  4000001      13c ...plication1.Person  0 instance 00000000 John**  
60fc6c10  4000002      140 ...tModel.IContainer  0 instance 00000000 components  
6039aadc  4000003      144 ...dows.Forms.Button  0 instance 015ad06c button1  

0:008> !DumpHeap -mt 00366b70    
 Address       MT     Size  
total 0 objects  
Statistics:  
      MT    Count    TotalSize Class Name  
Total 0 objects  

答案实际上在您链接到的问题的答案中:

当侦听器将事件侦听器附加到事件时,源 对象将获得对侦听器对象的引用。这意味着 垃圾收集器在之前无法收集侦听器 事件处理程序被分离,或者源对象被收集

您正在释放对象(
Person
),因此侦听器(您的
表单
)可以被收集,这就是没有内存泄漏的原因

当这种情况正好相反时,即当您想处理
表单
,但事件(您的
对象)仍然存在,并持有对它的引用时,将发生内存泄漏。

这是循环引用的情况。表单引用了通过John字段侦听事件的对象。反过来,当表单的构造函数订阅了表单的UponWakingUp事件时,John有一个对表单的引用

在某些自动内存管理方案中,循环引用可能是一个问题,尤其是在引用计数中。但是.NET垃圾回收器没有问题。只要form对象和Person对象都没有任何附加引用,这两个对象之间的循环引用就无法保持彼此的活动状态

您的代码中没有对这两者的其他引用。这通常会导致两个对象都被垃圾收集。但是Form类是特殊的,只要它的本机Windows窗口存在,Winforms维护的对象句柄表中存储的内部引用就会使Form对象保持活动状态。这让约翰活了下来


因此,通常的清理方法是,用户通过单击右上角的X来关闭窗口。这反过来会导致本机窗口句柄被破坏。这将从该内部表中删除表单引用。下一个垃圾收集现在只看到循环引用,并将它们都收集起来。

在本例中,Form1是源的listner,它是Person类实例。你是说约翰会参考Form1吗????我现在完全糊涂了。@palmsnow是的,
John.UponWakingUp
包含对代理的引用,该代理包含对
Form1
的引用。否则引发该事件将无法工作。有关更多详细信息,请参阅文章。顺便说一句,对于一个结构良好的问题,@svick:我理解。那个引用还没有被清除,我想这可能是内存泄漏。如果你们中的任何人能添加一些例子,可能会更helpful@Benjamin. 谢谢因此,如果我有另一个表单(比如表单2)。我在Form1中有一个类变量,然后dispose Form1和调用GC.Collect(),我是否仍然应该看到Form1没有在Form2中清理,因为在Form1中我们从未取消订阅此事件?