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

时间:2015-04-03 11:05:15

标签: c# memory-leaks lambda delegates

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

自从在C#中发现lambda和代表以来,我已成为他们的一大消费者。但是,当涉及在闭包中维护的对象上释放内存时,我一直关注,特别是在处理嵌套闭包时。例如,考虑下面的一组课程我写的是我认为适当的"适当的" 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;
}

这应该做什么(它可能 - 仍然需要测试,我认为我需要在这里和那里进行几次空检查):

  • 当目标上的引用类型属性发生更改时,为依赖属性引发PropertyChanged事件(由dependantRef标识)
  • 此外,将PropertyChanged处理程序附加到foundationHost,以便在基础发生变化时,通知目标
  • 通过在执行事件处理程序之前解析foundationHost并在发生这种情况时进行分离,确保目标不会收到与foundationHostRef不再相关的foundationHost的通知。

因此,使用上述逻辑,并将foundationHostRef s嵌套多层深层的问题(这是一项正在进行的工作)放在一边,它看起来像任何已被引用的对象foundationHostRef将保持对目标的闭包引用,即使它与它不再相关,至少在它试图引发事件之前。

现在我对此的理解是,我创建的事件处理程序可以轻松地阻止目标占用的内存被释放。所有需要发生的事情都是某个对象在某个时刻占用target,然后在其他地方重新分配,并且其生命周期比target更长,具体取决于target正在做什么从烦人的(target是一个不占用大量内存的单身人物)到灾难性的({{1}}在程序生命周期内被创建和消除了数千次,并且有一些属性占用了很多内存,GC从不收集它。)

所以,我的问题:对于这种事情,有什么内在的保护措施,如果有的话?如果没有,我应该如何调整我的代表/ lambdas,以便他们不再是邪恶的?

1 个答案:

答案 0 :(得分:1)

您可以通过两件事来减轻这种风险:

  1. 如果您认为特定的代码存在风险,请尽量不要在那里使用lambdas。以旧方式进行闭包:通过在类上创建实例方法。
  2. 安装Resharper Allocation Plugin。查看常见C#习语的所有分配是非常有教育意义的。该插件会告诉您关闭了哪些变量。 R#警告所捕获的变量&#34;隐含地&#34;同样(不确定这意味着什么,但对你来说听起来像是一个有用的警告)。
  3.   

    看起来像任何被FoundationHostRef引用的对象都会保持对目标的闭包引用,即使它与它不再相关

    我完全不遵守上面给出的代码。从来没有从闭包中引用无限量的对象。对象引用的数量是不变的。

    但是,可能存在的问题是,看似未使用或超出范围的局部变量仍然会影响强引用。这是因为C#编译器在关闭变量未使用时不会将其置空。对于普通的当地人来说,它不会这样做,但JIT足够聪明,可以做同样的事情。

    在我知道的每种情况下,显式地清空你想要清除的变量(尽管它可能不受C#规范保证)。