c# - type - ¿Darse de baja del delegado pasado a través de la palabra clave ref al método de suscripción?
ref c# (6)
Editar : Ok, esa fue una mala idea, así que volvamos a lo básico:
Recomiendo crear una clase contenedora sobre una Acción:
class ActionWrapper
{
public Action<string> Action;
}
Y reestructurando su clase inicial para trabajar con wrappers:
private ActionWrapper localSource;
public void Subscribe(ActionWrapper source)
{
source.Action += Broadcast;
localSource = source;
}
public void Dispose()
{
localSource.Action -= Broadcast;
}
Ahora deberías obtener los resultados deseados.
Tengo la siguiente clase:
public class Terminal : IDisposable
{
readonly List<IListener> _listeners;
public Terminal(IEnumerable<IListener> listeners)
{
_listeners = new List<IListener>(listeners);
}
public void Subscribe(ref Action<string> source)
{
source += Broadcast;
//Store the reference somehow?
}
void Broadcast(string message)
{
foreach (var listener in _listeners) listener.Listen(message);
}
public void Dispose()
{
//Unsubscribe from all the stored sources?
}
}
He buscado durante un tiempo y parece que un argumento pasado con la palabra clave ref no se puede almacenar. Intentar agregar el argumento de origen a una lista o asignarlo a una variable de campo no le permite mantener una referencia a la referencia original del delegado real; entonces mis preguntas son:
- ¿Hay alguna manera de darse de baja de todas las fuentes sin pasar sus referencias nuevamente?
- De no ser así, ¿cómo se puede cambiar la clase para soportarla, pero aún mantener la suscripción pasando a un delegado a través de un método?
- ¿Es posible lograrlo sin usar Reflection?
- ¿Es posible lograrlo sin envolver el delegado / evento en una clase y luego pasar la clase como un parámetro para la suscripción?
Gracias.
EDITAR: Parece que sin usar un Wrapper o Reflection, no hay solución para el problema dado. Mi intención era hacer que la clase fuera lo más portátil posible, sin tener que envolver delegados en clases de ayuda. Gracias a todos por las contribuciones.
Es razonablemente simple, pero hay algunas trampas. Si almacena una referencia a los objetos fuente, como la mayoría de los ejemplos hasta ahora han propuesto, el objeto no será basura . La mejor manera de evitar esto es usar una WeakReference, que permitirá que el GC funcione correctamente.
Entonces, todo lo que tienes que hacer es esto:
1) Agregue una lista de fuentes a la clase:
private readonly List<WeakReference> _sources = new List<WeakReference>();
2) Agregue la fuente a la lista:
public void Subscribe(ref Action<string> source)
{
source += Broadcast;
//Store the reference
_sources.Add(new WeakReference(source));
}
3) Y luego simplemente implementar dispose:
public void Dispose()
{
foreach (var r in _sources)
{
var source = (Action<string>) r.Target;
if (source != null)
{
source -= Broadcast;
source = null;
}
}
_sources.Clear();
}
Dicho esto, también está la cuestión de por qué la Acción debe aprobarse como una ref. En el código actual, no hay razón para eso. De todos modos, no afecta el problema o la solución.
Sugeriría que el método de suscripción devuelva una implementación de una clase SubscriptionHelper, que implementa IDisposable. Una implementación simple sería que SubscriptionHelper tuviera una referencia a la lista de suscripción y una copia del delegado de suscripción; la lista de suscripción en sí sería una Lista <SubscriptionHelper>, y el método Dispose para SubscriptionHelper se eliminaría de la lista. Tenga en cuenta que si el mismo delegado se suscribe varias veces, cada suscripción devolverá un SubscriptionHelper diferente; llamando a Dispose en una suscripciónHelper cancelará la suscripción para la que se devolvió.
Este enfoque sería mucho más limpio que el método Delegate.Combine / Delegate.Remove utilizado por el patrón normal de .NET, cuya semántica puede ser muy extraña si se intenta suscribir y cancelar la suscripción de delegados de múltiples destinatarios.
EDITAR:
Sí, es malo: los delegados son tipos inmutables, por lo que agregar un método a una lista de invocación realmente creará una nueva instancia de delegado.
Lo que lleva a una respuesta no a su pregunta. Para darse de baja del delegado, debe eliminar su método de Broadcast
de la lista de invocación del delegado. Esto significa crear un nuevo delegado y asignarlo al campo o variable original. Pero no puedes acceder al original una vez que estás fuera del método de Subscribe
. Además, puede haber otras copias de ese campo / variable original que tengan su método en la lista de invocación. Y no hay manera de que usted sepa sobre todos ellos y cambie sus valores.
Sugeriría declarar una interfaz con el evento para su propósito. Este será un enfoque bastante flexible.
public interface IMessageSource
{
event Action<string> OnMessage;
}
public class MessageSource : IMessageSource
{
public event Action<string> OnMessage;
public void Send(string m)
{
if (OnMessage!= null) OnMessage(m);
}
}
public class Terminal : IDisposable
{
private IList<IMessageSource> sources = new List<IMessageSource>();
public void Subscribe(IMessageSource source)
{
source.OnMessage += Broadcast;
sources.Add(source);
}
void Broadcast(string message)
{
Console.WriteLine(message);
}
public void Dispose()
{
foreach (var s in sources) s.OnMessage -= Broadcast;
}
}
Respuesta original
¿Hay alguna razón en particular por la que pasa el delegado de source
como ref
? Lo necesita si, por ejemplo, desea devolver un delegado diferente del método.
De lo contrario, el delegado es un tipo de referencia, por lo que puede suscribirse sin pasarlo como ref
...
public class Terminal : IDisposable
{
List<IListener> _listeners;
List<Action<string>> _sources;
public Terminal(IEnumerable<IListener> listeners)
{
_listeners = new List<IListener>(listeners);
_sources = new List<Action<string>>();
}
public void Subscribe(ref Action<string> source)
{
_sources.Add( source );
source += Broadcast;
}
void Broadcast(string message)
{
foreach (var listener in _listeners) listener.Listen(message);
}
public void Dispose()
{
foreach ( var s in _sources ) s -= Broadcast;
}
}
Quizás, en lugar de tratar de almacenar una referencia al delegado, tenga las llamadas Suscribir que utilizan su referencia al objeto con el delegado para crear acciones para la suscripción y desuscripción. Es un parámetro adicional, pero sigue siendo sencillo.
public void Subscribe(Action<Action<string>> addHandler,Action<Action<string>> removeHandler)
{
//Prevent error for possibly being null in closure
Action<string> onEvent = delegate { };
//Broadcast when the event occurs, unlisten after (you could store onEvent and remove handler yourself)
onEvent = (s) => { Broadcast(s); removeHandler(onEvent); };
addHandler(onEvent);
}
Y un ejemplo de suscripción.
public event Action<string> CallOccured;
public void Program()
{
Subscribe(a => CallOccured += a, a => CallOccured -= a);
CallOccured("Hello");
}