Testing/Verifying a WeakReference

前端 未结 3 1940
逝去的感伤
逝去的感伤 2021-02-12 22:29

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

3条回答
  •  借酒劲吻你
    2021-02-12 23:08

    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 WeakReferences? 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 func, bool useGetHashCode = false)
    {
        var foo = func();
        var result = new Tuple(new WeakReference(foo), new ManualResetEvent(false), useGetHashCode ? (foo?.GetHashCode() ?? 0) : RuntimeHelpers.GetHashCode(foo));
    
        Task.Factory.StartNew(() =>
        {
            result.Item2.WaitOne();
            GC.KeepAlive(foo);  // need this here to make sure it doesn't get GC-ed ahead of time
            foo = null;
        });
    
        return result;
    }
    
    
    

    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 Strings. 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("").ToString() 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!");
    }
    

    提交回复
    热议问题