在收集实例之前,如何将 WeakReference<Action> 保存到实例的方法?

How can I hold a WeakReference<Action> to a method of an instance until the instance is collected?

class Program
{
    static void Main(string[] args)
    {
        var inst = new SomeClass();
        var weakRef = new WeakReference<Action>(inst.DoSomething);
        GC.Collect();
        Console.WriteLine($"inst is alive = {inst != null} : weakRef.Target is alive = {weakRef.TryGetTarget(out Action callback)}");
        Console.ReadLine();
    }
}

public class SomeClass
{
    public void DoSomething() { }
}

输出显示inst不为null,但WeakReference<Action>指向的引用为。我预计这是因为创建了一个指向实例方法的新 Action,而不是存储对实例方法本身的引用。

在实例尚未被垃圾回收期间,如何保持对对象实例方法的弱引用?

从 MethodGroup 创建一个 Action 实例会创建一个不以任何方式附加到该方法所属的 class 实例的实例。这意味着没有什么可以让你的 Action root,GC 会很乐意收集它。

如果您将对 Action 的引用存储为 class 的成员,它将植根于该实例的生命周期,只要该实例还在,它就不会被收集还活着。

public class SomeClass
{
    public SomeClass()
    {
        DoSomething = this.DoSomething_Internal ;
    }

    public Action DoSomething { get; } 

    private void DoSomething_Internal() { }
}

注意:我将原来的 DoSomething 设为私有,并将其重命名为 DoSomething_Internal,将旧的 DoSomething 替换为只读的 属性 以保留 class 签名尽可能接近你原来的 class。您不需要完全遵循此模式,对存储在 class 中的 Action 的任何引用,包括在普通字段中,都可以。如果您真的想使用它,您仍然必须以某种方式公开该引用。

你可以这样测试:

var inst = new SomeClass();
var weakRef = new WeakReference<Action>(inst.DoSomething);

GC.Collect();
GC.WaitForPendingFinalizers(); // You should do this after forcing a GC, just in case there is still GC work being done in the background.

Console.WriteLine($"inst is alive = {inst != null} : weakRef.Target is alive = {weakRef.TryGetTarget(out Action callback1)}");

// These next 2 lines discard local variables, as Hans points out in the comments,  
// DO NOT do this in production code.  Please read the link he posted for more details.
inst = null; // discard the class instance
callback1 = null; // discard the temporary Action instance from TryGetTarget, otherwise it will act as a GC Root, preventing it from being collected later.

GC.Collect();
GC.WaitForPendingFinalizers();

Console.WriteLine($"inst is alive = {inst != null} : weakRef.Target is alive = {weakRef.TryGetTarget(out Action callback2)}");

产生以下输出:

inst is alive = True : weakRef.Target is alive = True
inst is alive = False : weakRef.Target is alive = False

如果您需要 Action 实例不会在 SomeClass 实例之前被收集,那么您需要添加从 SomeClass 实例到 Action 实例的引用。可以是指向Action实例的SomeClass实例字段,但是如果你不能改变SomeClass定义,那么你可以使用ConditionalWeakTable<TKey,TValue> class来附加动态字段。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default)]
public class SomeClass {
    public void DoSomething() { }
}
public static class DelegateKeeper {
    private static ConditionalWeakTable<object, List<Delegate>> cwt = new ConditionalWeakTable<object, List<Delegate>>();
    public static void KeepAlive(Delegate d) => cwt.GetOrCreateValue(d?.Target ?? throw new ArgumentNullException(nameof(d))).Add(d);
}
static class Program {
    static void Main() {
        SomeClass inst = new SomeClass();
        Action a1 = inst.DoSomething;
        DelegateKeeper.KeepAlive(a1);
        Action a2 = inst.DoSomething;
        WeakReference<SomeClass> winst = new WeakReference<SomeClass>(inst);
        WeakReference<Action> wa1 = new WeakReference<Action>(a1);
        WeakReference<Action> wa2 = new WeakReference<Action>(a2);
        GC.Collect();
        Console.WriteLine($"{winst.TryGetTarget(out _),5}:{wa1.TryGetTarget(out _),5}:{wa2.TryGetTarget(out _),5}");
        GC.KeepAlive(a1);
        GC.KeepAlive(a2);
        GC.Collect();
        Console.WriteLine($"{winst.TryGetTarget(out _),5}:{wa1.TryGetTarget(out _),5}:{wa2.TryGetTarget(out _),5}");
        GC.KeepAlive(inst);
        GC.Collect();
        Console.WriteLine($"{winst.TryGetTarget(out _),5}:{wa1.TryGetTarget(out _),5}:{wa2.TryGetTarget(out _),5}");
    }
}

输出:

 True: True: True
 True: True:False
False:False:False

tio.run

DelegateKeeper class 中,我使用 List<Delegate> 作为依赖对象类型,因此您可以为每个 class 实例保留多个委托。我使用 Delegate.Target 作为 table 的键,因此您不需要单独传递实例。这不适用于匿名方法,因为它们可能让编译器在 Target 属性 中生成闭包 class。 GetOrCreateValue 获取绑定到键的值或使用默认构造函数创建新值并自动将其添加到 table。