I\'d like to verify that code setting up a WeakReference does not accidentally hold a strong reference to the referenced object. (Here\'s an example of how it is easy to acciden
Unit tests involving WeakReference
objects are trickier than you might expect. As you and others have noted, GC.Collect()
can presumably "force" a garbage collection, but that still depends upon your object having no references to it.
Unfortunately, how you build your code can change whether objects still have references to them. More specifically, whether you are building in Debug or Release mode can and will change when objects are still rooted (more accurately, in depends on whether you have optimizations turned on; Debug defaults to having them off, while Release defaults to having them on). Debug mode turns off a lot of optimizations, and it even has a tendency to root objects that were created/declared in the method that is currently being executed. So, your unit tests may fail in Debug builds, but succeed in Release builds.
In your example, even though you set testObj
to NULL, the compiler is trying to be helpful in a Debug build by keeping its previous value rooted. That means that no matter how many times you call GC.Collect()
, wr.IsAlive
will always return TRUE.
So, how the heck can you test WeakReference
s? Simple: create them AND the objects they are based off of in another method. As long as that method doesn't get in-lined, and for the most part, it won't, the compiler won't root the object you care about, and you can have your tests pass in both Debug and Release builds.
The function below gives you a hint as to how to do this:
public static Tuple GetKillableWr(Func
Using this, as long as you create your object inside the func
parameter, you can create a WeakReference
to an object of your choosing that won't be rooted after you signal the returned ManualResetEvent
and call GC.Collect()
. As others have noted, it can be helpful to call the below code to ensure cleanup happens as you need it...
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
EDIT:
There are some other "gotcha's" to worry about. A common one involves String
s. String
literals and constants are always rooted, because they are compiled as a reference into your DLL/EXE. So, something like new WeakReference("foo")
will always show as being alive, because "foo" has been stored into your DLL and a reference to that stored literal is provided in the compiled code. An easy way around this is to use new StringBuilder("
instead of the string literal.
EDIT AGAIN:
Another "gotcha" is that in Release builds, optimizations cause the GC to be more aggressive, which, unlike the above scenarios, might cause objects to go out of scope sooner than you expect. In the code below, wr.IsAlive
can sometimes return FALSE, because the GC has detected that myObject
will not be used by anything else in the method, so it made it eligible for garbage collection. The way around this is to put GC.KeepAlive(myObject)
at the end of your method. That will keep myObject
rooted until at least that line is executed.
public static void SomeTest()
{
var myObject = new object();
var wr = new WeakReference(myObject);
GC.Collect();
Assert.True(wr.IsAlive, "This could fail in Release Mode!");
}