C#: Delegados e eventos
1. Conceitos de delegação
Um delegado é um ponteiro de função com segurança de tipos em C# que permite que métodos sejam passados como parâmetros. Um delegado define a assinatura de um método (tipo de retorno e lista de parâmetros), e somente métodos que correspondam a essa assinatura podem ser atribuídos a uma instância de delegado.
Ao contrário dos ponteiros de função em C/C++, os delegados são orientados a objetos, seguros quanto ao tipo e suportam invocação multicast.
2. Declaração e uso de delegados
Use a palavra-chave delegate para declarar um tipo de delegado; em seguida, crie uma instância e associe-a a um método.
▶ Exemplo
delegate int MathOp(int a, int b);
class Program
{
static int Add(int a, int b) => a + b;
static int Multiply(int a, int b) => a * b;
static void Main()
{
MathOp op = Add;
Console.WriteLine(op(3, 4));
op = Multiply;
Console.WriteLine(op(3, 4));
}
}
7
12
3. Delegados de multicast
Os delegados suportam multicast, o que significa que uma única instância de delegado pode fazer referência a vários métodos. Use += para adicionar um método e -= para remover um. Quando invocados, todos os métodos são executados na ordem em que foram adicionados. Para delegados com valores de retorno, uma invocação multicast retorna apenas o resultado do último método.
▶ Exemplo
delegate void NotifyHandler(string msg);
class Program
{
static void LogToConsole(string msg) => Console.WriteLine($"Console: {msg}");
static void LogToFile(string msg) => Console.WriteLine($"File: {msg}");
static void LogToDb(string msg) => Console.WriteLine($"Database: {msg}");
static void Main()
{
NotifyHandler handler = LogToConsole;
handler += LogToFile;
handler += LogToDb;
handler("System started");
Console.WriteLine("---");
handler -= LogToFile;
handler("System shutdown");
}
}
Console: System started
File: System started
Database: System started
---
Console: System shutdown
Database: System shutdown
4. Delegados embutidos
O C# oferece três delegados embutidos comumente utilizados que atendem à maioria dos cenários sem a necessidade de definições personalizadas.
(1) Ação
Action representa um método sem valor de retorno. Ele aceita de 0 a 16 parâmetros genéricos: Action, Action<T>, Action<T1,T2>, etc.
▶ Exemplo
class Program
{
static void Main()
{
Action```<string>``` greet = name => Console.WriteLine($"Hello, {name}!");
greet("Alice");
Action noParam = () => Console.WriteLine("Parameterless Action");
noParam();
}
}
Hello, Alice!
Parameterless Action
(2) Func
Func<T, TResult> representa um método com um valor de retorno. O último parâmetro genérico é o tipo de retorno, e os anteriores são parâmetros de entrada: Func<TResult>, Func<T, TResult>, Func<T1,T2,TResult>, etc.
▶ Exemplo
class Program
{
static void Main()
{
Func`<int, int, int>` add = (a, b) => a + b;
Console.WriteLine(add(10, 20));
Func```<string>``` getTime = () => DateTime.Now.ToString("HH:mm:ss");
Console.WriteLine(getTime());
}
}
30
14:30:00
(3) Predicado
Predicate<T> representa um método que retorna bool, comumente utilizado para filtragem e verificações condicionais.
▶ Exemplo
class Program
{
static void Main()
{
Predicate```<int>``` isEven = n => n % 2 == 0;
Console.WriteLine(isEven(4));
Console.WriteLine(isEven(7));
var numbers = new List```<int>``` { 1, 2, 3, 4, 5, 6 };
var evens = numbers.FindAll(isEven);
Console.WriteLine(string.Join(", ", evens));
}
}
True
False
2, 4, 6
5. Métodos anônimos
Os métodos anônimos utilizam a palavra-chave delegate para definir um bloco de código inline sem a necessidade de um método nomeado separado.
▶ Exemplo
class Program
{
static void Main()
{
Func`<int, int>` square = delegate(int x) { return x * x; };
Console.WriteLine(square(5));
Action```<string>``` log = delegate(string msg) { Console.WriteLine($"[LOG] {msg}"); };
log("Anonymous method example");
}
}
25
[LOG] Anonymous method example
x => x * x devido à sua sintaxe mais concisa.
6. Eventos
Um evento é um mecanismo de publicação-assinatura baseado em delegados. Quando um campo delegado é modificado com a palavra-chave event, somente a classe que declara o evento pode acioná-lo internamente; classes externas podem apenas se inscrever ou cancelar a inscrição usando += e -=, e não podem invocá-lo diretamente.
▶ Exemplo
class Button
{
public event EventHandler```<string>``` OnClick;
public void Click()
{
OnClick?.Invoke(this, "Button clicked");
}
}
class Program
{
static void Main()
{
var btn = new Button();
btn.OnClick += (sender, msg) => Console.WriteLine($"Event received: {msg}");
btn.Click();
btn.Click();
}
}
Event received: Button clicked
Event received: Button clicked
OnClick.Invoke() diretamente resultará em uma exceção NullReferenceException; portanto, você deve usar a invocação condicional nula ?.Invoke().
7. Padrão de publicação-assinatura
O padrão de publicação-assinatura (Editor-Assinante) é o principal cenário de aplicação para eventos. O editor é responsável por gerar eventos, e os assinantes são responsáveis por responder a eles — os dois estão completamente desacoplados.
▶ Exemplo
class NewsPublisher
{
public event EventHandler```<string>``` OnNewsPublished;
public void Publish(string news)
{
Console.WriteLine($"Publishing news: {news}");
OnNewsPublished?.Invoke(this, news);
}
}
class NewsSubscriber
{
public string Name { get; }
public NewsSubscriber(string name)
{
Name = name;
}
public void Subscribe(NewsPublisher publisher)
{
publisher.OnNewsPublished += HandleNews;
}
public void Unsubscribe(NewsPublisher publisher)
{
publisher.OnNewsPublished -= HandleNews;
}
private void HandleNews(object sender, string news)
{
Console.WriteLine($" [{Name}] Received: {news}");
}
}
class Program
{
static void Main()
{
var publisher = new NewsPublisher();
var sub1 = new NewsSubscriber("Reader A");
var sub2 = new NewsSubscriber("Reader B");
sub1.Subscribe(publisher);
sub2.Subscribe(publisher);
publisher.Publish("C# 12 officially released");
sub2.Unsubscribe(publisher);
publisher.Publish(".NET 9 performance improved by 50%");
}
}
Publishing news: C# 12 officially released
[Reader A] Received: C# 12 officially released
[Reader B] Received: C# 12 officially released
Publishing news: .NET 9 performance improved by 50%
[Reader A] Received: .NET 9 performance improved by 50%
8. EventArgs personalizados
O T em EventHandler<T> normalmente herda de EventArgs e é usado para transportar dados de eventos personalizados.
▶ Exemplo
class TemperatureEventArgs : EventArgs
{
public double Temperature { get; }
public DateTime Timestamp { get; }
public TemperatureEventArgs(double temperature)
{
Temperature = temperature;
Timestamp = DateTime.Now;
}
}
class TemperatureMonitor
{
public event EventHandler<TemperatureEventArgs> OnTemperatureAlert;
private double _threshold;
public TemperatureMonitor(double threshold)
{
_threshold = threshold;
}
public void CheckTemperature(double current)
{
Console.WriteLine($"Current temperature: {current}°C");
if (current > _threshold)
{
var args = new TemperatureEventArgs(current);
OnTemperatureAlert?.Invoke(this, args);
}
}
}
class Cooler
{
public void StartCooling(object sender, TemperatureEventArgs e)
{
Console.WriteLine($" 🚨 Temperature exceeded! {e.Temperature}°C > threshold, starting cooling (time: {e.Timestamp:HH:mm:ss})");
}
}
class Program
{
static void Main()
{
var monitor = new TemperatureMonitor(30.0);
var cooler = new Cooler();
monitor.OnTemperatureAlert += cooler.StartCooling;
monitor.CheckTemperature(25.0);
monitor.CheckTemperature(32.5);
monitor.CheckTemperature(28.0);
monitor.CheckTemperature(35.0);
}
}
Current temperature: 25°C
Current temperature: 32.5°C
🚨 Temperature exceeded! 32.5°C > threshold, starting cooling (time: 14:30:00)
Current temperature: 28°C
Current temperature: 35°C
🚨 Temperature exceeded! 35°C > threshold, starting cooling (time: 14:30:01)
9. Delegados x Eventos
| Recurso | Delegado | Evento |
|---|---|---|
| Natureza | Tipo (ponteiro de função) | Modificador (instância de delegado restrita) |
| Chamada externa | Pode ser chamada em qualquer lugar | Somente a classe que a declara pode levantá-la internamente |
| Atribuição externa | Pode ser substituída por = |
Só é possível usar += / -= |
| Utilização | Callbacks, padrão de estratégia | Publicação-assinatura, mecanismos de notificação |
| Segurança | Inferior | Superior, engloba as permissões de invocação |
event, que restringe o acesso externo à invocação e à atribuição, garantindo que apenas o emissor possa acionar o evento.
❓ Perguntas Frequentes
P: Qual é a diferença entre um delegado e um evento? R: Um evento é um delegado ao qual foi aplicado o modificador
event; o código externo só pode se inscrever ou cancelar a inscrição usando+=/-=, não pode invocá-lo ou sobrescrevê-lo diretamente, e somente a classe declarante pode declará-lo internamente.
P: O que um delegado multicast com valor de retorno retorna? R: Ele retorna o valor de retorno do último método invocado; os valores de retorno dos métodos anteriores são descartados.
P: Qual é a diferença entre Action e Func? R: Action não tem valor de retorno (void), enquanto Func tem um valor de retorno — o último parâmetro genérico é o tipo de retorno.
P: Por que usar
?.Invoke()para disparar um evento? R: Para evitar umNullReferenceExceptionquando o evento não tem assinantes;?.Invoke()ignora automaticamente a chamada quando o evento é nulo.
P: Predicate e Func
<T, bool>são equivalentes? R: Eles são funcionalmente equivalentes.Predicate<T>é uma versão especializada deFunc<T, bool>com semântica mais explícita, comumente utilizada em cenários de filtragem.
📖 Resumo
- Os delegados são ponteiros de função com segurança de tipo que definem assinaturas de métodos
- Use a palavra-chave
delegatepara declarar tipos de delegados personalizados - Os delegados multicast combinam vários métodos por meio de
+=/-=, executando-os em ordem Actionnão tem valor de retorno,Functem um valor de retorno,Predicateretorna um bool- Os métodos anônimos utilizam
delegate{ }para definição inline; as expressões lambda são uma alternativa mais concisa - A palavra-chave
eventrestringe as permissões de invocação e atribuição de delegados, permitindo que apenas a classe declarante a instancie - O padrão de publicação-assinatura separa os editores dos assinantes
- As classes
EventArgspersonalizadas herdam da classe baseEventArgspara transportar dados de eventos
📝 Exercícios
- Defina um delegado
delegate string Transform(string s);, associe-o a três métodos para converter para maiúsculas, converter para minúsculas e inverter a sequência de caracteres; em seguida, teste a invocação por multicast - Use
Func<int, int, bool>para implementar um comparador que determine se o primeiro número é maior que o segundo - Crie uma classe
FileWatchercomevent EventHandler``<string>`` OnFileChanged, simulando notificações aos assinantes quando um arquivo for alterado - Definir o parâmetro personalizado
StockPriceEventArgs(que contém o código da ação, o preço e a porcentagem de variação) e implementar um monitor de cotação de ações que acione um evento quando a porcentagem de variação exceder um limite - Compare o uso de um delegado personalizado com o uso de
EventHandler<T>para implementar o mesmo evento e resuma os cenários em que cada um deles se aplica