C# Delegates
Até agora, as variáveis guardavam dados: um número, um texto, uma lista. Um delegate ("delegado", "representante") é uma variável que guarda um método. Com ele, você pode trocar a regra de um programa sem mexer no resto, e passar um comportamento para outro método do mesmo jeito que passa um número. É a base das lambdas, dos eventos e do LINQ, os próximos assuntos do guia.
- O que é um delegate e por que ele existe.
- Como declarar um tipo delegate:
delegate decimal RegraDePreco(decimal preco);. - Como guardar um método numa variável, chamar com
()ouInvokee trocar o método guardado. - Como passar um método como parâmetro de outro método.
- Como juntar vários métodos num delegate só, com
+=e-=. - O que acontece quando o delegate está vazio (
null) e como evitar o erro com?.Invoke. - Os erros reais: assinatura errada (
CS0123,CS0407), argumentos errados (CS1593), parênteses a mais (CS7036) e o delegate no lugar errado do arquivo (CS8803).
Todos os programas desta aula rodaram com o .NET 10 (C# 14) na máquina da Ana (usuária ana, computador notebook), na pasta ~/cs, com dotnet run arquivo.cs (veja Rodar com dotnet run). Você pode repetir cada um no editor do guia, que compila e roda C# de verdade no navegador.
A ideia: a etiqueta de preço que muda de regra
Na lojinha da Ana, o caixa calcula o preço de cada produto. Num dia comum, o preço é o da etiqueta. Na Black Friday, tudo sai com 30% de desconto. Para os clientes de atacado, 10%.
A Ana não quer reescrever o caixa a cada promoção. Ela quer um caixa com um encaixe, onde ela pluga a regra do dia: "hoje a regra é a da Black Friday". O caixa não sabe qual é a regra; ele só sabe que a regra recebe um preço e devolve um preço.
Esse encaixe é um delegate. Ele tem um formato (recebe decimal, devolve decimal), e qualquer método com esse formato pode ser plugado nele. O nome vem do verbo "delegar": o caixa delega a conta para o método que estiver plugado.
| Na lojinha | Em C# |
|---|---|
| O formato do encaixe: "recebe um preço, devolve um preço" | O tipo delegate: delegate decimal RegraDePreco(decimal preco); |
| As regras que cabem no encaixe | Métodos com a mesma assinatura: PrecoNormal, BlackFriday |
| O encaixe com uma regra plugada | Uma variável do tipo delegate: RegraDePreco regra = BlackFriday; |
| O caixa usando a regra | A chamada: regra(50m) |
1. Declarar um tipo delegate e guardar um método
O primeiro programa cria o formato do encaixe, pluga a regra normal, usa, troca pela regra da Black Friday e usa de novo:
RegraDePreco regra = PrecoNormal;
Console.WriteLine($"Normal: {regra(50m):C}");
regra = BlackFriday;
Console.WriteLine($"Black Friday: {regra(50m):C}");
Console.WriteLine($"Com Invoke: {regra.Invoke(50m):C}");
static decimal PrecoNormal(decimal preco) => preco;
static decimal BlackFriday(decimal preco) => preco * 0.7m;
delegate decimal RegraDePreco(decimal preco);
$ dotnet run regras.cs Normal: R$ 50,00 Black Friday: R$ 35,00 Com Invoke: R$ 35,00
Comece pela última linha, que cria o formato. Ela parece a primeira linha de um método, com a palavra delegate na frente e um ; no lugar do corpo:
- delegate
- Palavra-chave: "o que vem a seguir é um tipo de delegate", um formato de método. Não é um método; é um molde.
- decimal
- O tipo de retorno que os métodos plugados precisam ter.
- RegraDePreco
- O nome do tipo, em PascalCase, como o de uma classe. Depois desta linha,
RegraDePrecoé um tipo, comointoustring. - (decimal preco)
- Os parâmetros que os métodos plugados precisam receber. O nome
precosó documenta; os métodos podem usar outro nome. - ;
- Não tem corpo: o delegate só descreve o formato. Quem faz a conta são os métodos.
Agora o resto, linha a linha:
| Linha | O que faz |
|---|---|
RegraDePreco regra = PrecoNormal; | Cria a variável regra, do tipo RegraDePreco, e guarda nela o método PrecoNormal. Repare: sem parênteses. PrecoNormal é o método em si; PrecoNormal(50m) seria o resultado de chamá-lo. |
regra(50m) | Chama o método que está guardado em regra, passando 50m. Neste momento é o PrecoNormal, que devolve o próprio preço. |
regra = BlackFriday; | Troca o método guardado. A variável é a mesma; a regra plugada mudou. |
regra.Invoke(50m) | O mesmo que regra(50m), escrito por extenso. Invoke quer dizer "invocar", chamar. |
static decimal PrecoNormal(…) => preco; | Um método que recebe decimal e devolve decimal: cabe no encaixe. O => é o corpo de uma linha (veja Métodos). |
static decimal BlackFriday(…) => preco * 0.7m; | Outro método com o mesmo formato: devolve 70% do preço (30% de desconto). |
Num arquivo rodado com dotnet run arquivo.cs, a ordem é: primeiro as instruções (o programa), depois os métodos e, por último, as declarações de tipo (delegate, class, record…). Os métodos escritos no meio das instruções são funções locais, e funcionam como qualquer método. Se o delegate vier antes das instruções, dá o erro CS8803 (veja Quando dá errado).
O delegate não sabe qual regra está plugada
A linha Console.WriteLine($"Black Friday: {regra(50m):C}"); não menciona a Black Friday em lugar nenhum: ela só diz regra(50m). Quem decide o que acontece é o método guardado em regra naquele momento. É exatamente o encaixe do caixa da Ana.
Missão: crie o método Atacado, que dá 10% de desconto (preco * 0.9m), e troque a regra para ele antes da segunda linha de saída. A saída deve ser Atacado: R$ 45,00.
RegraDePreco regra = PrecoNormal;
Console.WriteLine($"Normal: {regra(50m):C}");
// troque a regra aqui
Console.WriteLine($"Atacado: {regra(50m):C}");
static decimal PrecoNormal(decimal preco) => preco;
// crie o método Atacado aqui
delegate decimal RegraDePreco(decimal preco);
RegraDePreco regra = PrecoNormal;
Console.WriteLine($"Normal: {regra(50m):C}");
regra = Atacado;
Console.WriteLine($"Atacado: {regra(50m):C}");
static decimal PrecoNormal(decimal preco) => preco;
static decimal Atacado(decimal preco) => preco * 0.9m;
delegate decimal RegraDePreco(decimal preco);
2. Passar um método como parâmetro
Guardar o método numa variável já é útil, mas o grande poder do delegate é outro: um delegate pode ser parâmetro de um método. Assim, quem chama decide a regra.
decimal[] precos = [12.50m, 30m, 7.90m];
MostrarPrecos(precos, PrecoNormal);
MostrarPrecos(precos, BlackFriday);
void MostrarPrecos(decimal[] lista, RegraDePreco regra)
{
foreach (decimal p in lista)
{
Console.WriteLine($"{p:C} -> {regra(p):C}");
}
Console.WriteLine("---");
}
static decimal PrecoNormal(decimal preco) => preco;
static decimal BlackFriday(decimal preco) => preco * 0.7m;
delegate decimal RegraDePreco(decimal preco);
$ dotnet run mostrar.cs R$ 12,50 -> R$ 12,50 R$ 30,00 -> R$ 30,00 R$ 7,90 -> R$ 7,90 --- R$ 12,50 -> R$ 8,75 R$ 30,00 -> R$ 21,00 R$ 7,90 -> R$ 5,53 ---
- RegraDePreco regra
- Um parâmetro como outro qualquer, só que o tipo é o delegate. Quem chamar
MostrarPrecostem de mandar um método com o formato certo. - regra(p)
- Dentro do método, a regra recebida é chamada para cada preço.
MostrarPrecosnunca sabe se está dando desconto ou não. - MostrarPrecos(precos, BlackFriday)
- Na chamada, o método vai sem parênteses: você está entregando a regra, não o resultado dela.
O 7,90 × 0,7 dá 5,53 exatos, e o formato C (de currency, "moeda") mostra os centavos com o R$ da cultura brasileira (veja Formatar Números).
Sem delegate, a Ana precisaria de dois métodos quase iguais (MostrarPrecosNormais e MostrarPrecosBlackFriday), e um terceiro a cada promoção nova. Com o delegate, o laço é escrito uma vez só, e cada promoção é um método pequeno.
3. Vários métodos num delegate só (+= e -=)
Um delegate pode guardar uma lista de métodos. Quando você chama o delegate, todos rodam, na ordem em que foram adicionados. Isso se chama delegate multicast ("transmissão para vários"). É útil para avisos: quando a Ana vende um produto, o caixa e o estoque precisam saber.
Aviso? avisos = null;
avisos += AvisarCaixa;
avisos += AvisarEstoque;
avisos("Caderno vendido");
Console.WriteLine("---");
avisos -= AvisarCaixa;
avisos?.Invoke("Lápis vendido");
Console.WriteLine("---");
avisos -= AvisarEstoque;
Console.WriteLine(avisos is null);
avisos?.Invoke("Borracha vendida");
Console.WriteLine("fim");
static void AvisarCaixa(string msg) => Console.WriteLine($"[caixa] {msg}");
static void AvisarEstoque(string msg) => Console.WriteLine($"[estoque] {msg}");
delegate void Aviso(string mensagem);
$ dotnet run avisos.cs [caixa] Caderno vendido [estoque] Caderno vendido --- [estoque] Lápis vendido --- True fim
| Linha | O que faz |
|---|---|
delegate void Aviso(string mensagem); | Um formato que recebe um texto e não devolve nada (void). |
Aviso? avisos = null; | Começa vazio. O ? diz que a variável pode ser null (veja Referências Anuláveis). |
avisos += AvisarCaixa; | Adiciona um método à lista. Agora avisos tem um. |
avisos += AvisarEstoque; | Adiciona outro. Agora tem dois. |
avisos("Caderno vendido"); | Chama os dois, na ordem: primeiro o caixa, depois o estoque. |
avisos -= AvisarCaixa; | Tira o método da lista. Sobra só o do estoque. |
avisos?.Invoke("Lápis vendido"); | Chama se a lista não estiver vazia. O ?. é o operador condicional nulo (veja Operadores ??, ?. e ??=). |
avisos -= AvisarEstoque; | Tira o último. Uma lista vazia de métodos vira null: a linha seguinte mostra True. |
avisos?.Invoke("Borracha vendida"); | Como avisos é null, nada acontece, e o programa continua até o fim. |
E se o delegate devolve um valor?
Com vários métodos que devolvem algo, todos rodam, mas a chamada devolve só o resultado do último:
Calculo c = Dobro;
c += Triplo;
Console.WriteLine(c(10));
Console.WriteLine(c.GetInvocationList().Length);
Console.WriteLine(c.GetType().BaseType);
static int Dobro(int x) { Console.WriteLine("Dobro rodou"); return x * 2; }
static int Triplo(int x) { Console.WriteLine("Triplo rodou"); return x * 3; }
delegate int Calculo(int x);
$ dotnet run multicast.cs Dobro rodou Triplo rodou 30 « só o resultado do último (Triplo); o 20 do Dobro se perdeu 2 « GetInvocationList: a lista tem 2 métodos System.MulticastDelegate « todo tipo delegate é, por baixo, uma classe que herda daqui
Por isso, na prática, o multicast é usado com delegates void, como o Aviso. É assim que funcionam os eventos.
4. O delegate vazio: NullReferenceException
Chamar um delegate que está null (sem nenhum método) derruba o programa. O compilador avisa antes:
Este código tem um erro de propósito.
Aviso? avisos = null;
avisos("Caderno vendido");
delegate void Aviso(string mensagem);
$ dotnet run nulo.cs /home/ana/cs/nulo.cs(2,1): warning CS8602: Desreferência de uma referência possivelmente nula. Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object. at Program.<Main>$(String[] args) in /home/ana/cs/nulo.cs:line 2 $ echo $? 134
- O aviso
CS8602(do compilador, em português): "desreferência de uma referência possivelmente nula", ou seja, você está usando algo que pode sernull. - A exceção (do programa rodando, em inglês): "referência de objeto não definida para uma instância de um objeto". Não havia método nenhum para chamar.
- O código de saída
134é o de um programa que terminou com uma exceção não tratada.
Como consertar: chame com avisos?.Invoke("Caderno vendido");. O ?. só chama se houver método; se não houver, a linha não faz nada. Repare que avisos?("…") não existe em C#: com o ?., use sempre o Invoke.
O compilador também acompanha o -=: depois de avisos -= AvisarCaixa;, ele sabe que a lista pode ter ficado vazia e avisa CS8602 se você chamar avisos(…) direto. Foi por isso que o programa avisos.cs usou ?.Invoke depois de cada -=. Veja Avisos do Compilador.
Delegate próprio, Func ou lambda: qual usar
Você acabou de ver a forma "completa": declarar o tipo e escrever um método com nome para cada regra. Ela é a base de tudo, mas no dia a dia o C# oferece dois atalhos, cada um com aula própria:
| Forma | Exemplo | Quando usar |
|---|---|---|
| Tipo delegate próprio + método com nome | delegate decimal RegraDePreco(decimal preco);RegraDePreco r = BlackFriday; | Quando o nome do tipo ajuda a entender o código (RegraDePreco diz mais que "recebe e devolve decimal") e em eventos. |
| Delegates prontos do .NET | Func<decimal, decimal> r = BlackFriday; | Na maioria dos casos: você não precisa declarar tipo nenhum. Aula Func, Action e Predicate. |
| Lambda (método sem nome) | RegraDePreco r = p => p * 0.7m; | Quando a regra é curta e só usada ali. Aula Lambdas. |
Quando dá errado
Este código tem um erro de propósito.
RegraDePreco regra = Desconto; Console.WriteLine(regra(50m)); static decimal Desconto(int porcento) => porcento; delegate decimal RegraDePreco(decimal preco);
$ dotnet run assinatura.cs /home/ana/cs/assinatura.cs(1,22): error CS0123: Nenhuma sobrecarga de "Desconto" corresponde ao representante "RegraDePreco" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: "nenhuma versão de Desconto combina com o delegate RegraDePreco". A tradução em português chama o delegate de representante; em inglês, a mensagem é No overload for 'Desconto' matches delegate 'RegraDePreco'. O delegate recebe um decimal, e Desconto recebe um int.
Como consertar: os parâmetros do método precisam ser os mesmos do delegate, na mesma ordem: static decimal Desconto(decimal preco).
Este código tem um erro de propósito.
RegraDePreco regra = Arredondar; Console.WriteLine(regra(9.99m)); static int Arredondar(decimal preco) => (int)preco; delegate decimal RegraDePreco(decimal preco);
$ dotnet run retorno.cs /home/ana/cs/retorno.cs(1,22): error CS0407: "int Arredondar(decimal)" tem o tipo de retorno incorreto Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: os parâmetros combinam, mas o método devolve int e o delegate promete decimal.
Como consertar: faça o método devolver o tipo do delegate (static decimal Arredondar(decimal preco) => Math.Floor(preco);) ou crie outro tipo delegate para métodos que devolvem int.
Este código tem um erro de propósito.
RegraDePreco regra = PrecoNormal(); Console.WriteLine(regra(50m)); static decimal PrecoNormal(decimal preco) => preco; delegate decimal RegraDePreco(decimal preco);
$ dotnet run parenteses.cs /home/ana/cs/parenteses.cs(1,22): error CS7036: Não há nenhum argumento fornecido que corresponda ao parâmetro necessário 'preco' de 'PrecoNormal(decimal)' Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: com os parênteses, PrecoNormal() é uma chamada, e falta o argumento preco. O compilador nem chega a pensar em delegate.
Como consertar: para guardar o método, escreva só o nome: RegraDePreco regra = PrecoNormal;.
Este código tem um erro de propósito.
RegraDePreco regra = PrecoNormal; Console.WriteLine(regra(50m, 10)); static decimal PrecoNormal(decimal preco) => preco; delegate decimal RegraDePreco(decimal preco);
$ dotnet run argumentos.cs /home/ana/cs/argumentos.cs(2,19): error CS1593: Delegado "RegraDePreco" não obtém 2 argumentos Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: "o delegate RegraDePreco não recebe 2 argumentos" (em inglês, Delegate 'RegraDePreco' does not take 2 arguments). Ele foi declarado com um só.
Como consertar: chame com os argumentos que o tipo delegate pede (regra(50m)), ou mude a declaração do tipo, se a regra realmente precisa de mais um dado.
delegate antes das instruções
Este código tem um erro de propósito.
delegate decimal RegraDePreco(decimal preco); RegraDePreco regra = PrecoNormal; Console.WriteLine(regra(50m)); static decimal PrecoNormal(decimal preco) => preco;
$ dotnet run ordem.cs /home/ana/cs/ordem.cs(3,1): error CS8803: As instruções de nível superior precisam preceder as declarações de namespace e de tipo. Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: a declaração delegate …; cria um tipo, e num arquivo com instruções soltas (veja Program.cs e o Main) os tipos ficam no fim.
Como consertar: mova a linha do delegate para o final do arquivo. Num projeto com vários arquivos, ela também pode ir num arquivo próprio, como uma classe.
Visto na seção 4: Object reference not set to an instance of an object., código de saída 134. Como consertar: delegate?.Invoke(…).
Referência
| Código | O que faz |
|---|---|
delegate decimal Regra(decimal p); | Declara um tipo delegate: métodos que recebem um decimal e devolvem um decimal. |
Regra r = Metodo; | Guarda um método (sem parênteses) numa variável do tipo delegate. |
r(50m) ou r.Invoke(50m) | Chama o método guardado. |
r = OutroMetodo; | Troca o método guardado. |
void Usar(Regra r) { … r(x) … } | Recebe um método como parâmetro. |
Usar(Metodo); | Passa um método como argumento. |
d += Metodo; / d -= Metodo; | Adiciona / remove um método da lista (multicast). |
d?.Invoke(x); | Chama só se houver algum método (evita NullReferenceException). |
d.GetInvocationList() | Devolve a lista de métodos guardados. |
Exercícios
1. Declarar o formato
Complete a declaração de um tipo delegate Conversor para métodos que recebem um string e devolvem um int:
2. Guardar o método
Guarde o método BlackFriday na variável regra:
3. Chamar com segurança
Chame o delegate avisos com o texto "Fechou o caixa" sem derrubar o programa se ele estiver vazio:
4. Com delegate decimal RegraDePreco(decimal preco);, qual método pode ser guardado numa RegraDePreco?
5. Um delegate Calculo guarda Dobro e depois Triplo (com +=). O que c(10) devolve?
6. Qual a diferença entre r = PrecoNormal; e r = PrecoNormal(50m);?
Missão final: o método Total soma os preços do carrinho, mas sempre com o preço normal. Acrescente um parâmetro RegraDePreco regra, use regra(p) na soma e mostre também o total da Black Friday. A saída deve ser Total normal: R$ 50,40 e Total Black Friday: R$ 35,28.
decimal[] carrinho = [12.50m, 30m, 7.90m];
Console.WriteLine($"Total normal: {Total(carrinho):C}");
decimal Total(decimal[] precos)
{
decimal soma = 0m;
foreach (decimal p in precos)
{
soma += p;
}
return soma;
}
static decimal PrecoNormal(decimal preco) => preco;
static decimal BlackFriday(decimal preco) => preco * 0.7m;
delegate decimal RegraDePreco(decimal preco);
decimal[] carrinho = [12.50m, 30m, 7.90m];
Console.WriteLine($"Total normal: {Total(carrinho, PrecoNormal):C}");
Console.WriteLine($"Total Black Friday: {Total(carrinho, BlackFriday):C}");
decimal Total(decimal[] precos, RegraDePreco regra)
{
decimal soma = 0m;
foreach (decimal p in precos)
{
soma += regra(p);
}
return soma;
}
static decimal PrecoNormal(decimal preco) => preco;
static decimal BlackFriday(decimal preco) => preco * 0.7m;
delegate decimal RegraDePreco(decimal preco);
No código inicial, o compilador avisa CS8321 ("a função local 'PrecoNormal' está declarada, mas nunca é usada"), porque ninguém usa as regras ainda. O aviso some quando você as passa para o Total.
Resumo
- Um delegate é uma variável que guarda um método. O tipo dele descreve o formato:
delegate decimal RegraDePreco(decimal preco);. - Guarde o método sem parênteses (
regra = BlackFriday;) e chame comregra(50m)ouregra.Invoke(50m). - O método precisa ter os mesmos parâmetros (
CS0123) e o mesmo retorno (CS0407) do delegate. - Um delegate pode ser parâmetro: quem chama decide a regra, e o laço é escrito uma vez só.
+=e-=juntam e tiram métodos (multicast). Todos rodam na ordem; com retorno, vale o do último.- Um delegate sem métodos é
null; chamá-lo dáNullReferenceException. Use?.Invoke(…). - Num arquivo com instruções soltas, a declaração
delegatefica no fim (CS8803).
Próxima aula: Lambdas, o jeito curto de escrever a regra direto onde ela é usada: p => p * 0.7m.