Um evento permite que uma classe anuncie que algo aconteceu sem saber quem está ouvindo. Outros objetos se inscrevem com +=, e quando a classe dispara o evento, o método de cada inscrito executa.
Saída:
Order A-17 saved
receipt emailed for A-17
Order A-18 saved
receipt emailed for A-18
free gift added to A-18
Shop não sabe que recibos ou brindes existem. Ele dispara OrderPlaced, e quem se inscreveu reage. Adicionar uma nova reação (pontos de fidelidade, analytics) significa adicionar um inscrito, não editar PlaceOrder.
Declarando um evento
A declaração de um evento é um membro com tipo delegate e a palavra-chave event:
public event EventHandler<OrderPlacedEventArgs> OrderPlaced;
O tipo delegate define a assinatura do handler. Código .NET quase sempre usa um de dois tipos embutidos, e seguir a convenção deixa seus eventos familiares para qualquer outro desenvolvedor .NET:
EventHandler:void (object sender, EventArgs e), para eventos que não carregam dados. Dispare-o comEventArgs.Empty.EventHandler<TEventArgs>:void (object sender, TEventArgs e), em queTEventArgsé a sua classe com os dados.
As convenções em volta deles:
senderé o objeto que disparou o evento, normalmentethis.- A classe de dados se chama
{Something}EventArgs, deriva deEventArgs(opcional desde o .NET 4.5, mas ainda costumeiro) e expõe propriedades somente leitura. - O evento recebe o nome do que aconteceu:
OrderPlaced,Closed,PriceChanged. Um nome terminado em...ing(Closing) é usado para eventos disparados antes da ação, muitas vezes com uma forma de cancelá-la. - Em classes feitas para herança, o disparo passa por um método
protected virtual void OnOrderPlaced(OrderPlacedEventArgs e), para que as classes derivadas possam se encaixar.
Inscrevendo e cancelando a inscrição
+= adiciona um handler e -= o remove. Um handler pode ser um método ou uma lambda. Para remover uma lambda depois, guarde-a em uma variável, porque escrever a mesma lambda de novo cria um delegate diferente que o -= não vai encontrar:
Saída:
set to 32
display shows 32 C
alarm: too hot
set to 35
display shows 35 C
set to 20
display shows 20 C
Remover um handler que nunca foi adicionado não é um erro; não faz nada. EventHandler<int> mostra que o argumento de tipo não precisa derivar de EventArgs no .NET moderno, embora uma classe dedicada deixe espaço para adicionar campos depois sem quebrar os inscritos.
Os handlers executam de forma síncrona, na ordem de inscrição, na thread que disparou o evento. Se um handler lança uma exceção, os handlers restantes não executam e a exceção se propaga para o código que disparou o evento.
Disparando um evento com segurança
Um evento sem inscritos guarda null. Chamá-lo diretamente lançaria NullReferenceException, então dispare-o com o operador condicional nulo:
OrderPlaced?.Invoke(this, args);
Antes do C# 6, o padrão era copiar o campo para uma variável local, verificá-la e invocar a cópia. A cópia importa em código com várias threads: verificar OrderPlaced != null e depois chamar OrderPlaced(...) lê o campo duas vezes, e outra thread poderia remover o último handler nesse meio tempo. ?. o lê uma vez, então é mais curto e correto.
Por que event e não um campo delegate público
Sem a palavra-chave event, um campo delegate público funciona para inscrições, mas entrega controle total a cada inscrito:
Saída:
analytics ping
analytics ping
Um = digitado no lugar de += removeu sem aviso os dois handlers anteriores, e o código de fora pôde disparar o "clique" sem clique nenhum. Marcar o campo como event transforma as duas linhas em erros de compilação:
error CS0070: The event 'Button.Clicked' can only appear on the left hand side of += or -= (except when used from within the type 'Button')
Dentro de Button, o evento continua se comportando como um campo delegate normal, então a classe pode invocá-lo e verificar se é null.
O vazamento da inscrição esquecida
Quando você se inscreve, o evento guarda um delegate, e o delegate mantém uma referência para o objeto inscrito. Enquanto o publicador estiver vivo e o handler estiver conectado, o inscrito não pode ser coletado pelo garbage collector, e continua recebendo eventos depois que você terminou de usá-lo:
Saída:
closed widget shows 101.5
open widget shows 101.5
subscribers left: 1
closed widget shows 99.0
Definir leaky como null não fez nada pelo feed: a lista de handlers dele ainda referencia aquele widget, então o widget "fechado" continua sendo atualizado e fica na memória. O widget do bloco using cancelou a inscrição no Dispose e parou de receber. Esse é um dos vazamentos de memória mais comuns em código .NET de desktop e de servidor, especialmente com eventos static e serviços da aplicação inteira, que vivem até o processo terminar.
A regra: quem se inscreve em um evento de um objeto que vive mais que ele precisa cancelar a inscrição, normalmente no Dispose (veja a instrução using). Quando o publicador e o inscrito têm o mesmo tempo de vida, como um formulário e os próprios botões, não é preciso cancelar.
Acessores add e remove próprios
Um evento pode definir o que += e -= fazem, como o get e o set de uma propriedade. É raro, mas é assim que frameworks guardam handlers em um dicionário compartilhado ou os repassam para outro objeto:
private EventHandler closed;
public event EventHandler Closed
{
add { Console.WriteLine("subscriber added"); closed += value; }
remove { Console.WriteLine("subscriber removed"); closed -= value; }
}
Com acessores próprios, a classe dispara o evento pelo campo de apoio (closed?.Invoke(this, EventArgs.Empty)), já que o evento em si não tem mais armazenamento.
Perguntas frequentes
O que é um evento em C#?
Um evento é um membro de classe que permite que outros objetos peçam para ser avisados quando algo acontece. Ele é apoiado por um delegate multicast, mas o código de fora só pode adicionar handlers (+=) e removê-los (-=). Só a classe que declara o evento pode dispará-lo.
Qual a diferença entre um evento e um delegate em C#?
Um delegate é um tipo que referencia métodos. Um evento é um membro declarado com um tipo delegate mais a palavra-chave event, que restringe o acesso: de fora da classe você não pode invocá-lo, ler a lista de handlers nem atribuí-lo com =, só se inscrever e cancelar a inscrição. Um campo delegate público permite tudo isso, então qualquer inscrito poderia apagar os outros ou disparar o evento.
Como passar dados com um evento em C#?
Crie uma classe derivada de EventArgs com os dados como propriedades somente leitura (public class OrderPlacedEventArgs : EventArgs { public decimal Total { get; } }) e declare o evento como EventHandler<OrderPlacedEventArgs>. Dispare-o com OrderPlaced?.Invoke(this, new OrderPlacedEventArgs(total));.
Como disparar um evento com segurança em C#?
Use MyEvent?.Invoke(this, args);. Um evento sem inscritos é null, então chamá-lo diretamente lança NullReferenceException. O operador ?. lê o campo uma vez, o que também evita uma condição de corrida em que outra thread remove o último handler entre a verificação de null e a chamada.
Eventos em C# podem causar vazamento de memória?
Sim. Inscrever-se guarda um delegate que referencia o inscrito, então o publicador mantém o inscrito vivo enquanto o próprio publicador viver. Um publicador de vida longa (um evento static, um serviço da aplicação inteira) com inscritos de vida curta que nunca cancelam a inscrição mantém todos eles na memória. Cancele a inscrição com -= quando o inscrito terminar, normalmente no Dispose.