Странное поведение с действиями, локальными переменными и сборкой мусора в MVVM light Messenger
У меня действительно странная проблема с системой Messenger
в свете MVVM. Это трудно объяснить, поэтому вот небольшая программа, которая демонстрирует проблему:
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 A
First Message received by B
Second Message received by B
Как вы можете видеть, второе сообщение никогда не принимается получателем A. Однако единственное различие между B и A-это одна строка: утверждение var x = target;
. Если удалить эту строку, A
получит второе сообщение.
Также, если вы удалите GC.Collect();
затем A
получает второе сообщение. Однако это только скрывает проблему, так как в реальной программе сборщик мусора в конечном итоге автоматически запустится.
Почему это происходит? Я предполагаю, что каким-то образом, если действие получателя ссылается на переменную из содержащей его области метода, оно связывает время жизни действия с этой областью, так что после выхода из области оно может быть собрано в мусор. Я вообще не понимаю, почему это происходит. Я также не понимаю, почему действия, которые не ссылаются переменные из области, в которой они определены, не имеют этой проблемы.
Может ли кто-нибудь объяснить, что здесь происходит?2 ответа:
Что ж, теперь я понимаю, почему это происходит (во всяком случае, я верю). Я воспроизвел его в более короткой форме, которая не использует лямбда-выражения, а затем объясню, почему лямбды важны.
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); } }
В основном, мессенджер сохраняет толькослабую ссылку на обработчик сообщений. (Тоже адресату, но здесь это не проблема.) Более конкретно, он, по-видимому, имеет слабую ссылку на объект обработчика target. Кажется, ему все равно. о самом объекте делегата, но цель важна. Поэтому в приведенном выше коде, когда вы держите
Теперь давайте посмотрим на два ваших обработчика:Receiver
объект живой, делегат, который имеет этот объект в качестве цели, все еще используется. Однако, когда целевой объект разрешается собирать мусор, обработчик, использующий этот объект, не используется.public void RegisterMessageA(object target) { Messenger.Default.Register(target, (Message msg) => { Console.WriteLine(msg.Name + " received by A"); var x = target; }); }
Это лямбда-выражение захватывает параметр
target
. Чтобы захватить его, компилятор генерирует новый класс-так что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"); }
Здесь этостатический метод, поэтому нет цели делегата вообще. Если делегат захватил
this
, он будет создан как метод экземпляра. Но важно то, что в этом нет необходимости. создайте дополнительный класс... так что нечего собирать мусор.Я не изучал точно, как MvvmLight делает это - является ли это просто слабой ссылкой на делегат, и что CLR обрабатывает это каким-то особым образом, или же MvvmLight отделяет цель от самого делегата. В любом случае, я надеюсь, что это объясняет поведение, которое вы видите. С точки зрения того, как исправить любую проблему, которую вы видите с реальным кодом-в основном вам нужно будет убедиться, что вы держите сильную ссылку на любую цель делегата, которая вам нужна.
EDIT: Хорошо, похоже, что это теперь из-за
WeakActionGeneric
и его базовый классWeakAction
. Я не знаю, является ли это поведение ожидаемым поведением (автором), но это код, ответственный :)
Я согласен, поведение этой программы действительно странно.
Я попробовал сам и, как вы уже поняли, проблема каким-то образом связана с этой строкой:
var x = target;
Я понятия не имею, почему эта строка вызывает какие-либо проблемы, но вы можете рассмотреть этот обходной путь:
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(target, (Message msg) => { Console.WriteLine(msg.Name + " received by A"); var x = msg.Target; }); } public void RegisterMessageB(object target) { Messenger.Default.Register(target, (Message msg) => { Console.WriteLine(msg.Name + " received by B"); }); } public void SendMessage(string name) { Messenger.Default.Send(new Message { Name = name }); } class Message : MessageBase //part of the MVVM Light framework { public string Name { get; set; } } }
MessageBase-это класс из MVVM Light Framework, который предлагает возможность извлечения цели из самого сообщения.
Но я не уверен, что это то, что вы пытаетесь сделать. достигать...