Cancelar inscrição de método anônimo em C #

222

É possível cancelar a inscrição de um método anônimo de um evento?

Se eu me inscrever em um evento como este:

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

MyEvent += MyMethod;

Posso cancelar a assinatura assim:

MyEvent -= MyMethod;

Mas se eu me inscrever usando um método anônimo:

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

é possível cancelar a inscrição desse método anônimo? Se sim, como?

Eric
fonte
4
Quanto ao motivo pelo qual você não pode fazer isso: stackoverflow.com/a/25564492/23354
Marc Gravell

Respostas:

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

MyEvent += myDelegate;


// .... later

MyEvent -= myDelegate;

Basta manter uma referência ao delegado.

Jacob Krall
fonte
141

Uma técnica é declarar uma variável para armazenar o método anônimo, que estaria disponível no próprio método anônimo. Isso funcionou para mim porque o comportamento desejado era cancelar a inscrição após o tratamento do evento.

Exemplo:

MyEventHandler foo = null;
foo = delegate(object s, MyEventArgs ev)
    {
        Console.WriteLine("I did it!");
        MyEvent -= foo;
    };
MyEvent += foo;
J c
fonte
1
Usando esse tipo de código, o Resharper reclama do acesso a um fechamento modificado ... essa abordagem é confiável? Quero dizer, temos certeza de que a variável 'foo' dentro do corpo do método anônimo realmente faz referência ao próprio método anônimo?
BladeWise
7
Encontrei uma resposta para minha dúvida, e é que 'foo' manterá verdadeiramente uma referência ao método anônimo. A variável capturada é modificada, pois é capturada antes que o método anônimo seja atribuído a ela.
BladeWise
2
Era exatamente disso que eu precisava! Estava faltando o = null. (MyEventHandler foo = delegado {... MyEvent- = foo;}; MyEvent + = foo; não funcionou ...)
TDaver
O Resharper 6.1 não reclama se você o declarar como uma matriz. Parece um pouco estranho, mas vou confiar cegamente em minhas ferramentas nesta: MyEventHandler [] foo = {null}; foo [0] = ... {... MyEvent - = foo [0]; }; MyEvent + = foo [0];
Mike Post
21

De memória, a especificação explicitamente não garante o comportamento de qualquer maneira quando se trata da equivalência de delegados criados com métodos anônimos.

Se precisar cancelar a inscrição, você deve usar um método "normal" ou reter o delegado em outro lugar para poder cancelar a inscrição exatamente com o mesmo delegado usado para assinar.

Jon Skeet
fonte
Eu Jon, o que você meen? Eu não entendo a solução exposta por "J c" não funcionará corretamente?
Eric Ouellet
@ EricOuellet: Essa resposta é basicamente uma implementação de "reter o delegado em outro lugar para que você possa cancelar a inscrição exatamente com o mesmo delegado que usou para assinar".
precisa
Jon, desculpe, eu li sua resposta várias vezes tentando descobrir o que você quer dizer e onde a solução "J c" não usa o mesmo delegado para se inscrever e cancelar a inscrição, mas não consigo descobrir. Talvez você possa me indicar um artigo que explique o que você está dizendo? Conheço sua reputação e gostaria muito de entender o que você quer dizer. Qualquer coisa com a qual você possa vincular seria realmente apreciada.
Eric Ouellet
1
Encontrei: msdn.microsoft.com/en-us/library/ms366768.aspx, mas eles recomendam não usar o anônimo, mas não dizem que há algum problema grave?
Eric Ouellet
Eu encontrei ... Muito obrigado (consulte a resposta de Michael Blome): social.msdn.microsoft.com/Forums/en-US/csharplanguage/thread/…
Eric Ouellet
16

Na versão 3.0 pode ser reduzido para:

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

fonte
15

Como o recurso de funções locais do C # 7.0 foi lançado, a abordagem sugerida por J c se torna realmente interessante.

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

Portanto, honestamente, você não tem uma função anônima como variável aqui. Mas suponho que a motivação para usá-lo no seu caso possa ser aplicada a funções locais.

Mazharenko
fonte
1
Para tornar a legibilidade ainda melhor, você pode mover o MyEvent + = foo; linha a ser antes da declaração de foo.
Mark Zhukovsky
9

Em vez de manter uma referência a qualquer delegado, você pode instrumentar sua classe para devolver a lista de chamadas do evento ao chamador. Basicamente, você pode escrever algo assim (assumindo que MyEvent seja declarado dentro de MyClass):

public class MyClass 
{
  public event EventHandler MyEvent;

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

Assim, você pode acessar toda a lista de chamadas de fora do MyClass e cancelar a inscrição de qualquer manipulador que desejar. Por exemplo:

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

Eu escrevi um post completo sobre esta técnica aqui .

hemme
fonte
2
Isso significa que eu poderia cancelar acidentalmente uma instância diferente (ou seja, não eu) do evento se eles se inscreveram depois de mim?
dumbledad
@dumbledad é claro que isso sempre cancelaria o registro do último registrado. Se você deseja cancelar dinamicamente a inscrição de um delegado anônimo específico, é necessário identificá-lo de alguma forma. Eu sugiro mantendo uma referência então :)
LuckyLikey
É muito legal o que você está fazendo, mas não consigo imaginar um caso em que isso possa ser útil. Mas eu realmente não resolvo a questão do OP. -> +1. IMHO, simplesmente não se deve usar delegados anônimos se eles devem ser cancelados o registro mais tarde. Mantê-los é estúpido -> use melhor o Método. A remoção de apenas alguns delegados na lista de Invocação é bastante aleatória e inútil. Corrija-me se eu estiver errado. :)
LuckyLikey
6

Tipo de abordagem manca:

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

  ...

  public event Action OnDoSomething
  {
    add {
      _eventList.Add(value);
    }
    remove {
      _eventList.Remove(value);
    }
  }
}
  1. Substitua os métodos de adição / remoção de eventos.
  2. Mantenha uma lista desses manipuladores de eventos.
  3. Quando necessário, limpe todos e adicione novamente os outros.

Isso pode não funcionar ou ser o método mais eficiente, mas deve fazer o trabalho.

casademora
fonte
14
Se você acha que é ruim, não poste.
Jerry Nixon
2

Se você quiser controlar o cancelamento da inscrição, precisará seguir a rota indicada na sua resposta aceita. No entanto, se você estiver apenas preocupado em limpar as referências quando sua classe de assinante ficar fora do escopo, haverá outra solução (um pouco complicada) que envolve o uso de referências fracas. Acabei de postar uma pergunta e resposta sobre este tópico.

Benjol
fonte
2

Uma solução simples:

basta passar a variável eventhandle como parâmetro para si mesma. Evento, se você tiver o caso de não conseguir acessar a variável criada original devido ao multithreading, poderá usar este:

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
fonte
e se MyEvent for chamado duas vezes antes de MyEvent -= myEventHandlerInstance;ser executado? Se for possível, você terá um erro. Mas não tenho certeza se é esse o caso.
precisa saber é o seguinte
0

se você quiser se referir a algum objeto com esse delegado, pode ser que você possa usar Delegate.CreateDelegate (Type, Object target, MethodInfo methodInfo) .net considere o delegado igual a target e methodInfo

user3217549
fonte
0

Se a melhor maneira é manter uma referência no eventHandler inscrito, isso pode ser obtido usando um Dicionário.

Neste exemplo, eu tenho que usar um método anônimo para incluir o parâmetro mergeColumn para um conjunto de DataGridViews.

O uso do método MergeColumn com o parâmetro enable definido como true habilita o evento enquanto o usa com false o desativa.

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
fonte