The key problem in this is that you are accessing a reference field during a finalizer. The underlying problem is that the WeakReference
itself has (or can be, unpredictably) already been collected (since collection order is non-deterministic). Simply: the WeakReference
no longer exists, and you are query IsValid
/ Target
etc on a ghost object.
So accessing this object at all is unreliable and brittle. Finalizers should only talk to direct value-type state - handles, etc. Any reference (unless you know it will always out-live the object being destroyed) should be treated with distrust and avoided.
If instead, we pass in the WeakReference
and ensure that the WeakReference
is not collected, then everything works fine; the following should show one success (the one where we've passed in the WeakReference
), and one fail (where we've created the WeakReference
just for this object, thus it is eligible for collection at the same time as the object):
using System;
class Program
{
static void Main(string[] args)
{
A a = new A();
CreateB(a);
WeakReference weakRef = new WeakReference(a);
CreateB(weakRef);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.KeepAlive(a);
GC.KeepAlive(weakRef);
Console.ReadKey();
}
private static void CreateB(A a)
{
B b = new B(a);
}
private static void CreateB(WeakReference a)
{
B b = new B(a);
}
}
class A
{ }
class B
{
private WeakReference a;
public B(WeakReference a)
{
this.a = a;
}
public B(A a)
{
this.a = new WeakReference(a);
}
~B()
{
Console.WriteLine("a.IsAlive: " + a.IsAlive);
Console.WriteLine("a.Target: " + a.Target);
}
}
What makes you say it isn't collected? It looks eligible.... no field on a live object holds it, and the variable is never read past that point (and indeed that variable may well have been optimised away by the compiler, so no "local" in the IL).
You might need a GC.KeepAlive(a)
at the bottom of Main
to stop it.