Dar de baja el método anónimo en C #

222

¿Es posible cancelar la suscripción a un método anónimo de un evento?

Si me suscribo a un evento como este:

void MyMethod()
{
    Console.WriteLine("I did it!");
}

MyEvent += MyMethod;

Puedo cancelar la suscripción de esta manera:

MyEvent -= MyMethod;

Pero si me suscribo usando un método anónimo:

MyEvent += delegate(){Console.WriteLine("I did it!");};

¿Es posible darse de baja de este método anónimo? ¿Si es así, cómo?

Eric
fuente
44
En cuanto a por qué no puede hacer esto: stackoverflow.com/a/25564492/23354
Marc Gravell

Respuestas:

230
Action myDelegate = delegate(){Console.WriteLine("I did it!");};

MyEvent += myDelegate;


// .... later

MyEvent -= myDelegate;

Solo mantenga una referencia al delegado.

Jacob Krall
fuente
141

Una técnica es declarar una variable para contener el método anónimo que luego estaría disponible dentro del propio método anónimo. Esto funcionó para mí porque el comportamiento deseado era darse de baja después de que se manejó el evento.

Ejemplo:

MyEventHandler foo = null;
foo = delegate(object s, MyEventArgs ev)
    {
        Console.WriteLine("I did it!");
        MyEvent -= foo;
    };
MyEvent += foo;
J c
fuente
1
Usando este tipo de código, Resharper se queja de acceder a un cierre modificado ... ¿Es confiable este enfoque? Quiero decir, ¿estamos seguros de que la variable 'foo' dentro del cuerpo del método anónimo, realmente hace referencia al método anónimo en sí mismo?
BladeWise
77
Encontré una respuesta a mi dubt, y es que 'foo' realmente tendrá una referencia al método anónimo itslef. La variable capturada se modifica, ya que se captura antes de que se le asigne el método anónimo.
BladeWise
2
Eso es exactamente lo que necesitaba! Me faltaba el = nulo. (MyEventHandler foo = delegado {... MyEvent- = foo;}; MyEvent + = foo; no funcionó ...)
TDaver
Resharper 6.1 no se queja si lo declaras como una matriz. Parece un poco extraño, pero voy a confiar ciegamente en mis herramientas en este caso: MyEventHandler [] foo = {null}; foo [0] = ... {... MiEvento - = foo [0]; }; MyEvent + = foo [0];
Mike Post
21

De memoria, la especificación explícitamente no garantiza el comportamiento de ninguna manera cuando se trata de equivalencia de delegados creados con métodos anónimos.

Si necesita darse de baja, debe usar un método "normal" o retener al delegado en otro lugar para que pueda darse de baja con exactamente el mismo delegado que solía suscribirse.

Jon Skeet
fuente
Yo Jon, ¿qué quieres decir? No entiendo. Cómo funciona la solución expuesta por "J c"?
Eric Ouellet
@EricOuellet: Esa respuesta es básicamente una implementación de "retener al delegado en otro lugar para que pueda darse de baja exactamente con el mismo delegado que solía suscribirse".
Jon Skeet
Jon, lo siento, leí tu respuesta muchas veces tratando de entender a qué te refieres y dónde la solución "J c" no usa el mismo delegado para suscribirse y cancelar la suscripción, pero no puedo encontrarlo. ¿Quizás me puedas señalar un artículo que explique lo que estás diciendo? Conozco tu reputación y realmente me gustaría entender a qué te refieres, cualquier cosa que puedas vincular sería realmente apreciada.
Eric Ouellet
1
Encontré: msdn.microsoft.com/en-us/library/ms366768.aspx pero recomiendan no usar anónimo pero no dicen que hay algún problema importante.
Eric Ouellet
Lo encontré ... Muchas gracias (ver la respuesta de Michael Blome): social.msdn.microsoft.com/Forums/en-US/csharplanguage/thread/…
Eric Ouellet
16

En 3.0 se puede acortar a:

MyHandler myDelegate = ()=>Console.WriteLine("I did it!");
MyEvent += myDelegate;
...
MyEvent -= myDelegate;

fuente
15

Desde que se lanzó la función de funciones locales C # 7.0 , el enfoque sugerido por J c se vuelve realmente claro.

void foo(object s, MyEventArgs ev)
{
    Console.WriteLine("I did it!");
    MyEvent -= foo;
};
MyEvent += foo;

Entonces, honestamente, aquí no tienes una función anónima como variable. Pero supongo que la motivación para usarlo en su caso se puede aplicar a las funciones locales.

mazharenko
fuente
1
Para mejorar aún más la legibilidad, puede mover MyEvent + = foo; línea para estar antes de la declaración de foo.
Mark Zhukovsky
9

