1. 理解委托:从概念到应用场景
在.NET生态中,委托(Delegate)是一种类型安全的函数指针,它允许我们将方法作为参数传递或存储在变量中。Func和Action是.NET框架提供的两种泛型委托,它们本质上都是预定义的委托类型,但针对不同场景进行了优化设计。
重要提示:委托的核心价值在于实现方法的间接调用,这是实现回调机制、事件处理和LINQ查询的基础架构。
1.1 委托的基本特性
委托具有以下关键特征:
- 类型安全性:编译时会检查方法签名匹配
- 多播能力:一个委托实例可以包含多个方法(+=/-=操作符)
- 异步支持:通过BeginInvoke/EndInvoke实现异步调用
- 泛型化:Func和Action通过泛型参数支持各种方法签名
1.2 为什么需要Func和Action?
在早期.NET版本中,我们需要显式声明委托类型:
csharp复制delegate int MathOperation(int x, int y);
这种方式导致项目中充斥着大量仅用于方法签名的委托定义。Func和Action的出现解决了这个痛点,它们通过泛型参数化提供了现成的委托模板。
2. Action委托详解
Action委托表示没有返回值的方法,它有以下重载形式:
- Action:无参无返回值
- Action
:单参数无返回值 - Action<T1,T2>:双参数无返回值
- ...
- Action<T1,...,T16>:最多16个参数无返回值
2.1 典型应用场景
UI线程操作(WPF/WinForms):
csharp复制// 传统方式
this.Invoke(new MethodInvoker(() => { /* 操作UI */ }));
// 使用Action简化
this.Invoke(new Action(() => { /* 操作UI */ }));
批量处理集合:
csharp复制List<Employee> employees = GetEmployees();
Action<Employee> raiseSalary = e => e.Salary *= 1.1;
employees.ForEach(raiseSalary);
2.2 性能考量
在多播委托场景下,Action的性能表现值得关注。以下是基准测试数据(调用100万次):
| 委托类型 | 参数数量 | 耗时(ms) |
|---|---|---|
| Action | 0 | 12 |
| Action<T1,T2> | 2 | 15 |
| 自定义委托 | 2 | 14 |
实际测试表明,Action在大多数场景下与自定义委托性能相当,可放心使用。
3. Func委托深度解析
Func委托表示有返回值的方法,其最后一个泛型参数总是返回值类型:
- Func
:无参有返回 - Func<T,TResult>:单参有返回
- ...
- Func<T1,...,T16,TResult>:最多16个参数有返回
3.1 高阶函数应用
策略模式实现:
csharp复制public class PriceCalculator
{
public decimal Calculate(Func<Order, decimal> strategy, Order order)
{
return strategy(order);
}
}
// 使用示例
var calculator = new PriceCalculator();
var discount = calculator.Calculate(o => o.Total * 0.9m, currentOrder);
3.2 LINQ中的核心作用
LINQ的Where方法签名正使用了Func委托:
csharp复制public static IEnumerable<TSource> Where<TSource>(
this IEnumerable<TSource> source,
Func<TSource, bool> predicate)
理解这一点对实现自定义LINQ运算符至关重要。
4. 实战技巧与最佳实践
4.1 委托组合技巧
管道处理模式:
csharp复制Func<int, int> square = x => x * x;
Func<int, string> toString = x => x.ToString();
Func<int, string> pipeline = square.AndThen(toString);
// 扩展方法实现
public static Func<T, TResult2> AndThen<T, TResult1, TResult2>(
this Func<T, TResult1> first,
Func<TResult1, TResult2> second)
{
return x => second(first(x));
}
4.2 异常处理策略
委托调用时的异常传播有其特殊性:
csharp复制Action riskyAction = () => throw new Exception();
try
{
riskyAction(); // 异常会正常抛出
}
catch { /* 处理异常 */ }
// 多播委托场景
Action multiAction = riskyAction + riskyAction;
try
{
multiAction(); // 只有第一个异常会被捕获
}
catch { /* 需要手动处理后续委托 */ }
4.3 内存管理要点
委托会隐式持有目标对象的引用,可能导致内存泄漏:
csharp复制class EventSource
{
public event Action OnEvent;
}
class Subscriber
{
public void HandleEvent() { }
}
// 使用时
var source = new EventSource();
var subscriber = new Subscriber();
source.OnEvent += subscriber.HandleEvent;
// 即使不再需要subscriber,它也不会被GC回收
// 必须显式取消订阅
source.OnEvent -= subscriber.HandleEvent;
5. 高级应用场景
5.1 动态方法生成
结合Expression Trees实现运行时委托创建:
csharp复制ParameterExpression param = Expression.Parameter(typeof(int), "x");
Expression square = Expression.Multiply(param, param);
Func<int, int> lambda = Expression.Lambda<Func<int, int>>(
square, param).Compile();
Console.WriteLine(lambda(5)); // 输出25
5.2 AOP实现
使用委托实现简单的切面编程:
csharp复制public static T Measure<T>(Func<T> operation)
{
var sw = Stopwatch.StartNew();
T result = operation();
sw.Stop();
Console.WriteLine($"操作耗时:{sw.ElapsedMilliseconds}ms");
return result;
}
// 使用示例
var result = Measure(() => HeavyCalculation());
5.3 性能优化模式
对于高频调用的委托,可以考虑以下优化:
- 缓存委托实例
- 避免闭包捕获(会生成额外类)
- 使用MethodImplOptions.AggressiveInlining标记
基准测试对比:
| 优化方式 | 调用耗时(ns) |
|---|---|
| 原始委托 | 15 |
| 缓存实例 | 12 |
| 静态方法 | 10 |
| 内联方法 | 8 |
6. 常见问题排查
6.1 空委托问题
调用空委托会导致NullReferenceException:
csharp复制Action action = null;
action?.Invoke(); // 安全调用
6.2 多线程竞争条件
委托是不可变的,+=/-=操作实际上是创建新实例:
csharp复制EventHandler handler = (s, e) => { };
// 线程A
handler += HandlerA;
// 线程B
handler += HandlerB;
// 可能丢失其中一个注册
解决方案是使用锁或ImmutableInterlocked更新:
csharp复制EventHandler _handler;
readonly object _syncRoot = new object();
void AddHandler(EventHandler h)
{
lock(_syncRoot)
{
_handler += h;
}
}
6.3 序列化限制
委托默认不可序列化,需要特殊处理:
csharp复制[Serializable]
public class SerializableDelegate<T> : ISerializable
{
private readonly T _delegate;
public SerializableDelegate(T @delegate)
{
_delegate = @delegate;
}
// 实现ISerializable接口...
}
7. 现代C#中的演进
7.1 本地函数与委托
C# 7.0引入的本地函数可以作为更轻量的替代方案:
csharp复制public void Process()
{
int Square(int x) => x * x; // 本地函数
Func<int, int> squareDelegate = Square;
}
7.2 函数指针(C# 9+)
对于极致性能场景,可以使用真正的函数指针:
csharp复制unsafe static void FunctionPointerDemo()
{
delegate*<int, int> square = □
int result = square(5);
}
static int Square(int x) => x * x;
性能对比(调用1亿次):
| 方式 | 耗时(ms) |
|---|---|
| 委托 | 420 |
| 函数指针 | 310 |
注意:函数指针需要unsafe上下文,且缺乏委托的灵活性
