WeakReference se comporta de maneira diferente entre .Net Framework e .Net Core
Considere o seguinte código:
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] };
}
}
}
- Com um lançamento ou uma versão de depuração no .Net Framework 4.8, esse código é impresso
False. - Com uma versão ou uma versão de depuração no .Net Core 3.1, esse código é impresso
True.
O que está causando essa diferença de comportamento? (Está fazendo com que alguns de nossos testes de unidade falhem.)
Nota: Eu coloquei a inicialização da lista makeList()e desliguei o inlining em uma tentativa de fazer a versão .Net Core funcionar da mesma forma que a versão .Net Framework, mas sem sucesso.
[EDIT] Como Hans apontou, adicionar um loop corrige o problema.
O seguinte código será impresso 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);
Mas isso vai imprimir 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);
Isto tem que ser algum tipo de coisa Jitter estranho ...
Respostas
Só porque algo pode ser coletado, não significa que seja obrigado a ser coletado o mais rápido possível. Embora a linguagem declare que o GC tem permissão para determinar que uma variável local nunca é lida novamente e, portanto, não a considera uma raiz, isso não significa que você pode contar com o conteúdo de uma variável local sendo coletado imediatamente após sua última leitura .
Isso não é uma mudança entre o comportamento definido no tempo de execução, é um comportamento indefinido em ambos os tempos de execução, portanto, as diferenças entre eles são inteiramente aceitáveis.
Eu tenho a referência a ser liberada, quando removi a variável de lista:
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);
}
}
}
O seguinte caso de teste falha:
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);
}
}
}
Se olharmos para o IL, podemos ver que null é atribuído à variável local 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