C# 为什么WeakReference在析构函数中是无用的?

C# 为什么WeakReference在析构函数中是无用的?,c#,weak-references,C#,Weak References,考虑以下代码: class Program { static void Main(string[] args) { A a = new A(); CreateB(a); GC.Collect(); GC.WaitForPendingFinalizers(); Console.WriteLine("And here's:" + a); GC.KeepAlive(a); }

考虑以下代码:

class Program
{
    static void Main(string[] args)
    {
        A a = new A();
        CreateB(a);

        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine("And here's:" + a);
        GC.KeepAlive(a);
    }

    private static void CreateB(A a)
    {
        B b = new B(a);
    }
}

class A
{ }

class B
{
    private WeakReference a;
    public B(A a)
    {
        this.a = new WeakReference(a);
    }

    ~B()
    {
        Console.WriteLine("a.IsAlive: " + a.IsAlive);
        Console.WriteLine("a.Target: " + a.Target);
    }
}
具有以下输出:

a.IsAlive: False
a.Target:
And here's:ConsoleApp.A
为什么它是假的和空的?A还没有收到

编辑:哦,你这个小信仰的人

我添加了以下行:

Console.WriteLine("And here's:" + a);
GC.KeepAlive(a);

请参阅更新的输出。

其中的关键问题是,在终结器期间,您正在访问一个参考字段。潜在的问题是,
WeakReference
本身已经(或者可以不可预测地)被收集(因为收集顺序是不确定的)。简单地说:
WeakReference
不再存在,您可以在一个重影对象上查询
IsValid
/
Target

因此,访问这个对象是不可靠和脆弱的。终结器应与直接值类型状态-句柄等对话。任何引用(除非您知道它将始终在被销毁的对象之外)都应受到不信任的对待并避免使用

相反,如果我们传入
WeakReference
,并确保未收集
WeakReference
,则一切正常;下面应该显示一个成功(我们在
WeakReference
中通过的一个),和一个失败(我们仅为此对象创建了
WeakReference
,因此它可以与对象同时收集):

你为什么说它没有被收集?它看起来合格。。。。活动对象上没有字段保存它,并且变量的读取永远不会超过该点(事实上,该变量可能已经被编译器优化掉,因此IL中没有“局部”变量)

您可能需要在
Main
的底部添加一个
GC.KeepAlive(a)
来停止它

为什么它是假的和空的?A还没有收到

你不一定知道。GC可以在不再需要它时立即收集它——在本例中,这是在它被塞进WeakReference之后


顺便说一句,Raymond Chen最近对这个话题有了一个新的看法。

垃圾收集器已确定
a
已死亡,因为它在GC.collect()之后不再被引用。如果将代码更改为:

GC.Collect();
GC.WaitForPendingFinalizers();
System.Console.WriteLine("And here's:"+a);

在B的定稿过程中,您会发现
a
处于活动状态。

更新问题的更新答案

对于新问题,我们将执行以下步骤

  • A和B活着并生根,B.A活着并通过B生根
  • A是活动的,B不是根目录,符合收集条件。B.a.无根且不合格
  • 收集发生了。B和B.a都是可最终确定的,因此它们被放在最终确定者队列中。B未收集,因为它是最终确定的。B.a未被收集,因为它是可最终确定的,并且因为它被B引用,而B尚未最终确定
  • B.a最终确定,或B最终确定
  • B.a或B中的另一个已确定
  • B.a和B可供收集
  • (如果B在第4点最终确定,则有可能在第5点之前收集,因为等待最终确定的B会阻止B和B.a收集,等待最终确定的B.a不会影响B的收集)

    所发生的事情是,4到5之间的订单是这样的,即B.a最终确定,然后B最终确定。由于WeakReference对对象的引用不是普通引用,因此它需要自己的清理代码来释放其GCHandle。显然,它不能依赖于正常的GC收集行为,因为它引用的全部要点是它们不遵循正常的GC收集行为


    现在B的Finalizer正在运行,但由于B.a的Finalizer的行为是释放其引用,它为IsAlive返回false(或者在1.1之前的.NET中,如果我记得版本正确,则抛出一个错误)。

    这确实有点奇怪,我不能说,我已经找到了答案,但这是我迄今为止的发现。根据您的示例,我在调用
    GC.Collect
    之前立即连接了WinDbg。此时,弱引用会像预期的那样保留实例

    接下来,我挖掘了
    WeakReference
    的实际实例,并在引用本身上设置了一个数据断点。从这一点开始,调试器在
    mscorwks处中断!WKS::FreeWeakHandle+0x12
    (将句柄设置为null),托管调用堆栈如下所示:

    OS Thread Id: 0xf54 (0)
    ESP       EIP     
    0045ed28 6eb182d3 [HelperMethodFrame: 0045ed28] System.GC.nativeCollectGeneration(Int32, Int32)
    0045ed80 00af0c62 System.GC.Collect()
    0045ed84 005e819d app.Program.Main(System.String[])
    0045efac 6eab1b5c [GCFrame: 0045efac] 
    

    这似乎表明,对
    GC.Collect
    的调用反过来也会修改弱引用。这可以解释观察到的行为,但我不能说它在所有情况下是否都会这样

    尽管
    WeakReference
    没有实现
    IDisposable
    ,但它确实使用了非托管资源(a
    GCHandle
    )。当一个
    WeakReference
    被放弃时,它必须确保在
    WeakReference
    本身被垃圾回收之前释放资源;如果没有,系统将无法知道不再需要
    GCHandle
    。为了解决这个问题,一个
    WeakReference
    在其
    Finalize
    方法中释放其
    GCHandle
    (从而使其自身无效)。如果这发生在尝试使用
    WeakReference
    Finalize
    方法执行之前,则后一种方法将无法获取
    WeakReference
    的前一个目标

    WeakReference
    的构造函数接受一个参数,该参数指示在其目标符合立即终止条件时(参数值
    false
    ),还是仅当其目标符合消除条件时(参数值
    true
    ),其目标是否应立即失效。我不确定该参数是否会导致
    WeakReference
    本身在一个GC周期内重新启动,但这可能是一种可能性

    否则,如果你是
    OS Thread Id: 0xf54 (0)
    ESP       EIP     
    0045ed28 6eb182d3 [HelperMethodFrame: 0045ed28] System.GC.nativeCollectGeneration(Int32, Int32)
    0045ed80 00af0c62 System.GC.Collect()
    0045ed84 005e819d app.Program.Main(System.String[])
    0045efac 6eab1b5c [GCFrame: 0045efac]