En lugar de mantener una referencia a cualquier delegado, puede instrumentar su clase para devolver la lista de invocación del evento a la persona que llama. Básicamente, puede escribir algo como esto (suponiendo que MyEvent se declare dentro de MyClass):

public class MyClass 
{
  public event EventHandler MyEvent;

  public IEnumerable<EventHandler> GetMyEventHandlers()  
  {  
      return from d in MyEvent.GetInvocationList()  
             select (EventHandler)d;  
  }  
}

Por lo tanto, puede acceder a toda la lista de invocación desde fuera de MyClass y cancelar la suscripción de cualquier controlador que desee. Por ejemplo:

myClass.MyEvent -= myClass.GetMyEventHandlers().Last();

He escrito una publicación completa sobre esta técnica aquí .

hemme
fuente
2
¿Significa esto que podría cancelar accidentalmente una instancia diferente (es decir, no yo) del evento si se suscribieron después de mí?
Dumbledad
@dumbledad, por supuesto, esto siempre anularía el registro del último registrado. Si desea darse de baja dinámicamente de un delegado anónimo específico, debe identificarlo de alguna manera. Sugeriría mantener una referencia entonces :)
LuckyLikey
Es genial lo que estás haciendo, pero no puedo imaginar un caso en el que esto pueda ser útil. Pero realmente no resuelve la pregunta del OP. -> +1. En mi humilde opinión, uno simplemente no debe utilizar delegados anónimos si deben ser dados de baja más tarde. Mantenerlos es estúpido -> Mejor método de uso. Eliminar solo un delegado en la lista de Invocación es bastante aleatorio e inútil. Corrígeme si me equivoco. :)
LuckyLikey
6

Enfoque poco convincente:

public class SomeClass
{
  private readonly IList<Action> _eventList = new List<Action>();

  ...

  public event Action OnDoSomething
  {
    add {
      _eventList.Add(value);
    }
    remove {
      _eventList.Remove(value);
    }
  }
}
  1. Anular los métodos de agregar / eliminar eventos.
  2. Mantenga una lista de esos controladores de eventos.
  3. Cuando sea necesario, elimínelos todos y vuelva a agregar los demás.

Esto puede no funcionar o ser el método más eficiente, pero debe hacer el trabajo.

casademora
fuente
14
Si crees que es cojo, no lo publiques.
Jerry Nixon
2

Si desea controlar la cancelación de la suscripción, debe seguir la ruta indicada en su respuesta aceptada. Sin embargo, si solo le preocupa limpiar las referencias cuando su clase de suscripción queda fuera de alcance, entonces hay otra solución (ligeramente complicada) que implica el uso de referencias débiles. Acabo de publicar una pregunta y respuesta sobre este tema.

Benjol
fuente
2

Una solución simple:

simplemente pase la variable eventhandle como parámetro a sí misma. Evento si tiene el caso de que no puede acceder a la variable creada original debido a subprocesos múltiples, puede usar esto:

MyEventHandler foo = null;
foo = (s, ev, mehi) => MyMethod(s, ev, foo);
MyEvent += foo;

void MyMethod(object s, MyEventArgs ev, MyEventHandler myEventHandlerInstance)
{
    MyEvent -= myEventHandlerInstance;
    Console.WriteLine("I did it!");
}
Manuel Marhold
fuente
¿Qué pasa si MyEvent se invoca dos veces antes de MyEvent -= myEventHandlerInstance;ejecutarse? Si es posible, tendría un error. Pero no estoy seguro si ese es el caso.
LuckyLikey
0

si desea referirse a algún objeto con este delegado, puede usar Delegate.CreateDelegate (Type, Object target, MethodInfo methodInfo) .net considere que el delegado es igual por target y methodInfo

usuario3217549
fuente
0

Si la mejor manera es mantener una referencia en eventHandler suscrito, esto se puede lograr usando un Diccionario.

En este ejemplo, tengo que usar un método anónimo para incluir el parámetro mergeColumn para un conjunto de DataGridViews.

El uso del método MergeColumn con el parámetro enable establecido en true habilita el evento mientras que el uso con false lo deshabilita.

static Dictionary<DataGridView, PaintEventHandler> subscriptions = new Dictionary<DataGridView, PaintEventHandler>();

public static void MergeColumns(this DataGridView dg, bool enable, params ColumnGroup[] mergedColumns) {

    if(enable) {
        subscriptions[dg] = (s, e) => Dg_Paint(s, e, mergedColumns);
        dg.Paint += subscriptions[dg];
    }
    else {
        if(subscriptions.ContainsKey(dg)) {
            dg.Paint -= subscriptions[dg];
            subscriptions.Remove(dg);
        }
    }
}
Larry
fuente