在我参与的一个项目中,WeakAction的使用非常广泛。该类允许保留对 Action 实例的引用,而不会导致其目标未被垃圾收集。它的工作方式很简单,它对构造函数执行操作,并且对操作的目标和方法保持弱引用,但放弃对操作本身的引用。当执行 Action 的时间到了时,它会检查目标是否仍处于 Activity 状态,如果是,则在目标上调用该方法。

除了一种情况- Action 在闭包中实例化之外,所有其他方法都运行良好。考虑以下示例:

public class A
{
     WeakAction action = null;

     private void _register(string msg)
     {
         action = new WeakAction(() =>
         {
             MessageBox.Show(msg);
         }
     }
}

由于lambda表达式使用的是msg局部变量,因此C#编译器会自动生成一个嵌套类来容纳所有闭包变量。该操作的目标是嵌套类的实例,而不是A的实例。一旦构造函数完成,传递给WeakAction构造函数的操作就不会被引用,因此垃圾回收器可以立即对其进行处理。稍后,如果执行了WeakAction,则即使A的原始实例仍处于 Activity 状态,也将因为目标不再存在而无法工作。

现在,我无法更改WeakAction的调用方式(因为它已被广泛使用),但是我可以更改其实现。我当时正在考虑尝试找到一种方法来访问A实例,并在A实例仍处于 Activity 状态时强制嵌套类的实例保持 Activity 状态,但是我不知道该怎么做。

关于A与任何事情有什么关系,存在很多问题,并且建议更改A创建弱 Action 的方式(我们不能这样做),因此在此进行澄清:
A类的实例希望某个B类的实例在发生某些情况时通知它,因此它使用Action对象提供了回调。 A不知道B使用弱 Action ,它只是提供Action用作回调。 B使用WeakAction的事实是未公开的实现细节。 B需要存储此操作,并在需要时使用它。但是B的生存时间可能比A更长,并且对常规Action的强引用(其本身对生成它的A实例的强引用)导致A永远不会被垃圾回收。如果A是不再存在的项目列表的一部分,则我们希望A被垃圾回收,并且由于B拥有Action的引用(该引用本身指向A),因此存在内存泄漏。

因此,B不会将A保留为B提供的 Action ,而是将其包装为WeakAction并仅存储弱 Action 。当需要调用它时,B仅在WeakAction仍然存在的情况下才这样做,只要A仍然存在,就应该这样做。
A在方法内部创建该 Action ,并且不会自己对其进行引用-这是给定的。由于Action是在A的特定实例的上下文中构造的,因此该实例是A的目标,并且当A死亡时,对其的所有弱引用都变为null,因此B知道不对其进行调用并处置WeakAction对象。

但是有时生成Action的方法使用该函数中本地定义的变量。在这种情况下,执行操作的上下文不仅包括A的实例,还包括方法内部的局部变量的状态(称为“关闭”)。 C#编译器通过创建一个隐藏的嵌套类来保存这些变量(将其称为A__closure)来做到这一点,成为Action的目标的实例是A__closure的实例,而不是A的实例。这是用户不应该知道的。除了此A__closure实例仅由Action对象引用。并且由于我们创建了对目标的弱引用,并且不持有对 Action 的引用,因此也没有对A__closure实例的引用,并且垃圾收集器可以(并且通常确实)立即处理它。因此A存活了下来,A__closure死亡了,尽管A仍然希望回调被调用,但是B无法做到这一点。

那是错误。

我的问题是,是否有人知道WeakAction构造函数(实际上唯一保存原始Action对象的唯一代码)可以某种神奇的方式从A中找到的A__closure实例中提取Target的原始实例。的Action。如果是这样,我也许可以延长A__Closure的生命周期以匹配A的生命周期。

最佳答案

经过更多研究,并从此处发布的答案中收集了所有有用的信息之后,我意识到,对于该问题将不会有一个优雅而密封的解决方案。由于这是一个现实生活中的问题,因此我们采用务实的方法,试图通过处理尽可能多的场景来至少减少这种情况,因此我想发表我们所做的事情。

对传递给WeakEvent的构造函数的Action对象(尤其是Action.Target属性)的更深入研究表明,实际上存在两种不同的闭包对象情况。

第一种情况是Lambda使用调用函数范围内的局部变量,但不使用A类实例中的任何信息时。在下面的示例中,假定EventAggregator.Register是采取操作并存储包装它的WeakAction的方法。

public class A
{
    public void Listen(int num)
    {
        EventAggregator.Register<SomeEvent>(_createListenAction(num));
    }

    public Action _createListenAction(int num)
    {
        return new Action(() =>
        {
            if (num > 10) MessageBox.Show("This is a large number");
        });
    }
}

此处创建的lambda使用num变量,它是在_createListenAction函数范围内定义的局部变量。因此,编译器必须使用闭包类对其进行包装,以维护闭包变量。但是,由于lambda不访问任何类A的成员,因此不需要存储对A的引用。因此,该操作的目标将不包括对A实例的任何引用,并且绝对没有WeakAction构造函数的方式。达到它。

下例说明了第二种情况:
public class A
{
   int _num = 10;

    public void Listen()
    {
        EventAggregator.Register<SomeEvent>(_createListenAction());
    }

