【问题标题】:WeakReference to Action target is always alive for anonymous actions对 Action 目标的弱引用对于匿名操作始终有效
【发布时间】:2016-11-06 15:49:10
【问题描述】:

我正在尝试使用事件聚合器,并将操作存储在包装类中,在其中我将实际操作方法存储为Delegate,而操作方法所在的类为WeakReference

当我想调用事件处理程序时,我首先检查 WeakReference.IsAlive 是否为真。如果是,则调用它,否则我将其从集合中删除,因为这意味着它所属的对象已被清空/垃圾收集。

但是,当我创建一个匿名操作并将其添加到我的事件聚合器时,它始终处于活动状态,即使我将父类设为空(并强制运行垃圾收集器)。

如何让 IsAlive 将匿名方法设置为 FALSE?

void AddHandler<TEvent>(Action<TEvent> callback) {
    InternalHandler handler = new InternalHandler(callback);
    // Store the handler somewhere to use the callback later, if it still alive
}

class InternalHandler {
    WeakReference _reference;
    Delegate _method;

    public InternalHandler(Delegate handler) {
        _reference = new WeakReference(handler.Target);

        Type messageType = handler.Method.GetParameters()[0].ParameterType;
        Type delegateType = typeof(Action<,>).MakeGenericType(handler.Target.GetType(), messageType);

        _method = Delegate.CreateDelegate(delegateType, handler.Method);
    }

    bool IsAlive => _reference != null && _reference.IsAlive;

    bool Invoke(object data) {
        if (!IsAlive) return false;

        if (_reference.Target != null) _method.DynamicInvoke(_reference.Target, data);

        return true;
    }
}

在测试应用中

TempObject t = new TempObject();

// Some time later
t = null;
GC.Collect();

class TempObject {

    public TempObject() {
        myHandler.AddHandler<SomeObject>(o => {
            // Some code with a breakpoint
        });
    }
}

在调用GC.Collect()并发出一个新事件后,t中的匿名方法中的断点仍然被调用!

如何为匿名方法获取正确的WeakReference

【问题讨论】:

    标签: c# delegates garbage-collection action weak-references


    【解决方案1】:

    假设您没有在匿名的TempObject 处理程序中引用任何实例变量(因此,不要使用“this”),因为这样的假设会导致观察到的行为。

    要完全理解原因,最简单的方法是查找 C# 编译器为您提到的匿名方法生成的代码。由于使用的类和变量的名称,代码相当不可读,但这里是一个有点美化的版本:

    class TempObject {
        public TempObject(Handlers handlers) {
            handlers.AddHandler<object>(GeneratedClass._staticAction ?? (GeneratedClass._staticAction = GeneratedClass._staticField.Handler));
        }
    
        [CompilerGenerated]
        [Serializable]
        private sealed class GeneratedClass {
            public static readonly TempObject.GeneratedClass _staticField;
            public static Action<object> _staticAction;
    
            static GeneratedClass() {
                TempObject.GeneratedClass._staticField = new TempObject.GeneratedClass();
            }
    
            public GeneratedClass() {
    
            }
    
            internal void Handler(object o) {
                Console.WriteLine(o);
            }
        }
    }
    

    你在这里看到编译器生成了一个新类(这里命名为GeneratedClass),它有一个静态字段引用了这个类的一个实例,还有另一个静态字段引用了你的匿名处理程序被缓存。因此,您的匿名委托实际上是 GeneratedClass 类实例的实例方法(名为 Handler),该实例存储在 static 字段中。

    此时您应该已经意识到为什么会观察到这种行为。您的参考是

     _reference = new WeakReference(handler.Target);
    

    在这种情况下,handler.Target 引用了永远不会设置为 null 的静态字段,因此永远不会被垃圾收集。

    另一种直观地理解它的方法是,您永远不会在匿名处理程序中使用与TempObject实例相关的任何内容,因此您的匿名方法基本上是对静态方法的引用(从概念上讲) ,而不是实例方法。所以它实际上与TempObject 实例没有任何关系,它的生命周期与TempObjects 的生命周期无关。所以基本上是一样的:

    class TempObject {
        public TempObject(Handlers handlers) {
            handlers.AddHandler<object>(Handler);
        }
    
        private static void Handler(object arg) {
            Console.WriteLine(arg);
        }
    }
    

    当然,您的方法无法按预期用于静态方法也就不足为奇了(实际上对于静态方法,handler.Target 将为空,所以我想您的代码会失败)。

    现在让我们稍微改变一下:

    class TempObject {
    
        public TempObject(Handlers handlers) {
            handlers.AddHandler<object>(o => {
                Console.WriteLine(o + this.Name);
            });
        }
    
        public string Name { get; set; }
    }
    

    这次您的处理程序确实引用了“this”,因此它与TempObject 类的实例相关。编译器会为此状态生成不同的代码,我不会在这里展示,但最终结果将是,在这种情况下,当 TempObject 被垃圾回收时,您的 WeakReference 将不存在,因此将按预期工作。

    【讨论】:

    • 感谢您的详细回复 :) 我确实注意到,如果我添加了对“this”的引用,那么当 TempObject 被 GC 时,它就会被 GC。但是,我有一些用例不会引用“this”,这意味着将创建静态 GeneratedClass。当 TempObject 被 GC'ed 时,有没有办法删除这个静态引用?如果有,我怎么能确保 _reference.Target != null 即使它是静态的?换句话说,我如何在不需要引用“this”的情况下为这个问题创建一个解决方法?
    • 不,没有办法删除它,因为它是只读的(即使你以某种方式设法使用反射)。传递常规静态方法的情况相同——它们的 Target 为空,你不能用它做任何事情。我建议您允许明确传递“所有者”对象:AddHandler(owner, (0) => ....)。如果存在 - 将其用作您的弱参考。如果您将其设为可选,仍有可能滥用您的 api,因此您可以考虑将其设为必需。
    • 我想我现在明白了。出于好奇,这会发生在所有匿名方法上,还是仅发生在我的事件处理程序保留引用的那些方法上?如果不管我的事件处理程序如何都会发生这种情况,这会不会是 C#.Net 中的一种内存泄漏?
    • 是的,无论您的处理程序如何,都会发生这种情况。但这与引入您自己的静态方法大致相同(请记住,当您从外部范围引用某些内容时,例如“this”,它的工作方式不同并且目标对象被垃圾收集)。所以编译器引入了几个静态变量来缓存你的委托引用,只有一次(不是每次,所以我的意思不是每次你创建 TempObject 时),所以我很难说出那个内存泄漏。它比静态方法贵一点,但不会太多。
    猜你喜欢
    • 2019-05-05
    • 1970-01-01
    • 1970-01-01
    • 2012-10-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多