c# - Extraño comportamiento con acciones, variables locales y recolección de basura en MVVM light Messenger
wpf garbage-collection (2)
Bueno, ahora entiendo por qué está sucediendo (creo, de todos modos). Lo he reproducido en una forma más corta que no usa expresiones lambda, y luego explicaré por qué las lambdas son importantes.
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);
}
}
Básicamente, el messenger solo mantiene una referencia débil al manejador de mensajes. (También para el destinatario, pero no es un problema aquí). Más específicamente, parece tener una referencia débil al objeto de destino del manejador. No parece importarle el objeto delegado en sí, pero el objetivo es importante. Entonces, en el código anterior, cuando mantiene vivo un objeto Receiver
, el delegado que tiene ese objeto como objetivo todavía se usa. Sin embargo, cuando se permite que el destino sea basura, no se utiliza el controlador que usa ese objeto.
Ahora veamos a tu dos manejadores:
public void RegisterMessageA(object target)
{
Messenger.Default.Register(target, (Message msg) =>
{
Console.WriteLine(msg.Name + " received by A");
var x = target;
});
}
Esta expresión lambda captura el parámetro target
. Para capturarlo, el compilador genera una nueva clase, por lo que RegisterMessageA
es efectivamente:
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;
}
}
Ahora, no hay nada más que el delegado que mantiene viva la instancia de GeneratedClass
. Compare eso con su segundo manejador:
public void RegisterMessageB(object target)
{
Messenger.Default.Register(target, (Message msg) =>
{
Console.WriteLine(msg.Name + " received by B");
});
}
Aquí, no hay variables capturadas, por lo que el compilador genera un código como este:
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");
}
Aquí se trata de un método estático , por lo que no hay objetivo delegado en absoluto. Si el delegado capturó this
, se generaría como un método de instancia. Pero el punto importante es que no hay necesidad de generar una clase extra ... así que no hay nada que recolectar.
No he estudiado exactamente cómo MvvmLight está haciendo esto, si solo tiene una referencia débil al delegado, y si el CLR está tratando eso de alguna manera especial, o si MvvmLight está separando el objetivo del delegado. De cualquier manera, espero que eso explique el comportamiento que estás viendo. En términos de cómo solucionar cualquier problema que estés viendo con código real , básicamente necesitarás asegurarte de mantener una fuerte referencia al objetivo delegado que necesites.
EDITAR: Bien, parece que ahora se debe a WeakActionGeneric
y su clase base WeakAction
. No sé si este comportamiento es el comportamiento esperado (por el autor), pero ese es el código responsable :)
Tengo un problema realmente extraño con el sistema Messenger
en MVVM Light. Es difícil de explicar, así que aquí hay un pequeño programa que demuestra el problema:
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; }
}
}
}
Si ejecuta la aplicación, esta es la salida de la consola:
First Message recieved by A
First Message received by B
Second Message received by B
Como puede ver, el destinatario nunca recibe el segundo mensaje. Sin embargo, la única diferencia entre B y A es una línea: la instrucción var x = target;
. Si elimina esta línea, A
recibe el segundo mensaje.
Además, si elimina GC.Collect();
entonces A
recibe el segundo mensaje. Sin embargo, esto solo oculta el problema, ya que en un programa real, el recolector de basura se ejecutará eventualmente.
¿Por qué está pasando esto? Supongo que, de alguna manera, si la acción del destinatario se refiere a una variable que contiene el alcance del método, vincula el tiempo de vida de la acción con ese alcance para que, una vez fuera del alcance, pueda ser basura recolectada. No entiendo por qué es esto en absoluto. Tampoco entiendo por qué las acciones que no hacen referencia a variables del ámbito en el que están definidas no tienen este problema.
¿Alguien puede explicar lo que está pasando aquí?
Estoy de acuerdo, el comportamiento de este programa es realmente extraño.
Lo intenté yo mismo y, como ya entendiste, el problema está relacionado de alguna manera con esta línea:
var x = target;
No tengo idea de por qué esta línea causa problemas, pero podría considerar esta solución alternativa:
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 es una clase del MVVM Light Framework que ofrece la posibilidad de recuperar el objetivo del mensaje en sí.
Pero no estoy seguro de si esto es lo que intentas lograr ...