1

I discovered something I did not expect in the behavior of generic WeakReference.

In my test, the instance of WeakReference are freed after a GC.Collect() call but not for generic WeakReference:

Unit test case

using NUnit.Framework;
using System;

namespace NUnitTestProject1
{
    public class Tests
    {
        private const bool trackResurrection = false;

        [SetUp]
        public void Setup()
        {
        }

        [TestCase(2, GCCollectionMode.Default, true)]
        public void TestWeakReferenceWithObject(int generation, GCCollectionMode forced, bool blocking)
        {
            static WeakReference CreateWeakReference()
            {
                return new WeakReference(new object(), trackResurrection);
            }

            var x = CreateWeakReference();

            Assert.IsTrue(x.IsAlive);

            GC.Collect(generation, forced, blocking);

            Assert.IsFalse(x.IsAlive);
        }

        [TestCase(2, GCCollectionMode.Forced, true)]
        public void TestWeakReferenceWithString(int generation, GCCollectionMode forced, bool blocking)
        {
            static WeakReference CreateWeakReference()
            {
                return new WeakReference(new string('a', 100), trackResurrection);
            }

            var x = CreateWeakReference();

            Assert.IsTrue(x.IsAlive);

            GC.Collect(generation, forced, blocking);

            Assert.IsFalse(x.IsAlive);
        }

        [TestCase(2, GCCollectionMode.Forced, true)]
        public void TestGenericWeakReferenceWithObject(int generation, GCCollectionMode forced, bool blocking)
        {
            static WeakReference<object> CreateWeakReference()
            {
                return new WeakReference<object>(new object(), trackResurrection);
            }

            var x = CreateWeakReference();

            Assert.IsTrue(x.TryGetTarget(out var _));

            GC.Collect(generation, forced, blocking);

            Assert.IsFalse(x.TryGetTarget(out var _));
        }

        [TestCase(2, GCCollectionMode.Forced, true)]
        public void TestGenericWeakReferenceWithString(int generation, GCCollectionMode forced, bool blocking)
        {
            static WeakReference<string> CreateWeakReference()
            {
                return new WeakReference<string>(new string('a', 100), trackResurrection);
            }

            var x = CreateWeakReference();

            Assert.IsTrue(x.TryGetTarget(out var _));

            GC.Collect(generation, forced, blocking);

            Assert.IsFalse(x.TryGetTarget(out var _));
        }
    }
}

I have tried the documentation:

And the .NET Framework reference source code:

But I can't find a reason to the different behave?

Test parameters

Passable parameters:

[TestCase(0, GCCollectionMode.Default, true)]
[TestCase(1, GCCollectionMode.Default, true)]
[TestCase(2, GCCollectionMode.Default, true)]
[TestCase(0, GCCollectionMode.Forced, true)]
[TestCase(1, GCCollectionMode.Forced, true)]
[TestCase(2, GCCollectionMode.Forced, true)]

Not passable parameters:

//[TestCase(0, GCCollectionMode.Optimized, true)]
//[TestCase(1, GCCollectionMode.Optimized, true)]
//[TestCase(2, GCCollectionMode.Optimized, true)]
  • 1
    There's no guarantee that any particular reference will be collected immediately after calling `GC.Collect()` so the runtime can behave differently for different types. See [my related question and its answer here](https://stackoverflow.com/questions/63491193/weakreference-behaves-differently-between-net-framework-and-net-core) – Matthew Watson Aug 30 '20 at 08:29
  • 2
    Why do you think that the fact that one test uses generics has more relevance that the fact that one test uses `x.IsAlive` and the other `x.TryGetTarget(out var _)`? – Holger Sep 01 '20 at 15:04
  • Thank you @Holger you pushed me in the right direction. – Andreas Synnerdahl Sep 03 '20 at 00:09

1 Answers1

1

I got my generic weak reference test cases to pass, just by commenting out the first assert:

// Assert.IsTrue(x.TryGetTarget(out var _));

Or by moving the call to WeakReference<T>.TryGetTarget(out T target) it's own function:

using NUnit.Framework;
using System;

namespace NUnitTestProject1
{
    public class Tests
    {
        private const bool trackResurrection = false;

        [SetUp]
        public void Setup()
        {
        }
       
        [TestCase(2, GCCollectionMode.Forced, true)]
        public void TestGenericWeakReferenceWithObject(int generation, GCCollectionMode forced, bool blocking)
        {
            static WeakReference<object> CreateWeakReference()
            {
                return new WeakReference<object>(new object(), trackResurrection);
            }

            static bool IsAlive(WeakReference<object> weakReference)
            {
                return weakReference.TryGetTarget(out var _);
            }

            var x = CreateWeakReference();

            Assert.IsTrue(IsAlive(x));            

            GC.Collect(generation, forced, blocking);

            Assert.IsFalse(IsAlive(x));
        }

        [TestCase(2, GCCollectionMode.Forced, true)]
        public void TestGenericWeakReferenceWithString(int generation, GCCollectionMode forced, bool blocking)
        {
            static WeakReference<string> CreateWeakReference()
            {
                return new WeakReference<string>(new string('a', 100), trackResurrection);
            }

            static bool IsAlive(WeakReference<string> weakReference)
            {
                return weakReference.TryGetTarget(out var _);
            }

            var x = CreateWeakReference();

            Assert.IsTrue(IsAlive(x));

            GC.Collect(generation, forced, blocking);

            Assert.IsFalse(IsAlive(x));
        }
    }
}

The discard variable in my call: x.WeakReference(out var _) must have keept a reference to the target of the weak reference. Adding a variable scope did not affect the test case, it still failed:

{
    x.WeakReference(out var _)
}