C# Eventos
Um evento é o jeito de um objeto avisar "aconteceu alguma coisa" para quem quiser saber, sem conhecer essas pessoas de antemão. O estoque da lojinha da Ana avisa quando um produto está acabando; um botão avisa que foi clicado; um download avisa que terminou. Por baixo, um evento é um delegate multicast (com += e -=) com uma proteção a mais: só o dono pode disparar.
- O que é um evento e quem são o publicador e os inscritos.
- Declarar com
event, inscrever com+=, sair com-=e disparar com?.Invoke. - Por que usar
evente não um delegate público (com a demonstração do estrago). - O padrão do .NET:
EventHandler<T>, osender, uma classe deEventArgse o métodoOnEvento. - A armadilha de tentar tirar uma lambda com
-=. - Os erros reais:
CS0070,CS0123, o avisoCS8618e aNullReferenceException.
Todos os programas rodaram com o .NET 10 (C# 14) na máquina da Ana, na pasta ~/cs, com dotnet run arquivo.cs. Você vai precisar de classes e das aulas de Delegates, Lambdas e Func e Action.
A ideia: o aviso de "chegou o produto"
Na lojinha, vários clientes perguntam pelo mesmo caderno, que está em falta. A Ana não vai ligar para a fábrica de hora em hora. Ela anota o telefone de cada interessado numa lista de aviso, e quando o caderno chega, liga para todos da lista. Quem desistir pede para sair da lista.
| Na lojinha | Em C# |
|---|---|
| A lista de aviso do caderno | O evento: public event Action<string>? EstoqueBaixo; |
| Quem tem a lista e liga para todos: a Ana | O publicador: a classe que declara e dispara o evento |
| Os clientes interessados | Os inscritos (subscribers): métodos ou lambdas |
| Pôr o nome na lista / pedir para sair | += / -= |
| Ligar para todos | Disparar o evento: EstoqueBaixo?.Invoke(produto) |
O ponto importante: a Ana não precisa saber quem está na lista nem o que cada um vai fazer com o aviso. O estoque só avisa; cada inscrito decide sua reação.
1. O primeiro evento
Estoque cadernos = new("Caderno", 5);
cadernos.EstoqueBaixo += AvisarAna;
cadernos.EstoqueBaixo += produto => Console.WriteLine($"[compras] Pedir mais {produto}");
cadernos.Vender(2);
cadernos.Vender(2);
cadernos.Vender(1);
static void AvisarAna(string produto) => Console.WriteLine($"[Ana] Está acabando: {produto}");
class Estoque(string produto, int quantidade)
{
public event Action<string>? EstoqueBaixo;
public void Vender(int qtd)
{
quantidade -= qtd;
Console.WriteLine($"Vendeu {qtd} {produto}, sobrou {quantidade}");
if (quantidade <= 2)
{
EstoqueBaixo?.Invoke(produto);
}
}
}
$ dotnet run estoque.cs Vendeu 2 Caderno, sobrou 3 Vendeu 2 Caderno, sobrou 1 [Ana] Está acabando: Caderno [compras] Pedir mais Caderno Vendeu 1 Caderno, sobrou 0 [Ana] Está acabando: Caderno [compras] Pedir mais Caderno
Primeiro, o lado do publicador (a classe Estoque):
- class Estoque(string produto, int quantidade)
- Uma classe com construtor primário C# 12: os parâmetros entre parênteses ficam disponíveis na classe inteira (veja Construtores).
new("Caderno", 5)cria um estoque de 5 cadernos. - public event
- Declara um evento. É público, para que outros possam se inscrever.
- Action<string>
- O tipo delegate do evento: diz o formato dos inscritos. Aqui, métodos que recebem um
string(o nome do produto) e não devolvem nada (veja Func, Action e Predicate). - ?
- O evento começa
null: ninguém se inscreveu ainda. O?diz isso ao compilador (veja Referências Anuláveis). - EstoqueBaixo
- O nome do evento. A convenção é um nome que diz o que aconteceu:
EstoqueBaixo,VendaFeita,Clicou. - EstoqueBaixo?.Invoke(produto)
- Dispara o evento: chama todos os inscritos, na ordem em que se inscreveram. O
?.evita o erro quando ninguém está inscrito.
Agora o lado dos inscritos (as instruções do começo do arquivo):
| Linha | O que faz |
|---|---|
cadernos.EstoqueBaixo += AvisarAna; | Inscreve o método AvisarAna (sem parênteses: é o método, não a chamada). |
cadernos.EstoqueBaixo += produto => …; | Inscreve uma lambda. O produto recebe o texto que o estoque mandou no Invoke. |
cadernos.Vender(2); | Sobram 3: mais que 2, nenhum aviso. |
cadernos.Vender(2); | Sobra 1: o estoque dispara o evento, e os dois inscritos reagem. |
cadernos.Vender(1); | Sobra 0: dispara de novo. |
Repare que a classe Estoque não sabe nada sobre a Ana nem sobre o setor de compras. Amanhã alguém pode inscrever um terceiro método (mandar um e-mail, gravar num arquivo) sem mexer em uma linha do Estoque.
Missão: ninguém está inscrito no evento, então as vendas acontecem sem aviso. Inscreva no EstoqueBaixo uma lambda que escreva Repor: seguido do nome do produto. A saída deve terminar com Repor: Caderno.
Estoque cadernos = new("Caderno", 4);
// inscreva-se no evento aqui
cadernos.Vender(1);
cadernos.Vender(2);
class Estoque(string produto, int quantidade)
{
public event Action<string>? EstoqueBaixo;
public void Vender(int qtd)
{
quantidade -= qtd;
Console.WriteLine($"Vendeu {qtd} {produto}, sobrou {quantidade}");
if (quantidade <= 2)
{
EstoqueBaixo?.Invoke(produto);
}
}
}
Estoque cadernos = new("Caderno", 4);
cadernos.EstoqueBaixo += produto => Console.WriteLine($"Repor: {produto}");
cadernos.Vender(1);
cadernos.Vender(2);
class Estoque(string produto, int quantidade)
{
public event Action<string>? EstoqueBaixo;
public void Vender(int qtd)
{
quantidade -= qtd;
Console.WriteLine($"Vendeu {qtd} {produto}, sobrou {quantidade}");
if (quantidade <= 2)
{
EstoqueBaixo?.Invoke(produto);
}
}
}
2. Por que event? O estrago de um delegate público
Se o event é "só" um delegate, por que não usar um campo público public Action<string>? EstoqueBaixo;? Veja o que o código de fora consegue fazer com ele. Esta versão tira a palavra event:
Estoque cadernos = new("Caderno", 3);
cadernos.EstoqueBaixo += p => Console.WriteLine($"[Ana] Está acabando: {p}");
cadernos.EstoqueBaixo = p => Console.WriteLine($"[Bruno] {p}?");
cadernos.EstoqueBaixo("Mochila");
cadernos.Vender(2);
class Estoque(string produto, int quantidade)
{
public Action<string>? EstoqueBaixo;
public void Vender(int qtd)
{
quantidade -= qtd;
if (quantidade <= 2)
{
EstoqueBaixo?.Invoke(produto);
}
}
}
$ dotnet run campo.cs [Bruno] Mochila? « alarme falso: ninguém vendeu mochila nenhuma [Bruno] Caderno? « e o aviso da Ana sumiu
Dois estragos, os dois em silêncio:
=no lugar de+=: o Bruno trocou a lista inteira pela lambda dele. A inscrição da Ana foi apagada, e ninguém foi avisado disso.- Disparar de fora:
cadernos.EstoqueBaixo("Mochila")deu um aviso falso. Só o estoque deveria saber quando o estoque está baixo.
Agora, a mesma coisa com public event Action<string>? EstoqueBaixo;:
Este código tem um erro de propósito.
Estoque cadernos = new("Caderno", 3);
cadernos.EstoqueBaixo += p => Console.WriteLine($"[Ana] Está acabando: {p}");
cadernos.EstoqueBaixo = p => Console.WriteLine($"[Bruno] {p}?");
cadernos.EstoqueBaixo("Mochila");
class Estoque(string produto, int quantidade)
{
public event Action<string>? EstoqueBaixo;
public void Vender(int qtd)
{
quantidade -= qtd;
if (quantidade <= 2)
{
EstoqueBaixo?.Invoke(produto);
}
}
}
$ dotnet run evento-erro.cs /home/ana/cs/evento-erro.cs(4,10): error CS0070: O evento "Estoque.EstoqueBaixo" pode apenas aparecer à esquerda de + = ou -= (exceto quando usado de dentro do tipo "Estoque") /home/ana/cs/evento-erro.cs(5,10): error CS0070: O evento "Estoque.EstoqueBaixo" pode apenas aparecer à esquerda de + = ou -= (exceto quando usado de dentro do tipo "Estoque") Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
"O evento só pode aparecer à esquerda de += ou -=, exceto dentro do tipo Estoque" (a tradução escreve + = com um espaço; em inglês, The event 'Estoque.EstoqueBaixo' can only appear on the left hand side of += or -=). A linha 3 (+=) passou; as linhas 4 (=) e 5 (disparar) foram barradas.
| De fora da classe, com… | delegate público | event |
|---|---|---|
+= (se inscrever) | pode | pode |
-= (sair) | pode | pode |
= (trocar a lista toda) | pode (apaga os outros!) | CS0070 |
Disparar: Evento(…), Evento.Invoke(…) | pode (aviso falso!) | CS0070 |
Regra prática: se o objetivo é avisar outros objetos, use event. É uma questão de encapsulamento: o estoque controla quando avisa.
3. O padrão do .NET: EventHandler<T>
Os eventos da biblioteca do .NET (e de quase todo código C# que você vai ler) seguem um padrão fixo. Em vez de um Action qualquer, eles usam o delegate pronto EventHandler ("tratador de evento"), que sempre recebe dois parâmetros:
void Tratador(object? sender, TEventArgs e)
│ └─ os dados do acontecimento ("argumentos do evento")
└─ quem disparou o evento ("remetente")
Com o sender, um mesmo método pode ouvir vários objetos e saber de qual veio o aviso. Veja dois estoques avisando o mesmo método:
Estoque cadernos = new("Caderno", 5);
Estoque lapis = new("Lápis", 3);
cadernos.EstoqueBaixo += AoFicarBaixo;
lapis.EstoqueBaixo += AoFicarBaixo;
cadernos.Vender(4);
lapis.Vender(1);
lapis.Vender(1);
static void AoFicarBaixo(object? sender, EstoqueBaixoEventArgs e)
{
Estoque origem = (Estoque)sender!;
Console.WriteLine($"Aviso de {origem.Produto}: restam {e.Restante}");
}
class EstoqueBaixoEventArgs(int restante) : EventArgs
{
public int Restante { get; } = restante;
}
class Estoque(string produto, int quantidade)
{
public string Produto { get; } = produto;
public event EventHandler<EstoqueBaixoEventArgs>? EstoqueBaixo;
public void Vender(int qtd)
{
quantidade -= qtd;
if (quantidade <= 1)
{
OnEstoqueBaixo(new EstoqueBaixoEventArgs(quantidade));
}
}
protected virtual void OnEstoqueBaixo(EstoqueBaixoEventArgs e)
{
EstoqueBaixo?.Invoke(this, e);
}
}
$ dotnet run handler.cs Aviso de Caderno: restam 1 Aviso de Lápis: restam 1
O lápis começou com 3: a primeira venda deixou 2 (sem aviso), a segunda deixou 1 (aviso). As peças do padrão:
| Peça | O que é |
|---|---|
class EstoqueBaixoEventArgs(int restante) : EventArgs | A "ficha" com os dados do acontecimento. Por convenção, o nome termina em EventArgs e a classe herda de EventArgs (veja Herança). Para mandar mais dados, acrescente propriedades. |
event EventHandler<EstoqueBaixoEventArgs>? | O evento no formato padrão: inscritos que recebem (object? sender, EstoqueBaixoEventArgs e). |
EstoqueBaixo?.Invoke(this, e) | Dispara mandando this (o próprio estoque) como sender, e a ficha e. |
protected virtual void OnEstoqueBaixo(…) | Por convenção, o disparo fica num método On + nome do evento, protected virtual: classes filhas podem reagir ou mudar o disparo com override (veja virtual e override). |
(Estoque)sender! | No inscrito, o sender chega como object?. O cast (Estoque) devolve o tipo verdadeiro, e o ! diz ao compilador "aqui não é null". Se não precisar dele, use o descarte: (_, e) => …. |
Use o EventHandler sem <>, cujo segundo parâmetro é um EventArgs "vazio", e dispare com Evento?.Invoke(this, EventArgs.Empty);. Exemplo: public event EventHandler? Fechou; num caixa que só avisa "fechei".
Action ou EventHandler?
Num programa pequeno, só seu, event Action<string>? é mais curto e funciona muito bem. Numa biblioteca que outras pessoas vão usar, siga o padrão EventHandler<T>: é o que todo programador C# espera encontrar, e é o que os editores e as ferramentas do .NET entendem.
4. Sair da lista (-=) e a armadilha da lambda
Para parar de receber avisos, use -= com o mesmo método. Com um método com nome, funciona. Com uma lambda, veja o que acontece:
Estoque cadernos = new("Caderno", 10);
cadernos.EstoqueBaixo += AvisarAna;
cadernos.EstoqueBaixo += p => Console.WriteLine($"[lambda] {p}");
cadernos.Vender(9);
Console.WriteLine("--- tirando os dois");
cadernos.EstoqueBaixo -= AvisarAna;
cadernos.EstoqueBaixo -= p => Console.WriteLine($"[lambda] {p}");
cadernos.Vender(0);
static void AvisarAna(string produto) => Console.WriteLine($"[Ana] {produto}");
class Estoque(string produto, int quantidade)
{
public event Action<string>? EstoqueBaixo;
public void Vender(int qtd)
{
quantidade -= qtd;
if (quantidade <= 2)
{
EstoqueBaixo?.Invoke(produto);
}
}
}
$ dotnet run desinscrever.cs [Ana] Caderno [lambda] Caderno --- tirando os dois [lambda] Caderno « a lambda continua inscrita!
O -= AvisarAna funcionou. O -= p => … não fez nada, e sem erro nenhum: cada lambda escrita no código é um objeto novo. A segunda lambda tem o mesmo texto, mas não é a mesma que foi inscrita, e o -= não acha nada para tirar.
Como fazer certo: se você vai precisar sair da lista, guarde a lambda numa variável e use a variável nas duas vezes:
Action<string> avisoCompras = p => Console.WriteLine($"[compras] {p}");
cadernos.EstoqueBaixo += avisoCompras;
// …
cadernos.EstoqueBaixo -= avisoCompras; // agora tira de verdade
Enquanto um objeto está inscrito num evento, o publicador guarda uma referência a ele, e o coletor de lixo não pode liberá-lo. Em programas que rodam por muito tempo (um servidor, um aplicativo com janelas), lembre de fazer o -= quando o inscrito não precisa mais dos avisos.
Quando dá errado
= ou disparar o evento de fora da classe
Visto na seção 2: O evento "Estoque.EstoqueBaixo" pode apenas aparecer à esquerda de + = ou -= (exceto quando usado de dentro do tipo "Estoque").
Como consertar: de fora, use só += e -=. Se o código de fora precisa provocar o aviso, crie um método público na classe (como o Vender) que decide quando disparar.
Este código tem um erro de propósito.
Caixa caixa = new();
caixa.Fechou += Avisar;
static void Avisar(string mensagem) => Console.WriteLine(mensagem);
class Caixa
{
public event EventHandler? Fechou;
public void Fechar() => Fechou?.Invoke(this, EventArgs.Empty);
}
$ dotnet run assinatura-ev.cs /home/ana/cs/assinatura-ev.cs(2,1): error CS0123: Nenhuma sobrecarga de "Avisar" corresponde ao representante "EventHandler" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: o evento é um EventHandler, que pede métodos (object? sender, EventArgs e), e Avisar recebe um string.
Como consertar: escreva o inscrito no formato do evento, static void Avisar(object? sender, EventArgs e) => Console.WriteLine("Caixa fechado");, ou use uma lambda que descarta os parâmetros: caixa.Fechou += (_, _) => Console.WriteLine("Caixa fechado");.
NullReferenceException: disparar sem ?.
Este código tem um erro de propósito.
Estoque cadernos = new("Caderno", 3);
cadernos.Vender(2);
class Estoque(string produto, int quantidade)
{
public event Action<string> EstoqueBaixo;
public void Vender(int qtd)
{
quantidade -= qtd;
if (quantidade <= 2)
{
EstoqueBaixo(produto);
}
}
}
$ dotnet run semnulo.cs /home/ana/cs/semnulo.cs(6,33): warning CS8618: O evento não anulável 'EstoqueBaixo' precisa conter um valor não nulo ao sair do construtor. Considere adicionar o modificador "obrigatório" ou declarar o evento como anulável. Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object. at Estoque.Vender(Int32 qtd) in /home/ana/cs/semnulo.cs:line 13 at Program.<Main>$(String[] args) in /home/ana/cs/semnulo.cs:line 2
O que quer dizer: o aviso CS8618 diz que o evento foi declarado sem ?, como se nunca fosse null, mas nada garante isso (o "modificador obrigatório" da tradução é o required). Como ninguém se inscreveu, o evento era null, e a linha 13 tentou chamar um delegate inexistente. A pilha mostra o caminho: o erro aconteceu dentro de Estoque.Vender, chamado pela linha 2.
Como consertar: declare o evento com ? (event Action<string>? EstoqueBaixo;) e dispare sempre com EstoqueBaixo?.Invoke(produto);.
-= com lambda não tira nada
Não há erro nem aviso: o inscrito continua recebendo os avisos (veja a seção 4). Como consertar: guarde a lambda numa variável, ou inscreva um método com nome.
Referência
| Código | Onde | O que faz |
|---|---|---|
public event Action<T>? Nome; | publicador | Declara um evento simples. |
public event EventHandler<XEventArgs>? Nome; | publicador | Declara um evento no padrão do .NET. |
public event EventHandler? Nome; | publicador | Evento padrão sem dados (dispare com EventArgs.Empty). |
Nome?.Invoke(…); | só dentro do publicador | Dispara o evento, se houver inscritos. |
protected virtual void OnNome(XEventArgs e) | publicador | Método padrão que dispara o evento. |
obj.Nome += Metodo; | qualquer lugar | Inscreve um método. |
obj.Nome += (s, e) => …; | qualquer lugar | Inscreve uma lambda. |
obj.Nome -= Metodo; | qualquer lugar | Tira a inscrição (com lambda, só via variável). |
Exercícios
1. Inscrever
Inscreva o método AvisarAna no evento EstoqueBaixo do objeto cadernos:
2. Disparar com segurança
Dentro da classe, dispare o evento VendaFeita (um Action<decimal>?) mandando a variável valor:
3. Declarar
Complete a declaração de um evento público Fechou, no padrão do .NET, sem dados (e que pode ser null):
4. Com public event Action<string>? Aviso;, o que o código de fora da classe pode fazer?
5. obj.Aviso += x => Console.WriteLine(x); e depois obj.Aviso -= x => Console.WriteLine(x);. O que acontece?
6. No padrão EventHandler, o que é o sender?
Missão final: o caixa já tem o evento VendaGrande, e o programa já se inscreveu nele, mas o evento nunca é disparado (o compilador avisa CS0067: "o evento nunca é usado"). Dentro de Registrar, dispare o evento com ?.Invoke quando o valor for de 100 ou mais. A venda de R$ 18,90 não pode dar aviso; a de R$ 129,90 deve mostrar Venda grande: R$ 129,90.
Caixa caixa = new();
caixa.VendaGrande += valor => Console.WriteLine($"Venda grande: {valor:C}");
caixa.Registrar(18.90m);
caixa.Registrar(129.90m);
class Caixa
{
public event Action<decimal>? VendaGrande;
public void Registrar(decimal valor)
{
Console.WriteLine($"Registrou {valor:C}");
// dispare o evento aqui, se o valor for de 100 ou mais
}
}
Caixa caixa = new();
caixa.VendaGrande += valor => Console.WriteLine($"Venda grande: {valor:C}");
caixa.Registrar(18.90m);
caixa.Registrar(129.90m);
class Caixa
{
public event Action<decimal>? VendaGrande;
public void Registrar(decimal valor)
{
Console.WriteLine($"Registrou {valor:C}");
if (valor >= 100m)
{
VendaGrande?.Invoke(valor);
}
}
}
Resumo
- Um evento avisa "aconteceu algo" para uma lista de inscritos. O publicador declara e dispara; os inscritos reagem.
- Declare com
public event Tipo? Nome;, dispare comNome?.Invoke(…), inscreva com+=e saia com-=. - Com
event, o código de fora só pode+=e-=(CS0070): ninguém apaga os outros inscritos nem dá aviso falso. - O padrão do .NET é
EventHandler<XEventArgs>: inscritos(object? sender, XEventArgs e), disparo numprotected virtual void OnNome. - Para tirar uma lambda com
-=, guarde-a numa variável; uma lambda nova nunca é a mesma. - Sem
?e sem?., um evento sem inscritos dáCS8618eNullReferenceException.
Próxima aula: Métodos de Extensão, como "acrescentar" métodos a tipos que não são seus, como decimal e string.