[TestFixture]
public class Tests
{
private class Relay
{
public Action Do { get; set; }
}
[Test]
public void OptimizerStrangeness()
{
var relay = new Relay();
var indicator = 0;
relay.Do = () => indicator++;
var weak = new WeakReference(relay);
GC.Collect();
var relayNew = weak.Target as Relay;
if (relayNew == null) Assert.Fail();
relayNew.Do();
Assert.AreEqual(1, indicator);
}
}
Run Code Online (Sandbox Code Playgroud)
Assert.Fail()尽管事实relay变量仍在范围内,但此代码仅在行中的Release模式下失败,因此我们仍然具有对该实例的强引用,因此WeakReference必须尚未死亡.
UPD:澄清一点:我意识到它可以"优化掉".但是根据这个优化indicator变量会有0或1有价值,即我们有实际可见的行为变化.
UPD2:来自C#语言规范,第3.9节
如果对象或其任何部分无法通过任何可能的继续执行来访问,除了运行析构函数之外,该对象被认为不再使用,并且它有资格进行销毁.C#编译器和垃圾收集器可以选择分析代码以确定将来可以使用对对象的哪些引用.例如,如果范围内的局部变量是对象的唯一现有引用,但该过程中当前执行点的任何可能的继续执行中从不引用该局部变量,则垃圾收集器可能(但是不要求将对象视为不再使用.
从技术上讲,这个对象可以并且将通过继续执行来访问,因此不能被视为"不再使用"(实际上C#规范没有提及弱引用,因为它是CLR的一部分而不是编译器 - 编译器输出很好).将尝试搜索有关CLR/JIT的内存管理信息.
UPD3:以下是CLR内存管理的一些信息 - "释放内存"部分:
...每个应用程序都有一组根.每个根指向托管堆上的对象或设置为null.应用程序的根包括全局和静态对象指针,线程堆栈上的局部变量和引用对象参数以及CPU寄存器.垃圾收集器可以访问实时(JIT)编译器和运行时维护的活动根列表.使用此列表,它会检查应用程序的根,并在此过程中创建一个图形,其中包含可从根目录访问的所有对象.
有问题的变量肯定是局部变量,因此它是可达的.如上所述,这一提及非常快速/模糊,所以我很高兴看到更具体的信息.
UPD4:来自.NET Framework的来源:
// This method DOES NOT DO ANYTHING in and of itself. It's used to
// prevent a finalizable object from losing any outstanding references
// a touch too early. The JIT is very aggressive about keeping an
// object's lifetime to as small a window as possible, to the point
// where a 'this' pointer isn't considered live in an instance method
// unless you read a value from the instance. So for finalizable
// objects that store a handle or pointer and provide a finalizer that
// cleans them up, this can cause subtle ----s with the finalizer
// thread. This isn't just about handles - it can happen with just
// about any finalizable resource.
//
// Users should insert a call to this method near the end of a
// method where they must keep an object alive for the duration of that
// method, up until this method is called. Here is an example:
//
// "...all you really need is one object with a Finalize method, and a
// second object with a Close/Dispose/Done method. Such as the following
// contrived example:
//
// class Foo {
// Stream stream = ...;
// protected void Finalize() { stream.Close(); }
// void Problem() { stream.MethodThatSpansGCs(); }
// static void Main() { new Foo().Problem(); }
// }
//
//
// In this code, Foo will be finalized in the middle of
// stream.MethodThatSpansGCs, thus closing a stream still in use."
//
// If we insert a call to GC.KeepAlive(this) at the end of Problem(), then
// Foo doesn't get finalized and the stream says open.
[System.Security.SecuritySafeCritical] // auto-generated
[ResourceExposure(ResourceScope.None)]
[MethodImplAttribute(MethodImplOptions.InternalCall)]
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
public static extern void KeepAlive(Object obj);
Run Code Online (Sandbox Code Playgroud)
如果您有兴趣,请参阅此处了解我的调查详情.
即使变量在范围内,如果在将来的任何代码路径中不再访问它,运行时也可以自由地收集它.这就是为什么在调试具有JIT优化的程序集时,可以为您提供有关变量值被优化的消息,即使它当前在范围内.
请参阅3.9自动内存管理的第 2项.
再具体一点,
例如,如果范围内的局部变量是对象的唯一现有引用,但该过程中当前执行点的任何可能的继续执行中从不引用该局部变量,则垃圾收集器可能(但是不要求将对象视为不再使用.
这是至关重要的一点; 该对象被认为是不可访问的,因为对该对象(只有一个)的所有强引用都是不可访问的.请记住,C#规范将包含有关语言的信息以及有关如何执行已编译代码的信息.还要记住,范围不能定义可达性.正如规范所述,如果编译器和运行时可以确定变量在任何未来的代码路径中都不存在(意味着它们根本没有被引用或仅在被确定为不可到达的路径中被引用,例如if(false)),那么变量是被认为无法到达,并不算作强有力的参考.
虽然规范的特定部分没有WeakReference明确说明,但它并不需要.就编译器而言,只有一个局部变量指向该值.
WeakReference只是另一个将对象作为参数的类; 从编译器的角度来看; 它没有理由相信(或以某种方式做出假设)关于该类是否持有它所通过的引用.考虑一下我是否有类似这样的课程:
public class MyClass
{
public MyClass(object foo)
{
Console.WriteLine(foo);
}
}
Run Code Online (Sandbox Code Playgroud)
在我的代码中我做到了这一点:
var relay = new Relay();
...
var myClass = new MyClass(relay);
Run Code Online (Sandbox Code Playgroud)
我没有对我赋予的值引入任何新的强引用relay,因为MyClass没有坚持该引用.事实上,WeakReference就编译器而言,"特殊"类旨在为您提供对不被视为强引用的对象的引用,这一事实无关紧要.
可达性不是由范围定义的; 它是由所讨论的变量(非值)是否存在于任何可能的未来代码路径中来定义的.由于relay函数后面没有以任何形式出现,变量(以及它对对象的引用)被认为是不可达的并且有资格收集.这就是DisableOptimizations标志存在于程序集级别的原因,以便运行时知道(除其他事项外)等待变量超出范围之后才有资格进行收集,以便调试器可以访问它.