.NET事件在被调用者超出范围时回调:安全做法

本文关键字:回调 安全 范围 事件 被调用者 NET | 更新日期: 2023-09-27 18:26:32

给定以下内容:

void Method1 {
    Foo _foo = new Foo();
    _foo.DataReady += ProcessData();
    _foo.StartProcessingData();
}
void ProcessData() { //do something }

StartProcessingData()是一个长时间运行的方法,它最终(和异步)引发DataReady事件。

现在,_foo曾经是一个类级变量,事件曾经在构造函数中连接。

然而,内存评测强调了这将如何将_foo及其所有从属项永远保留在内存中,因此我对上面的内容进行了更改。

我的问题是:总检察长会毁掉事情吗?Method1很快结束(当然是在事件触发之前),这意味着_foo不再是。然而,这是否意味着(因为_foo保留了其事件的引用)ProcessData()永远不会触发?或者,事件的存在是否足以使_foo在方法结束后保持活动状态,是否足以确保ProcessData激发?还是没有结论?

[在测试中,它运行得很好-ProcessData总是被调用。即使使StartProcessingData花费很长时间,并在中途强制GC收集(使用RedGate的内存档案器)也没有删除它。但我想确定一下!]

澄清:StartProcessingData()立即返回。Foo对象可能类似于:

class Foo
{
SomeSerice _service;
event EventHandler<EventArgs> DataReady;
Foo()
{
_service = new SomeService();
_service.ServiceCallCompleted += _service_ServiceCallCompleted;
}
void StartProcessingData()
{
_service.ServiceCallAsync();
}
void _service_ServiceCallCompleted
{
DataReady(null,e);
}

因此,它抽象和模拟了一个长时间运行的异步服务,使用事件来发出重要的,呃,事件。


以下是一个完整的工作示例(控制台应用程序)

class Program
        {
            static void Main(string[] args)
            {
                Class1 _class1 = new Class1();
                Console.WriteLine("Disposing of Class 1");
                _class1 = null;
                GC.Collect();
                System.Threading.Thread.Sleep(15000);
                Console.Read();
            }
        }
        internal class Class1
        {
            internal Class1()
            {
                Foo _foo = new Foo();
                _foo.DataReady += new EventHandler<EventArgs>(_foo_DataReady);
                _foo.StartProcessingData();
            }
            void _foo_DataReady(object sender, EventArgs e)
            {
                Console.WriteLine("Class 1 Processing Data");
            }
        }
        class Foo
        {
            internal event EventHandler<EventArgs> DataReady = delegate { };
            internal void StartProcessingData()
            {
                System.Threading.Timer _timer = new System.Threading.Timer(OnTimer);
                Console.WriteLine("Firing event in 10 secs");
                _timer.Change(10000, System.Threading.Timeout.Infinite);
            }
            private void OnTimer(object state)
            {
                DataReady(this, null);
            }
    }

如果你运行它,你会得到:

Firing event in 10 secs
Disposing of Class 1
Class 1 Processing Data

.NET事件在被调用者超出范围时回调:安全做法

让我们假设StartProcessingData()是完全同步的(即不涉及线程)。直到事件触发后,它才会返回,并且ProcessData()将从_foo.StartProcessingData()内的调用。如果您想验证这一点,请在ProcessData()中放置一个断点并查看调用堆栈。

因此,也就是说,当触发事件并调用处理程序时,_foo不会超出范围,因为Method1()尚未返回。

现在,如果涉及线程,则意味着在另一个线程中执行的代码必须持有对_foo的引用;否则,事件就不可能被启动。因此,_foo仍然不是垃圾收集的候选者。因此,在任何一种情况下,您都不必担心_foo会被垃圾收集。

(编辑)

挂接_serviceServiceCallCompleted事件现在意味着_service保留对_foo的引用,防止其被垃圾收集。

对象Foo可以订阅对象Bar中的事件,然后放弃对Bar的所有引用,而在大多数情况下不会影响Bar激发事件的能力,因为Bar的事件可能是由某个线程或外部对象触发的,只有当线程或外部对象有对它的引用时,才会发生这种情况。有两个原因可能是Foo保持对Bar的引用很重要;两者都不可能适用于你的情况。

  1. 如果"Foo"没有提及"Bar",如果它后来发现自己不需要"Bar"的服务,它可能无法让"Bar"知道。它可以尝试使用稍后某个事件回调的"Sender"参数来取消订阅,但这至少有两个问题:(1)有时一个对象会代表另一个对象引发事件,在这种情况下,"Sender"属性可能无法识别持有事件订阅的对象;(2) 事件可能永远不会被引发,但对订阅服务器的引用可能会阻止订阅服务器有资格进行垃圾收集,直到发布服务器有资格为止。
  2. 在系统中的任何地方,对"Bar"的唯一其他引用可能是"WeakReference"类型,因此,如果"Foo"不保留对"Bar'"的强引用,系统可能会使所有这些弱引用无效,从而有效地导致"Bar"消失。

就我个人而言,我认为对象在放弃订阅之前取消所有订阅是个好主意。人们可能会认为,在放弃订阅服务器时,事件发布服务器将超出范围,但如果事件保持连接,任何保持发布服务器活动的东西都将无用地保持被放弃的订阅服务器活动。如果这些被放弃的订阅者中的任何一个也是事件发布者,那么他们的被放弃订阅者反过来可能会被无用地保留,等等。