Um delegate é um tipo que representa a assinatura de um método. Uma variável de um tipo delegate guarda uma referência para um método com essa assinatura, e chamar a variável chama o método. É assim que C# trata funções como valores: você pode guardá-las, passá-las para outros métodos e chamá-las depois.
Saída:
40
70
68.00
delegate decimal PriceRule(decimal price); declara um tipo novo, do mesmo jeito que class faz. Qualquer método que recebe um decimal e retorna um decimal se encaixa nele. A variável rule apontou para três métodos diferentes, um de cada vez, e cada chamada executou o que ela guardava naquele momento.
Declarar, criar e invocar
Uma declaração de tipo delegate parece uma assinatura de método com a palavra-chave delegate na frente. Normalmente ela fica no nível do namespace, ao lado das classes, ou aninhada dentro de uma classe.
Há quatro formas de criar uma instância de delegate:
PriceRule a = HalfPrice; // method group (most common)
PriceRule b = new PriceRule(HalfPrice); // explicit constructor, same thing
PriceRule c = p => p - 5m; // lambda expression
PriceRule d = delegate (decimal p) { return p; }; // anonymous method (C# 2 syntax)
E duas formas de chamá-lo, que são idênticas: rule(80m) e rule.Invoke(80m). O compilador verifica a assinatura no momento da atribuição, então um método incompatível é um erro de compilação, e não uma surpresa em tempo de execução.
Um delegate também pode apontar para um método de instância. Nesse caso ele guarda tanto o método quanto o objeto sobre o qual chamá-lo:
Saída:
Ana got: Your order shipped
target: Mailbox, method: Receive
Target é o objeto sobre o qual o delegate vai chamar o método (null para um método static), e Method descreve o método. Guardar o alvo também significa que um delegate mantém esse objeto vivo, o que importa para eventos.
Delegates como callbacks
O principal trabalho de um delegate é permitir que um método chame um código escolhido por quem o chamou. O método define quando algo acontece; quem chama define o quê:
Saída:
[## ] 25%
[##### ] 50%
[####### ] 75%
[##########] 100%
import finished
ImportRows não sabe nada de console nem de barras. O mesmo método pode acionar uma barra de progresso, uma linha de log ou uma atualização de interface, dependendo do que quem chama passar. List.Sort(Comparison<T>), Array.Find(Predicate<T>), Task.Run(Action) e todo operador do LINQ seguem esse padrão.
Delegates multicast: += e -=
Um delegate pode guardar mais de um método. += acrescenta um método à lista de invocação, -= o remove, e invocar o delegate chama todos os métodos em ordem:
Saída:
email sent for A-1001
stock updated for A-1001
warehouse notified for A-1001
3 handlers
email sent for A-1002
warehouse notified for A-1002
Três detalhes dos delegates multicast importam na prática:
- Valores de retorno: se o tipo delegate retorna um valor, invocar um delegate multicast retorna só o resultado do último método. Para juntar todos os resultados, percorra
GetInvocationList()e invoque cada um você mesmo. - Exceções: se um método lança uma exceção, os métodos restantes não executam e a exceção chega a quem chamou.
- Imutabilidade: delegates são imutáveis.
+=cria um delegate novo com uma lista maior e o atribui de volta à variável, e é por isso que remover com-=em uma cópia não afeta o original.
Remover uma lambda com -= só funciona se você passar a mesma instância de delegate. Escrever a mesma lambda duas vezes cria dois delegates diferentes, então handlers -= id => Log(id); não remove nada. Guarde a lambda em uma variável quando pretender cancelar a inscrição dela.
Delegates null e ?.Invoke
Uma variável delegate sem nada atribuído é null, e remover o último método com -= também deixa null. Invocar null lança NullReferenceException:
Saída:
no handler, no crash
saved draft.txt
NullReferenceException
?.Invoke é a forma padrão de chamar um callback opcional. O operador ?. não pode ficar direto antes dos parênteses (onSaved?("x") não é sintaxe válida), então o padrão chama o método Invoke do delegate pelo nome, que faz o mesmo que a chamada abreviada.
Delegates, Func, Action e eventos
Declarar um tipo delegate para cada assinatura seria cansativo, então o .NET traz tipos genéricos prontos:
| Tipo embutido | Declaração própria equivalente |
|---|---|
Action | delegate void Action(); |
Action<T> | delegate void Action<T>(T arg); |
Func<TResult> | delegate TResult Func<TResult>(); |
Func<T, TResult> | delegate TResult Func<T, TResult>(T arg); |
Predicate<T> | delegate bool Predicate<T>(T obj); |
PriceRule, do primeiro exemplo, tem a mesma forma que Func<decimal, decimal>, mas os dois são tipos distintos e não se convertem um no outro. Nem dois tipos embutidos com assinaturas idênticas se convertem:
Func<int, bool> isPositive = x => x > 0;
Predicate<int> p = isPositive;
// error CS0029: Cannot implicitly convert type 'System.Func<int, bool>' to 'System.Predicate<int>'
Predicate<int> ok = new Predicate<int>(isPositive); // wrap it explicitly
Predicate<int> ok2 = x => isPositive(x); // or with a lambda
Uma lambda é convertida para qualquer tipo delegate compatível, então as APIs normalmente recebem lambdas e a questão nem aparece. A página de Func e Action cobre a família genérica e quando uma declaração própria ainda vale a pena.
Eventos são construídos diretamente sobre delegates. Um campo event é um delegate multicast em que o código de fora só pode usar += e -=, nunca invocar nem sobrescrever. A lista de inscritos do exemplo multicast acima é exatamente o que um evento administra para você, com essa proteção extra. Os eventos têm uma página própria.
Perguntas frequentes
O que é um delegate em C#?
Um delegate é uma referência com tipagem segura para um método. Você declara um tipo delegate com uma assinatura, como delegate decimal PriceRule(decimal price);, e qualquer método com essa assinatura (static, de instância ou uma lambda) pode ser guardado em uma variável desse tipo e chamado depois por meio dela. Delegates são a forma como C# passa funções como valores.
Qual a diferença entre um delegate e Func em C#?
Func<T, TResult> e Action<T> são tipos delegate que o .NET já declara para você, genéricos nos tipos dos parâmetros e do retorno. Uma declaração delegate própria dá ao tipo um nome e nomes de parâmetros que documentam o papel dele, e suporta parâmetros ref, out e in. Para a maior parte do código, Func e Action bastam.
O que é um delegate multicast em C#?
Um delegate que guarda vários métodos. += adiciona um método à lista de invocação e -= remove um; invocar o delegate chama todos em ordem. Se o delegate retorna um valor, quem chama só recebe o resultado do último método, e uma exceção em um método impede que os demais executem.
Como invocar com segurança um delegate que pode ser null?
Use o operador condicional nulo: onProgress?.Invoke(50);. Uma variável delegate sem nenhum método atribuído é null, e chamá-la diretamente lança NullReferenceException. ?.Invoke lê a variável uma única vez, então também é seguro se outra thread remover o último handler no mesmo instante.
Quando declarar meu próprio tipo delegate em vez de usar Func ou Action?
Quando a assinatura precisa de parâmetros ref, out ou in (Func e Action não conseguem expressá-los), quando um tipo com nome deixa uma API pública mais clara (delegate bool Validator(string input)), ou quando você quer que os nomes dos parâmetros apareçam no IntelliSense. Fora isso, prefira Func e Action, que todo desenvolvedor .NET reconhece.