事件侦听器委托中的自引用(c#)

本文关键字:自引用 侦听器 事件 | 更新日期: 2023-09-27 18:03:07

这可能是一个语义问题,但也可能不是,所以我问:下面两个片段有明显的区别吗?

public Parent()
{
    Child newChild = new Child();
    newChild.RequestSpecialEvent += (sender, e) =>
    {
        newChild.DoMagic();
    }
}

public Parent()
{
    Child newChild = new Child();
    newChild.RequestSpecialEvent += (sender, e) =>
    {
        ((Child)sender).DoMagic();
    }
}

明显的区别是选项1是自我引用自己,而选项2对对象执行强制类型转换。在性能方面,我预计演员会更贵。

然而,我的理论,在选项1中,它是技术上的"Parent"持有"newChild"的引用(通过在Parent中定义的委托),所以即使newChild消失(newChild = null或类似的东西),newChild对象也不能被垃圾收集(gc'ed),因为Parent已经定义了一个仍然附加到它的委托。newChild只能在Parent最终消失时被gc。

然而,在选项2中,Parent从来没有创建这样一个对newChild的"硬引用",所以当newChild = null发生时,它真的可以立即被gc。

我更喜欢选项1,因为它简洁易读,但担心选项2会更好。想法吗?我的理论是对的还是错的?是否有一种替代或更优选的方法(具有合理的推理)来声明与父/子类相同的事件侦听器关系?

对@StriplingWarrior的回应:

关于垃圾收集,我仍然有点怀疑。委托引用了newChild,所以对我来说,在委托消失之前newChild是不会消失的。现在,如果newChild消失,委托就会消失。但是直到delegate消失,newChild才会消失!似乎是循环的(几乎)。似乎这是必须发生的:

//newChild = null;
//This alone won't truly free up the newChild object because the delegate still
//points to the newChild object.
//Instead, this has to happen
newChild.RequestSpecialEvent = null; //destroys circular reference to newChild
newChild = null; //truly lets newChild object be gc'd

或者可以说'newChild = null;', newChild。RequestSpecialEvent停止指向委托,这允许委托离开,然后允许newChild离开?也许我只是说服自己接受了你的答案。:)

事件侦听器委托中的自引用(c#)

你的想法似乎非常正确,除了我很确定newChild仍然可以在选项1中被垃圾收集,因为引用它的委托本身仅由newChild本身的处理程序引用。

这两个片段在功能上是相同的,选项1使用稍微更多的内存,因为在委托中有额外的引用,但调用时速度略快,因为它避免了强制转换。这两种方法的区别都可以忽略不计,所以我建议使用感觉最干净的方法(如果你问我的话,我建议使用第一种方法)。

只有当你想把同一个委托应用到多个控件上时,我才会选择#2。在这种情况下,这将最终使用更少的内存:
var handler = new RequestSpecialEventHandler((sender, e) =>
    {
        ((Child)sender).DoMagic();
    });
foreach(var child in children)
{
    child.RequestSpecialEvent += handler;
}

另一个需要注意的警告是,由于选项#1指的是newChild变量,如果该变量的值稍后在方法中更改,则在调用处理程序时将使用新值。例如,在这个例子中:

foreach(var child in children)
{
    child.RequestSpecialEvent += (sender, e) =>
        {
            // BEWARE: Don't do this! Modified closure!
            child.DoMagic();
        };
}

…当事件在这些子元素上触发时,"Magic"将只在children集合中的最后一个子元素上执行N次

对于。net GC来说,循环引用不是问题,因为它不使用引用计数来识别活动对象。GC将识别那些保证仍在使用的引用,称为根(例如静态引用,当前执行方法堆栈上的引用等)。这些根所引用的对象保证是活的。传递性地,这些对象所引用的对象被保证为活动的。因此,GC可以跟踪从根到所有已知活动的对象的路径。其余的都死了。

在您的循环引用示例中,没有根指向newChild。因此,它将符合收集条件,即使事件处理程序引用回newChild,而newChild引用事件处理程序,而事件处理程序引用newChild…

你的例子中的Parent类没有保留任何对newChild的引用——它只是在Parent的构造函数中作为一个局部创建的。因此,在分配事件处理程序之后,它应该有资格直接进行收集。

这是一篇关于GC的好文章:http://msdn.microsoft.com/en-us/magazine/bb985010.aspx