正确处理闭包中维护的引用以避免内存泄漏

Proper handling of maintained references in closures to avoid memory leaks

(前缀:对于那些看到这里并认为 TL;DR 的人,实际问题在最后)

自从在 C# 中发现了 lambda 和委托,我就成了它们的忠实用户。但是,在释放闭包中维护的对象上的内存时,我一直很担心,尤其是在处理嵌套闭包时。例如,考虑下面的一组 类 我正在写我认为是 "appropriate" INotifyPropertyChanged 行为的内容。

public static PropertyChangedEventHandler GetHandler<TDependant, TDependantHost, TFoundation, TFoundationHost>
    (
        this TDependantHost target,
        PropertyChangedEventHandler invokeTarget,
        Expression<Func<TDependantHost, TDependant>> dependantRef,
        Expression<Func<TFoundationHost, TFoundation>> foundationRef,
        Expression<Func<TDependantHost, TFoundationHost>> foundationHostRef
    )
    where TDependantHost : ISupportsDependencyManager
    where TFoundationHost : class, INotifyPropertyChanged
{
    string foundationName = GetPropertyInfo(foundationRef).Name;
    string dependantName = GetPropertyInfo(dependantRef).Name;
    string foundationHostName = GetPropertyInfo(foundationHostRef).Name;
    Func<TDependantHost, TFoundationHost> foundationHostRefCompiled = foundationHostRef.Compile();
    PropertyChangedEventHandler oOut = null;

    // Complex situation. This is more complex because whilst TDependantHost bears a relationship to TFoundationHost
    // the actual dependency is on a property in TFoundationHost.
    // oOut is the property changed handler that will be attached to target, so it needs to
    // - Raise changed events whenever foundationHostRef would evaluate to a different object
    // - Whenever that change occurs, attach a new PropertyChangedEventHandler to the new foundationHost
    // - ... which also handles removal of itself from target so as to guarantee
    oOut = (s, e) =>
    {
        var sender = s as INotifyPropertyChanged;
        if (sender == null)
            return;
        if (e.PropertyName == foundationHostName)
        {
            // The Foundation Host has changed. So we need to attach a new inner PropertyChangedEventHandler to it.
            PropertyChangedEventHandler innerHandler = null;
            innerHandler =
                (s2, e2) =>
                {
                    // Caller safety...
                    var innerSender = s2 as TFoundationHost;
                    if (innerSender == null)
                        return;

                    // Check and see if this eventhandler still points to the right object
                    // If it does, we'll keep going - otherwise, got to remove the event handler and return
                    if (foundationHostRefCompiled(target) != innerSender)
                    {
                        innerSender.PropertyChanged -= innerHandler;
                        return;
                    }

                    // Now we know that the inner handler is executing for an entity that still bears the correct 
                    // relationship to target. So we just check the same way as usual - did foundation just change?
                    // If so, so did dependant
                    if (e2.PropertyName == foundationName)
                        invokeTarget.SafeInvoke(target, dependantName);
                };

            // since the foundation has shifted, the dependency will also have changed
            // Raise a handler for it.
            invokeTarget.SafeInvoke(sender, dependantName);
        }
    };
    return oOut;
}

这应该做什么(它可能 - 仍然需要测试,我想我需要在这里和那里进行一些空检查)是:

所以使用上面的逻辑,抛开嵌套超过一层的 foundationHosts 的问题(这是一项正在进行的工作),看起来任何对象都被引用过to by foundationHostRef 将保持对 target 的闭包引用,即使它不再与它相关,至少在它尝试引发事件之前是这样。

现在我的理解是,我创建的事件处理程序可以轻松地阻止 target 占用的内存被释放。所有需要发生的事情就是某个对象在某个时候占据 foundationHostRef,然后被重新分配到其他地方,并且比 target 具有更长的生命周期,具体取决于 target 正在做的事情从恼人的(target 是一个不占用太多内存的单例)到灾难性的(target 在程序生命周期中被创建和清空数千次,并且有一些 属性占用大量内存,GC从不回收)。

所以,我的问题是: 有什么内置保护措施(如果有的话)可以防止这种情况发生?如果没有,我应该如何调整我的 delegates/lambdas 让他们不再邪恶?

您可以通过两件事来降低这种风险:

  1. 如果您认为某段代码存在风险,请尽量不要在那里使用 lambda。以旧方式执行闭包:通过在 class.
  2. 上创建实例方法
  3. 安装 Resharper Allocation Plugin。看到常见的 C# 习语所做的所有分配是很有教育意义的。该插件会告诉您关闭了哪些变量。 R# 也会对捕获的变量发出警告 "implicitly"(不确定那是什么意思,但这听起来像是对您有用的警告)。

it looks like any object that has EVER been referred to by foundationHostRef will maintain a closure reference to target even when it is no longer related to it

我没有完全遵循上面给出的代码。从来没有无限数量的对象被闭包引用。对象引用的数量是恒定的。

不过,可能会出现问题的是,看起来未使用或超出范围的局部变量仍然会影响强引用。这是因为 C# 编译器在闭包变量未使用时不会清空它们。它也不会为普通本地人做那件事,但 JIT 足够聪明,可以做同样的事情。

在我所知道的每种情况下,显式清零您想要清除的变量都有效(尽管 C# 规范可能不保证)。