委托
在 C# 中,委托(Delegate) 是一种引用类型,它可以用来封装一个或多个方法,使得方法可以像变量一样被传递、存储和调用。简单来说,委托相当于一个 "方法的容器",允许你将方法作为参数传递给其他方法,或者在不同地方动态调用方法。
委托的基本特性
-
类型安全:委托在定义时必须指定它所能封装的方法的签名(参数类型、返回值类型),只能存储与该签名匹配的方法。
-
多播性:一个委托实例可以包含多个方法(形成调用列表),调用委托时会依次执行所有包含的方法。
-
引用方法:可以引用静态方法,也可以引用实例方法。
委托的使用步骤
1. 定义委托类型
委托的定义类似方法,但需要使用 delegate
关键字,语法如下:
// 定义一个委托类型,它可以封装 "接收两个int参数,返回int" 的方法 public delegate int CalculateDelegate(int a, int b);
这里的委托类型 CalculateDelegate
规定了它能封装的方法必须满足:两个 int
参数,返回 int
。
2. 创建委托实例并关联方法
定义委托类型后,可以创建其实例,并将匹配签名的方法 "绑定" 到委托上:
// 定义一个与委托签名匹配的方法 public static int Add(int x, int y) { return x + y; } public static int Multiply(int x, int y) { return x * y; } // 创建委托实例,并绑定方法 CalculateDelegate calc = new CalculateDelegate(Add);
3. 调用委托
委托实例可以像方法一样被调用,实际会执行它所绑定的方法:
int result = calc(3, 4); // 等价于调用 Add(3,4),结果为 7
委托的多播性(Multicast)
委托可以通过 +=
运算符添加多个方法,通过 -=
移除方法,形成一个方法调用列表:
// 添加第二个方法 calc += Multiply; // 调用委托时,会依次执行 Add 和 Multiply int result = calc(3, 4); // 执行顺序:先 Add(3,4)=7,再 Multiply(3,4)=12,最终返回最后一个方法的结果 12
注意:多播委托调用时,返回值为最后一个方法的返回值;如果方法有输出参数,会被最后一个方法覆盖。
内置委托
1. Action
系列
Action
委托用于封装无返回值(void
) 的方法,可接受 0 到 16 个参数(泛型参数指定参数类型)。
常用形式:
-
Action
:无参数的无返回值方法 -
Action<T>
:接收 1 个T
类型参数的无返回值方法 -
Action<T1, T2>
:接收 2 个参数的无返回值方法 -
... 以此类推,最多支持
Action<T1, T2, ..., T16>
示例:
// 无参数 Action printHello = () => Console.WriteLine("Hello"); printHello(); // 输出:Hello // 1 个参数 Action<string> printMessage = (msg) => Console.WriteLine(msg); printMessage("Action 示例"); // 输出:Action 示例 // 2 个参数 Action<int, int> printSum = (a, b) => Console.WriteLine(a + b); printSum(3, 5); // 输出:8
2. Func
系列
Func
委托用于封装有返回值的方法,最后一个泛型参数表示返回值类型,前面的参数表示输入参数类型,支持 0 到 16 个输入参数。
常用形式:
-
Func<TResult>
:无参数,返回TResult
类型 -
Func<T, TResult>
:1 个T
类型参数,返回TResult
-
Func<T1, T2, TResult>
:2 个参数,返回TResult
-
... 最多支持
Func<T1, ..., T16, TResult>
示例:
// 无参数,返回 string Func<string> getGreeting = () => "Hello Func"; Console.WriteLine(getGreeting()); // 输出:Hello Func // 1 个参数,返回 int(计算字符串长度) Func<string, int> getLength = (s) => s.Length; Console.WriteLine(getLength("test")); // 输出:4 // 2 个参数,返回 bool(判断是否相等) Func<int, int, bool> isEqual = (a, b) => a == b; Console.WriteLine(isEqual(5, 5)); // 输出:True
3. Predicate<T>
Predicate<T>
是一种特殊的泛型委托,用于封装返回 bool
类型的方法,仅接收 1 个 T
类型参数,通常用于判断某个条件是否成立(如集合筛选)。
定义:
public delegate bool Predicate<in T>(T obj);
示例:
// 判断数字是否为偶数 Predicate<int> isEven = (num) => num % 2 == 0; Console.WriteLine(isEven(4)); // 输出:True Console.WriteLine(isEven(7)); // 输出:False // 筛选列表中符合条件的元素 List<int> numbers = new List<int> { 1, 2, 3, 4, 5 }; List<int> evenNumbers = numbers.FindAll(isEven); // evenNumbers 结果:[2, 4]
4. 事件相关委托
用于定义事件处理方法的委托,遵循 .NET 事件设计规范:
(1)EventHandler
用于无自定义数据的事件,签名为:
public delegate void EventHandler(object sender, EventArgs e);
-
sender
:事件源(触发事件的对象) -
e
:事件数据(通常用EventArgs.Empty
表示无数据)
(2)EventHandler<TEventArgs>
用于带自定义数据的事件(泛型版本),签名为:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e) where TEventArgs : EventArgs;
-
TEventArgs
:继承自EventArgs
的自定义事件数据类
示例:
// 自定义事件数据 public class ValueChangedEventArgs : EventArgs { public int OldValue { get; } public int NewValue { get; } public ValueChangedEventArgs(int oldVal, int newVal) { OldValue = oldVal; NewValue = newVal; } } // 发布者类 public class Counter { private int _value; // 声明事件(使用泛型EventHandler) public event EventHandler<ValueChangedEventArgs> ValueChanged; public int Value { get => _value; set { if (_value != value) { // 触发事件 ValueChanged?.Invoke(this, new ValueChangedEventArgs(_value, value)); _value = value; } } } } // 使用 var counter = new Counter(); // 订阅事件 counter.ValueChanged += (sender, e) => Console.WriteLine($"值从 {e.OldValue} 变为 {e.NewValue}"); counter.Value = 10; // 输出:值从 0 变为 10 counter.Value = 20; // 输出:值从 10 变为 20
内置委托的优势
-
减少代码冗余:无需重复定义相同签名的委托,直接使用内置类型。
-
增强代码可读性:开发者对内置委托的行为有共识,降低理解成本。
-
适配框架 API:.NET 框架中的很多方法(如 LINQ、事件机制)都依赖这些内置委托,使用它们能更好地与框架集成。
示例:使用委托实现动态计算
using System; // 定义委托类型 public delegate int CalculateDelegate(int a, int b); class Program { static int Add(int x, int y) => x + y; static int Subtract(int x, int y) => x - y; static int Multiply(int x, int y) => x * y; // 接收委托作为参数的方法 static void CalculateAndPrint(CalculateDelegate calc, int a, int b) { int result = calc(a, b); Console.WriteLine($"结果:{result}"); } static void Main() { // 动态选择不同的计算方法 CalculateAndPrint(Add, 10, 5); // 结果:15 CalculateAndPrint(Subtract, 10, 5); // 结果:5 CalculateAndPrint(Multiply, 10, 5); // 结果:50 // 使用 lambda 表达式直接定义方法 CalculateAndPrint((a, b) => a / b, 10, 5); // 结果:2 } }
总结
委托是 C# 中实现方法回调、事件机制的核心机制,它使得方法可以像数据一样被传递和操作,极大地增强了代码的灵活性和扩展性。实际开发中,推荐优先使用 Action
和 Func
等系统委托,减少自定义委托的数量
事件
在 C# 中,事件(Event)的实现完全依赖于委托(Delegate),事件本质上是对委托的一种封装和约束,而 事件相关委托 则是专门用于定义事件处理方法签名的委托类型。
.NET 框架为事件处理提供了标准化的委托类型,遵循统一的设计规范,确保事件机制的一致性和易用性。
事件相关委托的核心规范
按照 .NET 设计约定,事件处理委托通常满足以下特征:
-
返回值为
void
(事件通知通常不需要返回值) -
第一个参数为
object sender
:表示触发事件的源头对象(事件发布者) -
第二个参数为
EventArgs
类型或其派生类:用于传递事件相关的数据
常用的事件相关委托
1. EventHandler
(无自定义数据)
最基础的事件委托,用于不需要传递自定义数据的事件,定义如下:
public delegate void EventHandler(object sender, EventArgs e);
-
参数说明:
-
sender
:事件的触发者(发布者实例) -
e
:事件数据,无数据时使用EventArgs.Empty
-
2. EventHandler<TEventArgs>
(带自定义数据)
泛型版本,用于需要传递自定义数据的事件,定义如下:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e) where TEventArgs : EventArgs; // 约束:必须继承自EventArgs
-
TEventArgs
:自定义事件数据类,需继承EventArgs
,用于携带事件相关信息
完整使用示例
步骤 1:定义自定义事件数据(可选)
如果需要传递自定义数据,创建继承自 EventArgs
的类:
// 自定义事件数据:存储价格变动信息 public class PriceChangedEventArgs : EventArgs { public decimal OldPrice { get; } public decimal NewPrice { get; } public DateTime ChangeTime { get; } public PriceChangedEventArgs(decimal oldPrice, decimal newPrice) { OldPrice = oldPrice; NewPrice = newPrice; ChangeTime = DateTime.Now; } }
步骤 2:定义事件发布者(使用事件委托)
在发布者类中声明事件,类型为 EventHandler
或 EventHandler<TEventArgs>
:
public class Product { private decimal _price; // 声明事件(使用带自定义数据的泛型委托) public event EventHandler<PriceChangedEventArgs> PriceChanged; public string Name { get; set; } public decimal Price { get => _price; set { if (_price != value) { // 触发事件前,先检查是否有订阅者 OnPriceChanged(_price, value); _price = value; } } } // 封装事件触发逻辑(约定命名为 OnXXX) protected virtual void OnPriceChanged(decimal oldPrice, decimal newPrice) { // 触发事件(使用 ?. 确保有订阅者时才调用) PriceChanged?.Invoke(this, new PriceChangedEventArgs(oldPrice, newPrice)); } }
步骤 3:定义事件订阅者(实现事件处理方法)
订阅者通过匹配委托签名的方法来响应事件:
public class Customer { public string Name { get; set; } // 事件处理方法(签名必须与EventHandler<T>匹配) public void OnProductPriceChanged(object sender, PriceChangedEventArgs e) { // 从sender获取事件源信息 var product = sender as Product; Console.WriteLine($"{Name} 收到通知:"); Console.WriteLine($"商品 {product?.Name} 价格变动:"); Console.WriteLine($"从 {e.OldPrice:C} 变为 {e.NewPrice:C}"); Console.WriteLine($"变动时间:{e.ChangeTime:yyyy-MM-dd HH:mm:ss}\n"); } }
步骤 4:订阅和触发事件
class Program { static void Main() { // 创建发布者和订阅者 var laptop = new Product { Name = "笔记本电脑", Price = 5999m }; var customer1 = new Customer { Name = "张三" }; var customer2 = new Customer { Name = "李四" }; // 订阅事件(+= 注册处理方法) laptop.PriceChanged += customer1.OnProductPriceChanged; laptop.PriceChanged += customer2.OnProductPriceChanged; // 修改价格,触发事件 laptop.Price = 5499m; // 取消订阅(-= 移除处理方法) laptop.PriceChanged -= customer1.OnProductPriceChanged; Console.WriteLine("张三取消订阅后:"); laptop.Price = 4999m; } }
输出结果
张三 收到通知: 商品 笔记本电脑 价格变动: 从 ¥5,999.00 变为 ¥5,499.00 变动时间:2023-10-01 15:30:00 李四 收到通知: 商品 笔记本电脑 价格变动: 从 ¥5,999.00 变为 ¥5,499.00 变动时间:2023-10-01 15:30:00 张三取消订阅后: 李四 收到通知: 商品 笔记本电脑 价格变动: 从 ¥5,499.00 变为 ¥4,999.00 变动时间:2023-10-01 15:30:01
事件委托的关键作用
-
约束签名:确保事件处理方法遵循统一的参数规范(
sender
+ 事件数据),便于框架和开发者理解。 -
解耦通信:发布者只需通过委托触发事件,无需知道订阅者的具体实现。
-
安全封装:事件通过委托实现多播(多个订阅者),但外部只能订阅 / 取消订阅,不能直接触发事件(只能在发布者内部触发)。
总结
事件相关委托(EventHandler
和 EventHandler<TEventArgs>
)是 C# 事件机制的基础,它们定义了事件处理方法的标准签名,确保了事件系统的规范性和灵活性。
实际开发中,应始终遵循 .NET 事件设计规范:
-
事件委托使用
EventHandler
或其泛型版本 -
事件数据类继承
EventArgs
-
事件触发方法命名为
OnXXX
并保护(protected
)