WeakReference se comporte différemment entre .Net Framework et .Net Core
Considérez le code suivant:
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
#nullable enable
namespace ConsoleApp1
{
class Program
{
static void Main()
{
var list = makeList();
var weakRef = new WeakReference(list[0]);
list[0] = null;
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine(weakRef.IsAlive);
}
[MethodImpl(MethodImplOptions.NoInlining)]
static List<int[]?> makeList()
{
return new List<int[]?> { new int[2] };
}
}
}
- Avec une version ou une version de débogage sur .Net Framework 4.8, ce code s'imprime
False. - Avec une version ou une version de débogage sur .Net Core 3.1, ce code s'imprime
True.
Qu'est-ce qui cause cette différence de comportement? (Cela provoque l'échec de certains de nos tests unitaires.)
Remarque: J'ai mis l'initialisation de la liste dans makeList()et désactivé l'inlining pour tenter de faire fonctionner la version .Net Core de la même manière que la version .Net Framework, mais en vain.
[EDIT] Comme Hans l'a souligné, l'ajout d'une boucle le corrige.
Le code suivant va imprimer False:
var list = makeList();
var weakRef = new WeakReference(list[0]);
list[0] = null;
for (int i = 0; i < 1; ++i)
GC.Collect();
Console.WriteLine(weakRef.IsAlive);
Mais ceci imprimera True:
var list = makeList();
var weakRef = new WeakReference(list[0]);
list[0] = null;
GC.Collect();
GC.Collect();
GC.Collect();
GC.Collect();
// Doesn't seem to matter how many GC.Collect() calls you do.
Console.WriteLine(weakRef.IsAlive);
Cela a obtenu d'être une sorte de chose bizarre ... Jitter
Réponses
Ce n'est pas parce que quelque chose est autorisé à être collecté qu'il est obligé d'être collecté dès que possible. Bien que le langage indique que le GC est autorisé à déterminer qu'une variable locale n'est plus jamais lue, et donc à ne pas la considérer comme une racine, cela ne signifie pas que vous pouvez compter sur le contenu d'une variable locale collectée immédiatement après votre dernière lecture. .
Ce n'est pas un changement entre le comportement défini dans le runtime, c'est un comportement non défini dans les deux runtimes, donc les différences entre eux sont tout à fait acceptables.
J'ai obtenu la référence à libérer, lorsque j'ai supprimé la variable de liste:
using NUnit.Framework;
using System;
using System.Collections.Generic;
namespace NUnitTestProject1
{
public class Tests
{
[TestCase(2, GCCollectionMode.Forced, true)]
public void TestWeakReferenceWithList(int generation, GCCollectionMode forced, bool blocking)
{
static WeakReference CreateWeakReference()
{
return new WeakReference(new List<int[]> { new int[2] });
}
var x = CreateWeakReference();
Assert.IsTrue(x.IsAlive);
GC.Collect(generation, forced, blocking);
Assert.IsFalse(x.IsAlive);
}
}
}
Le scénario de test suivant échoue:
using NUnit.Framework;
using System;
using System.Collections.Generic;
namespace NUnitTestProject1
{
public class Tests
{
[TestCase(2, GCCollectionMode.Forced, true)]
public void TestWeakReferenceWithList(int generation, GCCollectionMode forced, bool blocking)
{
static List<int[]> CreateList()
{
return new List<int[]> { new int[2] };
}
WeakReference x;
{
var list = CreateList();
x = new WeakReference(list);
list = null;
}
Assert.IsTrue(x.IsAlive);
GC.Collect(generation, forced, blocking);
Assert.IsFalse(x.IsAlive);
}
}
}
Si nous regardons l'IL, nous pouvons voir que null est assigné à la variable locale 1:
IL_0003: call class [System.Collections]System.Collections.Generic.List`1<int32[]> NUnitTestProject1.Tests::'<TestWeakReferenceWithList>g__CreateList|0_0'()
IL_0008: stloc.1
IL_0009: ldloc.1
IL_000a: newobj instance void [System.Runtime]System.WeakReference::.ctor(object)
IL_000f: stloc.0
IL_0010: ldnull
IL_0011: stloc.1
IL_0012: nop