    public Action _createListenAction()
    {
        return new Action(() =>
        {
            if (_num > 10) MessageBox.Show("This is a large number");
        });
    }
}

现在_num没有作为函数的参数提供,它来自A类实例。使用反射来了解Target对象的结构,可以发现编译器定义的最后一个字段包含对A类实例的引用。当lambda包含对成员方法的调用时,这种情况也适用,如以下示例所示:
public class A
{
    private void _privateMethod()
    {
       // do something here
    }

    public void Listen()
    {
        EventAggregator.Register<SomeEvent>(_createListenAction());
    }

    public Action _createListenAction()
    {
        return new Action(() =>
        {
             _privateMethod();
        });
    }
}
_privateMethod是成员函数,因此在A类实例的上下文中调用它,因此闭包必须保留对其的引用,以便在正确的上下文中调用lambda。

因此,第一种情况是Closure,它仅包含函数局部变量,第二种情况是对父A实例的引用。在这两种情况下,都没有对Closure实例的硬引用,因此,如果WeakAction构造函数只是按原样进行操作,则尽管A类实例仍然存在,但WeakAction会立即“死亡”。

我们在这里面临3个不同的问题:
  • 如何识别操作的目标是嵌套闭包
    类实例,而不是原始的A实例?
  • 如何获取对原始类A实例的引用?
  • 如何延长闭包实例的生命周期,使其生效
    只要A实例存在,但没有超出?

  • 第一个问题的答案是,我们依赖于闭包实例的3个特征:
    -它是私有(private)的(更准确地说,它不是“Visible”。当使用C#编译器时,反射类型将IsPrivate设置为true,但使用VB则不设置。在所有情况下,IsVisible属性为false)。
    -它是嵌套的。
    -正如@DarkFalcon在他的回答中提到的,它以[CompilerGenerated]属性进行修饰。
    private static bool _isClosure(Action a)
    {
        var typ = a.Target.GetType();
        var isInvisible = !typ.IsVisible;
        var isCompilerGenerated = Attribute.IsDefined(typ, typeof(CompilerGeneratedAttribute));
        var isNested = typ.IsNested && typ.MemberType == MemberTypes.NestedType;
    
    
        return isNested && isCompilerGenerated && isInvisible;
    }
    

    尽管这不是一个100%密封的谓词(恶意程序员可能会生成嵌套的私有(private)类,并使用CompilerGenerated属性对其进行修饰),但在现实生活中,这是足够准确的,而且,我们正在构建一种实用的解决方案,而不是学术性的一。

    这样就解决了问题1。弱 Action 构造函数确定 Action 目标是闭合的情况并对此进行响应。

    问题3也很容易解决。就像@usr在他的回答中所写的那样,一旦我们掌握了A类实例,然后添加一个带有单个条目的ConditionalWeakTable即可解决该问题,其中A类实例是键,而闭包实例是目标。只要A类实例存在,垃圾收集器就不会收集封闭实例。没关系。

    唯一不能解决的问题是第二个问题,如何获取对A类实例的引用? 正如我所说,有2个关闭的案例。一种是编译器创建一个保存该实例的成员,另一种则不是。在第二种情况下,根本没有办法获取它,因此我们唯一能做的就是创建对闭包实例的硬引用,以免立即被垃圾回收。这意味着它可能在类A实例中处于 Activity 状态(实际上,只要WeakAction实例存在,它就会存在(可能永远存在))。 但是毕竟不是一个糟糕的情况。在这种情况下,闭包类仅包含一些局部变量,在99.9%的情况下,它是一个非常小的结构。尽管这仍然是内存泄漏,但这并不是一个实质性的泄漏。

    但是,为了让用户甚至避免内存泄漏,我们现在向WeakAction类添加了一个额外的构造函数,如下所示:
    public WeakAction(object target, Action action) {...}
    

    当调用此构造函数时,我们添加一个ConditionalWeakTable条目,其中目标是键,而操作目标是值。我们对目标和行动目标的引用都很少,如果它们中的任何一个死亡,我们都将其清除。这样,行动目标的生命就不会少于提供的目标。从根本上讲,只要目标存在,WeakAction的用户就可以告诉它坚持关闭实例。因此,将告知新用户使用它以避免内存泄漏。但是在不使用此新构造函数的现有项目中,这至少可以最大程度地减少对不引用A类实例的闭包的内存泄漏。

    引用父级的闭包的情况更成问题,因为它们会影响garbase的收集。如果我们对闭包进行严格的引用,则会导致更严重的内存泄漏,因为A类实例也将永远不会被清除。但是这种情况也更容易治疗。由于编译器添加了最后一个成员,该成员拥有对类A实例的引用,因此我们仅使用反射来提取它,并完全执行用户在构造函数中提供它时的操作。当闭包实例的最后一个成员与闭包嵌套类的声明类型具有相同的类型时,我们确定这种情况。 (再次,它不是100%准确,但是对于现实生活中的情况,它足够接近)。

    总而言之,我在这里介绍的解决方案不是100%密封的解决方案,仅仅是因为似乎没有这样的解决方案。但是,由于我们必须对此烦人的错误提供一些答案,因此该解决方案至少可以大大减少问题。

    关于c# - 如果执行关闭操作,WeakAction中的错误,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/25730530/

    10-11 23:52