C# · 12月 22, 2021

c# – 在MVVM light Messenger中使用动作,局部变量和垃圾收集的奇怪行为

我在MVVM Light中的Messenger系统遇到了一个非常奇怪的问题.这很难解释,所以这里是一个小程序,演示了这个问题: using System;using GalaSoft.MvvmLight.Messaging;namespace TestApp{ class Program { static void Main(string[] args) { var prog = new Program(); var recipient = new object(); prog.RegisterMessageA(recipient); prog.RegisterMessageB(recipient); prog.SendMessage(“First Message”); GC.Collect(); prog.SendMessage(“Second Message”); } public void RegisterMessageA(object target) { Messenger.Default.Register(this,(Message msg) => { Console.WriteLine(msg.Name + ” recieved by A”); var x = target; }); } public void RegisterMessageB(object target) { Messenger.Default.Register(this,(Message msg) => { Console.WriteLine(msg.Name + ” received by B”); }); } public void SendMessage(string name) { Messenger.Default.Send(new Message { Name = name }); } class Message { public string Name { get; set; } } }}

如果您运行应用程序,这是控制台输出:

First Message recieved by AFirst Message received by BSecond Message received by B

正如你所看到的,第二个消息从来没有被收件人A收到.但是,B和A之间的唯一区别是一行:语句var x = target ;.如果删除该行,则A接收第二条消息.

另外,如果你删除GC.Collect();那么A接收第二个消息.然而,这只会隐藏这个问题,就像一个真正的程序一样,垃圾回收器最终会自动运行.

为什么会这样?我假设某种方式,如果收件人操作是从它的包含方法范围引用一个变量,它将该操作的生命周期与该范围相关联,以便一旦超出范围,它可以被垃圾回收.我不明白为什么这是一切.我也不明白为什么没有引用它们定义的范围的变量的动作没有这个问题.

谁能解释这里发生了什么?

解决方法 嗯,我现在明白为什么会发生这种情况(反之亦然).我以更短的形式复制了不使用lambda表达式的形式,然后我将解释为什么羊羔是重要的. using System;using GalaSoft.MvvmLight.Messaging;class Program{ static void Main(string[] args) { Receiver r1 = new Receiver(“r1”); Receiver r2 = new Receiver(“r2”); var recipient = new object(); Messenger.Default.Register<object>(recipient,r1).ShowMessage; Messenger.Default.Register<object>(recipient,r2).ShowMessage; GC.Collect(); Messenger.Default.Send(recipient,null); // Uncomment one of these to see the relevant message… // GC.KeepAlive(r1); // GC.KeepAlive(r2); }}class Receiver{ private string name; public Receiver(string name) { this.name = name; } public void ShowMessage(object message) { Console.WriteLine(“message received by {0}”,name); }}

基本上,信使只保留对消息处理程序的弱引用. (也是收件人,但这不是一个问题.)更具体地说,它似乎对处理程序的目标对象有一个弱的引用.它似乎不关心委托对象本身,但目标是重要的.所以在上面的代码中,当你保存一个Receiver
对象存活,仍然使用将该对象作为目标的委托.但是,当目标被允许被垃圾回收时,不使用该对象的处理程序.

现在我们来看看你的两个处理程序:

public void RegisterMessageA(object target){ Messenger.Default.Register(target,(Message msg) => { Console.WriteLine(msg.Name + ” received by A”); var x = target; });}

此lambda表达式捕获目标参数.为了捕获它,编译器生成一个新的类 – 所以RegisterMessageA是有效的:

public void RegisterMessageA(object target){ GeneratedClass x = new GeneratedClass(); x.target = target; Messenger.Default.Register(x.target,x.Method);}private class GeneratedClass{ public object target; public void Method(Message msg) { Console.WriteLine(msg.Name + ” received by A”); var x = target; }}

现在,除了保留GeneratedClass实例的委托之外,还没有其他的东西.比较你的第二个处理程序:

public void RegisterMessageB(object target){ Messenger.Default.Register(target,(Message msg) => { Console.WriteLine(msg.Name + ” received by B”); });}

在这里,没有捕获的变量,所以编译器生成的代码有点像这样:

public void RegisterMessageB(object target){ Messenger.Default.Register(target,RegisterMessageB_Lambda);}private static void RegisterMessageB_Lambda(Message msg){ Console.WriteLine(msg.Name + ” received by B”);}

这里是一个静态方法,所以没有任何委托目标.如果委托人捕获到这个,它将被生成为一个实例方法.但重要的是,没有必要生成一个额外的类…所以没有什么可以收集垃圾.

我没有仔细研究MvvmLight是如何做到这一点的 – 无论是对代理人的弱参考,CLR还是以某种特殊的方式来对待,还是MvvmLight是否将目标与委托本身分开.无论如何,我希望解释你所看到的行为.关于如何解决您使用真实代码看到的任何问题 – 基本上,您需要确保对所需的任何代理目标保持强有力的参考.

编辑:好的,它现在是由于WeakActionGeneric和它的基类WeakAction.我不知道这个行为是否是预期的行为(作者),但这是代码负责:)