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.

Você vai aprender
  • 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 () ou Invoke e 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 lojinhaEm 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 encaixeMétodos com a mesma assinatura: PrecoNormal, BlackFriday
O encaixe com uma regra plugadaUma variável do tipo delegate: RegraDePreco regra = BlackFriday;
O caixa usando a regraA 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, como int ou string.
(decimal preco)
Os parâmetros que os métodos plugados precisam receber. O nome preco só 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:

LinhaO 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).
Onde fica cada coisa no arquivo

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 MostrarPrecos tem de mandar um método com o formato certo.
regra(p)
Dentro do método, a regra recebida é chamada para cada preço. MostrarPrecos nunca 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
LinhaO 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 ser null.
  • 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.

Leia os avisos

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:

FormaExemploQuando usar
Tipo delegate próprio + método com nomedelegate 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 .NETFunc<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

CS0123: o método não tem os parâmetros do delegate

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).

CS0407: o tipo de retorno não combina

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.

CS7036: parênteses depois do nome do método

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;.

CS1593: número errado de argumentos na chamada

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.

CS8803: o 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.

NullReferenceException: chamar um delegate vazio

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ódigoO 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:

RegraDePreco 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 com regra(50m) ou regra.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 delegate fica no fim (CS8803).

Próxima aula: Lambdas, o jeito curto de escrever a regra direto onde ela é usada: p => p * 0.7m.