1. Callback e programação assíncrona
Callback (callback) — é o mecanismo de passar um método que deve ser chamado quando uma operação termina. Muito comum em operações assíncronas, timers, processamento de dados e UI.
Exemplo 1: Operação assíncrona com callback
Imagina que temos um app onde o usuário faz uma requisição e o resultado chega com atraso (por exemplo, da internet). Depois de receber os dados, queremos atualizar a tela.
// Delegate para callback
public delegate void DataReceivedHandler(string result);
// Mecanismo de download assíncrono (simulação)
public void DownloadDataAsync(DataReceivedHandler callback)
{
// Imaginemos que o download leva tempo (simulamos com um timer)
Task.Delay(1000).ContinueWith(_ =>
{
string data = "Resultados da busca: <dados>";
callback(data); // Chamada do delegate-callback
});
}
// Uso:
DownloadDataAsync(result =>
{
Console.WriteLine("Recebido: " + result);
});
Esse approach permite escrever código bem flexível, onde a lógica pós-recebimento fica totalmente separada da mecânica de obter os dados.
2. Delegates como parâmetros de métodos: estratégia e comparators
Tarefa comum: permitir que o usuário passe a “lógica” (uma função) pro seu método, pra ele definir como comparar, filtrar ou transformar elementos.
Exemplo 2: Implementação do pattern Strategy via delegates
Suponha que a gente tenha uma ordenação, mas quer que o usuário possa ordenar de diferentes formas — por nome, por data, por tamanho etc.
public delegate bool CompareFunc(int a, int b);
public void BubbleSort(int[] arr, CompareFunc compare)
{
for (int i = 0; i < arr.Length; i++)
{
for (int j = 0; j < arr.Length - 1; j++)
{
if (compare(arr[j], arr[j + 1]))
{
// Troca de posição
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
// Ordenação do maior pro menor
CompareFunc descending = (a, b) => a < b;
// Uso
int[] numbers = { 3, 1, 4, 2 };
BubbleSort(numbers, descending);
Console.WriteLine(string.Join(", ", numbers)); // Vai imprimir: 4, 3, 2, 1
Esse jeito é uma forma universal de injetar a sua “strategy” em código de terceiros sem alterar os fontes.
3. Métodos anônimos, expressões lambda e delegates
Com a evolução do C# ficou chato declarar uma classe ou método separado pra cada tarefa. Felizmente surgiram métodos anônimos e lambdas, que permitem criar delegates "on the fly".
Exemplo 3: Lambda como delegate
Func<int, int, int> operation = (x, y) => x * y;
int result = operation(3, 5); // 15
Exemplo 4: Escolha de operação por nome (switch + Delegates)
Func<int, int, int> op;
string userInput = "sum"; // "sub", "mul", "div"
switch (userInput)
{
case "sum": op = (a, b) => a + b; break;
case "sub": op = (a, b) => a - b; break;
case "mul": op = (a, b) => a * b; break;
case "div": op = (a, b) => a / b; break;
default: throw new Exception("Operação desconhecida!");
}
Console.WriteLine(op(6, 2));
Não esqueça de validar a entrada — delegates aqui dão flexibilidade e legibilidade.
4. Delegates e cadeias de tratamento (“chain of responsibility”)
Como delegates suportam multicast, dá pra construir facilmente cadeias de handlers.
Exemplo 5: Cadeia de filtros
Imagina que temos “filtros” que devem processar uma string.
public delegate string StringFilter(string input);
string RemoveDigits(string input) => new string(input.Where(ch => !char.IsDigit(ch)).ToArray());
string ToUpper(string input) => input.ToUpper();
StringFilter filters = RemoveDigits;
filters += ToUpper;
// O delegate aplicará a string por todos os filtros
string text = "Olá123";
foreach (StringFilter filter in filters.GetInvocationList())
{
text = filter(text);
}
Console.WriteLine(text); // Vai imprimir: "OLÁ"
Importante: se você chamar simplesmente filters(text), será retornado o valor apenas do último handler, não da cadeia inteira! Se precisar do "flow" do valor, itere explicitamente com GetInvocationList(), como no exemplo.
5. Delegates para binding dinâmico de comportamento em tempo de execução
Antes, pra trocar um comportamento era comum criar classes e interfaces. Com delegates e lambdas dá pra expressar muito do “polimorfismo leve” com funções.
Exemplo 6: Comportamento de um robô com comando dinâmico
public class Robot
{
public event Action<string>? OnCommandReceived;
public void ReceiveCommand(string command)
{
OnCommandReceived?.Invoke(command);
}
}
// Uso:
var robot = new Robot();
robot.OnCommandReceived += cmd => Console.WriteLine($"Robô executa: {cmd}");
robot.OnCommandReceived += cmd =>
{
if (cmd == "Ligar")
Console.WriteLine("Inicializando sistema...");
};
// Testando
robot.ReceiveCommand("Ligar");
robot.ReceiveCommand("Mover para frente");
Esse padrão é muito usado em testes, protótipos, em containers de DI e pra passar lógica de negócio via parâmetros.
6. Delegates como subscriptions de estado
Suponha que temos uma classe que guarda um estado e, quando ele muda, queremos notificar todos os inscritos. Com delegates (e eventos) isso é trivial.
Exemplo 7: Classe com subscription a mudança
public class Notifier<T>
{
private T _value = default!;
public event Action<T>? ValueChanged;
public T Value
{
get => _value;
set
{
if (!Equals(_value, value))
{
_value = value;
ValueChanged?.Invoke(_value);
}
}
}
}
// Uso:
var intValue = new Notifier<int>();
intValue.ValueChanged += v => Console.WriteLine($"Novo valor: {v}");
intValue.Value = 5; // Evento disparado
intValue.Value = 10;
Esse approach é praticamente "programação reativa em versão minimal", base pra MVVM, data binding e muitos frameworks UI modernos.
7. Delegates, closures e escopo léxico
Lambdas e métodos anônimos podem capturar variáveis do contexto ao redor (closure). Isso é conveniente, mas às vezes causa erros inesperados.
Exemplo 8: Captura de variável e a "armadilha" do loop
Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
{
actions[i] = () => Console.WriteLine(i);
}
foreach (var a in actions)
a(); // Vai imprimir três vezes 3 (!)
Por quê? Porque o closure referencia a mesma variável i, que depois do loop vale 3. E se a gente quiser capturar os valores 0, 1, 2?
for (int i = 0; i < 3; i++)
{
int loopValue = i; // "congela" o valor atual
actions[i] = () => Console.WriteLine(loopValue);
}
Agora o código funciona como esperado. Essas armadilhas são um dos erros mais comuns dos iniciantes com lambdas!
8. Combinando delegates
Multicast delegates contêm uma lista de métodos, e você pode adicionar (+=) ou remover (-=) handlers.
Detalhe: remoção por referência e assinatura
void Handler1() => Console.WriteLine("1");
void Handler2() => Console.WriteLine("2");
Action a = Handler1;
a += Handler2;
a -= Handler1; // Vai deixar apenas Handler2
a?.Invoke(); // Imprime "2"
Exemplo: controle dinâmico de handlers
Action a = Handler1;
a += Handler1;
a -= Handler1; // Agora fica apenas UM Handler1 na lista!
9. Delegates, extensibilidade e inversão de controle (IoC)
Em apps grandes é comum querer que componentes "chamem" código externo sem ficarem acoplados. Delegates ajudam a encaixar extensões, plugins e callbacks sem tight coupling.
Exemplo: Injeção de comportamento via construtor
public class Greeter
{
private readonly Func<string> _getName;
public Greeter(Func<string> getName)
{
_getName = getName;
}
public void Greet() => Console.WriteLine($"Olá, {_getName()}!");
}
// Injeção de comportamentos diferentes:
var greeter1 = new Greeter(() => "Ana");
var greeter2 = new Greeter(() => DateTime.Now.ToShortTimeString());
greeter1.Greet(); // "Olá, Ana!"
greeter2.Greet(); // "Olá, 14:35!"
Na prática esse pattern é usado pra tornar o código testável e fácil de manter.
10. Dicas úteis
Delegates nas interfaces padrão e LINQ
Você vai esbarrar em delegates sempre que trabalhar com LINQ, coleções, async.
- Muitos métodos como List<T>.Find, Array.Sort, Where, Select aceitam delegates (Func<T, bool>, Comparison<T> etc.).
- Métodos LINQ permitem passar lógica de filtragem, transformação, agregação — sem criar classes separadas.
Exemplo: Comparator pra ordenar objetos
var people = new[] { "Ivan", "Maria", "Pedro" };
Array.Sort(people, (a, b) => a.Length.CompareTo(b.Length));
Console.WriteLine(string.Join(", ", people)); // Ivan, Pedro, Maria
Delegates e currying (aplicação parcial de argumentos)
Com métodos anônimos/lambdas você pode "fixar" parte dos parâmetros e obter uma nova função.
Exemplo: Aplicação parcial
Func<int, int, int> sum = (x, y) => x + y;
// Criamos uma função que sempre adiciona 10
Func<int, int> add10 = y => sum(10, y);
Console.WriteLine(add10(5)); // 15
Particularidade da comparação de delegates
Em C# delegates podem ser comparados por igualdade (==) se tiverem a mesma invocation list.
void Handler1() { }
void Handler2() { }
Action a1 = Handler1;
Action a2 = Handler1;
Console.WriteLine(a1 == a2); // True
Action a3 = Handler1; a3 += Handler2;
Action a4 = Handler1; a4 += Handler2;
Console.WriteLine(a3 == a4); // True
Mas se o delegate for criado a partir de um método anônimo ou lambda — são comparadas as instâncias.
Serialização de delegates
Delegates podem ser serializados, mas só se os métodos aos quais eles referenciam estiverem definidos em classes serializáveis e todos os tipos estiverem disponíveis. Desde o .NET 8 o BinaryFormatter está desabilitado por padrão e é considerado obsoleto; em produção serializar delegates é raro.
Interação entre delegates e eventos: quando usar qual?
- Delegate — é um tipo/variável que você pode invocar explicitamente.
- Evento (event) — forma de restringir acesso ao delegate: de fora só se pode assinar/desassinar (+=/-=), mas invocar só dentro da classe.
- Evento sempre é de tipo delegate, mas nem todo delegate é um evento.
Quando usar? Se você quer que a lógica seja definida "fora" da classe, usa delegate. Se precisa controlar subscribe/unsubscribe e proteger a variável — usa event.
11. Erros típicos ao trabalhar com delegates
Erro nº1: Confundir delegate com event.
Usar um field público de delegate (public Action MyAction;) em vez de um evento (public event Action MyAction;) quebra encapsulamento. Código externo pode sobrescrever todos os inscritos (instance.MyAction = null;) ou invocá-los diretamente, o que viola a lógica da classe.
Erro nº2: Tratar mal retornos em delegates multicast.
Se o delegate retorna valor (por exemplo, Func<string, int>), numa chamada normal (myDelegate("test")) vai retornar só o resultado do último método na cadeia. Pra obter resultados de todos os inscritos, itere a invocation list com GetInvocationList().
// Exemplo de iteração pelos resultados de todos os inscritos
var list = myDelegate.GetInvocationList();
foreach (var d in list)
{
var r = ((Func<string, int>)d)("test");
Console.WriteLine(r);
}
Erro nº3: Captura da variável do loop no closure.
Armadilha clássica: a lambda criada dentro de um loop for captura a variável iteradora, não o seu valor atual.
// Errado: todas as ações vão imprimir o último valor de i
for (int i = 0; i < 3; i++)
{
actions[i] = () => Console.WriteLine(i);
}
// Certo: criamos uma cópia local por iteração
for (int i = 0; i < 3; i++)
{
int copy = i;
actions[i] = () => Console.WriteLine(copy);
}
Erro nº4: Criar memory leaks.
Se um método de instância se inscreve num delegate de um objeto de longa vida e não se desinscreve, surge leak. O objeto de longa vida mantém referência ao subscriber e o GC não consegue coletá-lo. Fique atento para desinscrever, especialmente em classes com ciclo de vida curto (ex.: componentes de UI).
GO TO FULL VERSION