WeakReference ведет себя по-разному в .Net Framework и .Net Core
Рассмотрим следующий код:
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] };
}
}
}
- С выпуском или сборкой отладки на.Net Framework 4.8 этот код печатает
False
. - В выпуске или отладочной сборке на.Net
Core 3.1 этот код печатает
True
.
Что вызывает эту разницу в поведении? (Это приводит к сбою некоторых наших модульных тестов.)
Примечание: я поместил инициализацию списка в makeList()
и отключил встраивание, пытаясь заставить версию.Net Core работать так же, как версию.Net Framework, но безрезультатно.
[РЕДАКТИРОВАТЬ] Как указал Ханс, добавление цикла исправляет это.
Следующий код будет печатать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);
Но это напечатает 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);
Это есть, чтобы быть какой - то странный джиттера вещи...
2 ответа
Просто потому, что что - то позволено быть собраны не означает, что она обязана быть собраны, как только возможно. Хотя в языке указано, что сборщику мусора разрешено определять, что локальная переменная больше не читается, и, следовательно, не считает ее корнем, это не означает, что вы можете полагаться на то, что содержимое локальной переменной собирается сразу после того, как вы в последний раз читали из нее..
Это не какое-то изменение между определенным поведением во время выполнения, это неопределенное поведение в обеих средах выполнения, поэтому различия между ними вполне приемлемы.
Я получил ссылку, которую нужно освободить, когда удалил переменную списка:
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);
}
}
}
Следующий тестовый пример не выполняется:
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);
}
}
}
Если мы посмотрим на IL, мы увидим, что null присваивается локальной переменной 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