C# 委托与匿名函数详解
目录
委托简介
委托(Delegate) 是 C# 中的一种类型安全的函数指针,它允许你将方法作为参数传递,也可以将方法存储在变量中。委托提供了类型安全的回调机制。
为什么要使用委托?
- 解耦合:将方法与调用方法分离
- 灵活性:可以在运行时动态指定要调用的方法
- 事件处理:C# 的事件系统基于委托实现
- 回调机制:实现函数式编程风格
委托的声明和使用
声明委托
// 声明一个委托类型
delegate void MyDelegate(string message);
delegate int CalculateDelegate(int x, int y);
解析:
委托类似一种函数类型,在上面的声明中,MyDelegate代表一种:接收单个字符串参数,不返回数据的函数;CalculateDelegate则是一种计算类型的函数,接受两个整型参数,返回一个整型结果。在实例化的时候,委托方法就需要符合参数与结果的定义。
委托的特点
- 委托是类型,不是实例
- 委托定义了方法的签名(参数和返回类型)
- 委托可以绑定到匹配签名的方法
基本使用示例
using System;
// 声明一个欢迎委托
delegate void GreetingDelegate(string name);
class Program
{
// 定义方法
static void SayHello(string name)
{
Console.WriteLine($"Hello, {name}!");
}
static void SayGoodbye(string name)
{
Console.WriteLine($"Goodbye, {name}!");
}
static void Main()
{
// 创建委托实例
GreetingDelegate greet;
// 绑定方法
greet = SayHello;
// 调用委托
greet("张三"); // 输出: Hello, 张三!
// 重新绑定
greet = SayGoodbye;
greet("李四"); // 输出: Goodbye, 李四!
}
}
在上述代码中,实例化了一个委托变量 greet,但是并没有制定执行哪种方法。在命名空间内部存在SayHello和SayGoofBye两个方法,它们接受的参数与返回类型均符合委托的定义。因此,可以将委托的实例绑定到这两个方法上。
委托作为参数
delegate double MathOperation(double x, double y);
class Calculator
{
static double Add(double x, double y) => x + y;
static double Multiply(double x, double y) => x * y;
static void ExecuteOperation(double a, double b, MathOperation operation)
{
double result = operation(a, b);
Console.WriteLine($"结果: {result}");
}
static void Main()
{
ExecuteOperation(10, 5, Add); // 输出: 结果: 15
ExecuteOperation(10, 5, Multiply); // 输出: 结果: 50
}
}
在上面的示例代码中,MathOperation 是一个返回 double 类型、接收两个 double 参数的委托类型。Calculator 类中定义了两个与此委托签名一致的方法:Add 和 Multiply。在 ExecuteOperation 方法中,第三个参数是一个委托类型,可以传入任何符合该签名的方法。这样做的好处是,可以根据需要灵活传递不同的算法实现,而不需要修改 ExecuteOperation 的内部逻辑。
在 Main 方法中,通过直接传递方法名(如 Add、Multiply)给委托参数,实现了对不同数学操作的复用。程序运行时会输出传入对应的结果值。
这种做法体现了委托作为参数传递的强大能力——将方法作为“第一类对象”传递,使得代码更具扩展性和可维护性,也方便实现回调、事件等功能。
多播委托
委托可以绑定多个方法,调用委托时,所有绑定的方法都会被执行。
delegate void ProcessDelegate(string data);
class Program
{
static void LogData(string data)
{
Console.WriteLine($"[日志] {data}");
}
static void SaveData(string data)
{
Console.WriteLine($"[保存] {data}");
}
static void Notify(string data)
{
Console.WriteLine($"[通知] {data}");
}
static void Main()
{
ProcessDelegate processor = LogData;
// 使用 += 添加方法
processor += SaveData;
processor += Notify;
// 调用时会执行所有方法
processor("用户登录");
// 输出:
// [日志] 用户登录
// [保存] 用户登录
// [通知] 用户登录
// 使用 -= 移除方法
processor -= SaveData;
processor("用户注销");
// 输出:
// [日志] 用户注销
// [通知] 用户注销
}
}
注意点
- 委托方法按添加顺序执行
- 使用
+=添加方法 - 使用
-=移除方法 - 如果委托为 null 或没有方法,调用会抛出异常
// 安全的调用方式
processor?.Invoke("数据");
// 安全的调用方式解释:
// 当我们直接调用一个委托(如 processor(“数据”))时,如果委托为 null(即没有绑定任何方法),会抛出 NullReferenceException 异常。
// 使用 ?.Invoke 运算符可以避免这个异常。当 processor 不为 null 时才会调用,否则什么也不做。
// 这样可以让代码更加健壮,防止因委托未初始化而导致的程序崩溃。例如:
/*
ProcessDelegate processor = null;
// processor(“数据”); // ❶ 这行会抛出异常,因为 processor 为 null
processor?.Invoke(“数据”); // ❷ 这行不会抛异常,即使 processor 为 null
*/
匿名函数
匿名函数是不需要明确定义名称的方法,包括:
- 匿名方法(Anonymous Methods)
- Lambda 表达式(Lambda Expressions)
匿名方法
C# 2.0 引入的特性,使用 delegate 关键字创建无名的方法。
// 创建一个委托,定义方法签名
delegate int Operation(int x, int y);
class Program
{
static void Main()
{
// 使用匿名方法
Operation add = delegate(int x, int y)
{
return x + y;
};
Console.WriteLine(add(5, 3)); // 输出: 8
// 当委托类型已明确时,可以省略参数类型
Operation subtract = delegate(x, y) { return x - y; };
Console.WriteLine(subtract(10, 4)); // 输出: 6
}
}
上述代码演示了如何使用匿名方法(Anonymous Method)来创建没有名字的方法,并将其赋值给委托类型变量,从而可以像普通方法那样调用。
主要讲解了以下几点:
- 通过
delegate关键字创建匿名方法,并赋值给一个委托变量,如Operation add = delegate(int x, int y) { return x + y; };。 - 匿名方法可以省略参数类型(由编译器推断),从而简化代码书写。
- 调用匿名方法非常直接,和调用委托绑定的普通方法一样:
add(5, 3)。 - 匿名方法通常用于临时、一次性使用的场景,比如集合的查找、排序条件,或事件的简单处理。
- 使用匿名方法可以减少代码量,提升代码的灵活性和可读性,无需单独为每个简单逻辑定义独立方法。
匿名方法的应用场景
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 查找偶数
List<int> evens = numbers.FindAll(
delegate(int n) { return n % 2 == 0; }
);
foreach (int n in evens)
{
Console.Write($"{n} "); // 输出: 2 4 6 8 10
}
Console.WriteLine();
// 使用匿名方法处理事件
Action<string> handler = delegate(string msg)
{
Console.WriteLine($"处理: {msg}");
};
handler("事件触发");
}
}
上述代码主要演示了匿名方法的实际应用场景,特别是如何在集合操作中使用匿名方法来实现逻辑。
以“查找偶数”这部分为例:
List<int> evens = numbers.FindAll(
delegate(int n) { return n % 2 == 0; }
);
解释如下:
- numbers 是一个整数列表,包含了 1 到 10。
- FindAll 是 List 的一个方法,用于从列表中查找所有满足指定条件的元素。
- FindAll 方法需要一个“谓词”——即一个能够接受列表中元素并返回 bool 类型(true/false)的函数。
- 这里的 delegate(int n) { return n % 2 == 0; } 就是匿名方法。它的意思是:对于列表中的每个元素 n,如果 n 除以 2 的余数等于 0(即能被 2 整除),则返回 true——也就是偶数。
- FindAll 会把所有让该匿名方法返回 true 的元素收集到一个新的列表中,这里最终得到的 evens 就是一个只含有偶数的列表。
总结:
- 这个匿名方法实际上就是一个“判断是不是偶数”的小函数。
- FindAll 把它作用于 numbers 中的每个元素,筛选出所有偶数。
如果用传统方式写出来,代码相当于:
bool IsEven(int n)
{
return n % 2 == 0;
}
List<int> evens = numbers.FindAll(IsEven);
用匿名方法可以让逻辑更加简洁地“内联”到调用处,不用单独定义一个新函数。
Lambda 表达式
Lambda 表达式是 C# 3.0 引入的简洁语法,用于创建匿名函数。它是写匿名方法的更简洁方式。
Lambda 表达式语法
// 基本语法: (parameters) => expression
// 或者: (parameters) => { statements }
delegate int Calculate(int x, int y);
class Program
{
static void Main()
{
// Lambda 表达式 - 单行
Calculate add = (x, y) => x + y;
Calculate multiply = (x, y) => x * y;
Console.WriteLine(add(5, 3)); // 输出: 8
Console.WriteLine(multiply(5, 3)); // 输出: 15
// Lambda 表达式 - 多行(需要花括号和 return)
Calculate complex = (x, y) =>
{
int result = x + y;
return result * 2;
};
Console.WriteLine(complex(5, 3)); // 输出: 16
}
}
Lambda 表达式的不同形式
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
// 形式1: 单个参数,括号可选
var evens = numbers.Where(n => n % 2 == 0);
// 形式2: 多个参数,必须使用括号
var sum = numbers.Aggregate((a, b) => a + b);
// 形式3: 无参数
Func<string> getMessage = () => "Hello World";
// 形式4: 显式指定参数类型
var evens2 = numbers.Where((int n) => n % 2 == 0);
// 形式5: 多行 Lambda
var processed = numbers.Select(n =>
{
if (n % 2 == 0)
return n * 2;
else
return n;
});
foreach (int n in processed)
{
Console.Write($"{n} "); // 输出: 1 4 3 8 5
}
}
}
Lambda 与闭包
Lambda 表达式可以捕获外部变量,形成闭包:
class Program
{
static void Main()
{
int multiplier = 10;
// Lambda 捕获外部变量 multiplier
Func<int, int> multiply = x => x * multiplier;
Console.WriteLine(multiply(5)); // 输出: 50
multiplier = 20;
Console.WriteLine(multiply(5)); // 输出: 100
// 注意:闭包捕获的是变量本身,不是值
Action<int> add = n => multiplier += n;
add(5);
Console.WriteLine(multiplier); // 输出: 25
}
}
在以下代码片段中:
int multiplier = 10;
// Lambda 捕获外部变量 multiplier
Func<int, int> multiply = x => x * multiplier;
Console.WriteLine(multiply(5)); // 输出: 50
multiplier = 20;
Console.WriteLine(multiply(5)); // 输出: 100
形成了闭包。这里,Lambda 表达式 x => x * multiplier 并不仅仅读取multiplier当时的值,而是捕获了变量multiplier本身,即在 Lambda 被调用时,它会实时访问当前的multiplier变量。因此后续将multiplier修改为20后,multiply(5)的结果也随之改变。
什么是闭包?
闭包(Closure)是指:函数可以“捕获”并保存其所在外部作用域中的变量,即使这些变量的生命周期已经超出了其原始作用域,该函数依然可以访问和修改它们。
在C#中,Lambda表达式和匿名方法都能够形成闭包。当Lambda表达式引用了它方法外部的局部变量时,这些变量会随委托对象一起被“捕获”保存下来,并且生命周期延长至委托的生命周期。
简言之,闭包是“函数+其引用的外部变量状态”的组合,让函数能够带着上下文环境一起使用。
例如,上例中,multiply这个 Func<int, int> 委托对象和multiplier变量形成了一个闭包。
C# 与 Python 闭包对比
相似之处
- 都允许函数捕获并访问外部作用域中的变量
- 都延长被捕获变量的生命周期
- 都支持读取和修改外部变量
关键差异
变量捕获时机:
// C# - 捕获的是变量本身(引用变量)
int multiplier = 10;
Func<int, int> multiply = x => x * multiplier;
Console.WriteLine(multiply(5)); // 50
multiplier = 20;
Console.WriteLine(multiply(5)); // 100(使用最新值)
# Python - 捕获的是变量在闭包创建时的值
def outer():
multiplier = 10
def inner(x):
return x * multiplier
return inner
func = outer()
print(func(5)) # 50
# 注意:C#中可以修改外部变量并影响闭包,
# Python中也有类似行为,但需要显式使用 nonlocal
装饰器应用:
Python中的装饰器确实是闭包最常见的应用之一:
def timer(func):
def wrapper(*args, **kwargs):
import time
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"耗时: {end - start}秒")
return result
return wrapper # 返回闭包
@timer
def my_function():
time.sleep(1)
my_function() # 输出: 耗时: 1.0秒
C#中的等价实现:
// C#中使用闭包实现类似功能
Func<Action, Action> timer = (func) =>
{
return () =>
{
var start = DateTime.Now;
func();
var end = DateTime.Now;
Console.WriteLine($"耗时: {(end - start).TotalSeconds}秒");
};
};
// 使用
Action myFunction = () => Thread.Sleep(1000);
Action timedFunction = timer(myFunction);
timedFunction();
总结:
- C# 和 Python 的闭包本质相同,都是函数+外部变量的组合
- Python 的装饰器是闭包最典型的应用场景
- C# 中闭包更多用于事件处理、回调函数、LINQ 等场景
常用委托类型
.NET Framework 提供了一些预定义的委托类型:
Action 委托
用于无返回值的方法:
using System;
class Program
{
static void Main()
{
// Action - 无参数
Action greet = () => Console.WriteLine("Hello");
// Action<T> - 一个参数
Action<string> greet2 = name => Console.WriteLine($"Hello, {name}");
// Action<T1, T2> - 两个参数
Action<string, int> greet3 = (name, age) =>
Console.WriteLine($"{name} is {age} years old");
// 最多支持16个参数: Action<T1, ..., T16>
greet(); // 输出: Hello
greet2("张三"); // 输出: Hello, 张三
greet3("李四", 25); // 输出: 李四 is 25 years old
}
}
Func 委托
用于有返回值的方法:
using System;
class Program
{
static void Main()
{
// Func<TResult> - 无参数,返回 TResult
Func<string> getMessage = () => "Hello World";
// Func<T, TResult> - 一个参数,返回 TResult
Func<int, int> square = x => x * x;
// Func<T1, T2, TResult> - 两个参数,返回 TResult
Func<int, int, int> add = (x, y) => x + y;
// 最后一个类型参数是返回类型
// Func<T1, ..., T16, TResult>
Console.WriteLine(getMessage()); // 输出: Hello World
Console.WriteLine(square(5)); // 输出: 25
Console.WriteLine(add(3, 7)); // 输出: 10
}
}
Predicate 委托
用于返回布尔值的委托:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Predicate<T> 等同于 Func<T, bool>
Predicate<int> isEven = n => n % 2 == 0;
List<int> evens = numbers.FindAll(isEven);
foreach (int n in evens)
{
Console.Write($"{n} "); // 输出: 2 4 6 8 10
}
}
}
Comparison 委托
用于比较两个对象:
using System;
using System.Collections.Generic;
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public override string ToString() => $"{Name}, {Age}";
}
class Program
{
static void Main()
{
List<Person> people = new List<Person>
{
new Person { Name = "张三", Age = 25 },
new Person { Name = "李四", Age = 30 },
new Person { Name = "王五", Age = 20 }
};
// 按年龄排序
Comparison<Person> sortByAge = (p1, p2) => p1.Age.CompareTo(p2.Age);
people.Sort(sortByAge);
foreach (var person in people)
{
Console.WriteLine(person);
}
// 输出:
// 王五, 20
// 张三, 25
// 李四, 30
}
}
实际应用场景
1. 事件处理
委托是 C# 事件系统的基础:
using System;
// 定义事件发布者
class Button
{
public event EventHandler Click;
public void OnClick()
{
Click?.Invoke(this, EventArgs.Empty);
}
}
class Program
{
static void Main()
{
Button button = new Button();
// 使用 Lambda 表达式订阅事件
button.Click += (sender, e) =>
{
Console.WriteLine("按钮被点击了!");
};
button.OnClick();
}
}
2. LINQ 查询
LINQ 大量使用 Lambda 表达式:
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<Person> people = new List<Person>
{
new Person { Name = "张三", Age = 25 },
new Person { Name = "李四", Age = 30 },
new Person { Name = "王五", Age = 20 }
};
// 使用 Lambda 表达式过滤和投影
var youngPeople = people
.Where(p => p.Age < 30) // Lambda: 筛选
.Select(p => p.Name) // Lambda: 投影
.ToList();
foreach (var name in youngPeople)
{
Console.WriteLine(name);
}
// 输出: 张三, 王五
}
}
3. 异步编程
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
// 使用 Lambda 表达式创建异步委托
Func<Task> asyncTask = async () =>
{
await Task.Delay(1000);
Console.WriteLine("异步任务完成");
};
await asyncTask();
// 使用 Lambda 处理异步结果
Func<int, Task<int>> processAsync = async (n) =>
{
await Task.Delay(100);
return n * 2;
};
int result = await processAsync(10);
Console.WriteLine($"结果: {result}"); // 输出: 结果: 20
}
}
4. 回调函数
using System;
class Downloader
{
public delegate void ProgressCallback(int percentage);
public void Download(ProgressCallback callback)
{
for (int i = 0; i <= 100; i += 10)
{
callback?.Invoke(i);
System.Threading.Thread.Sleep(200);
}
}
}
class Program
{
static void Main()
{
Downloader downloader = new Downloader();
// 使用 Lambda 传递回调
downloader.Download(percentage =>
Console.WriteLine($"下载进度: {percentage}%")
);
// 或者使用局部变量
int lastProgress = 0;
downloader.Download(percentage =>
{
if (percentage - lastProgress >= 20)
{
Console.WriteLine($"已达到 {percentage}%");
lastProgress = percentage;
}
});
}
}
5. 策略模式
委托可以实现策略模式:
using System;
// 策略接口(使用委托)
delegate double PricingStrategy(double price);
class Product
{
public string Name { get; set; }
public double Price { get; set; }
public double GetFinalPrice(PricingStrategy strategy)
{
return strategy(Price);
}
}
class Program
{
static void Main()
{
Product product = new Product { Name = "笔记本电脑", Price = 10000 };
// 定义不同的策略
PricingStrategy normalPrice = p => p;
PricingStrategy discount10 = p => p * 0.9;
PricingStrategy discount20 = p => p * 0.8;
PricingStrategy vipPrice = p => p * 0.7;
Console.WriteLine($"原价: {product.GetFinalPrice(normalPrice)}");
Console.WriteLine($"9折: {product.GetFinalPrice(discount10)}");
Console.WriteLine($"8折: {product.GetFinalPrice(discount20)}");
Console.WriteLine($"VIP价: {product.GetFinalPrice(vipPrice)}");
}
}
最佳实践
1. 使用空条件运算符
// 推荐
myDelegate?.Invoke();
// 不推荐
if (myDelegate != null)
{
myDelegate();
}
2. 避免空委托
// 推荐:初始化为空委托
Action<int> handler = _ => { };
handler += n => Console.WriteLine(n);
handler(5);
3. Lambda vs 方法组转换
// 当只传递方法时,使用方法组更简洁
Action<int> handler1 = Console.WriteLine;
// 需要参数转换时使用 Lambda
Action<string> handler2 = s => Console.WriteLine(int.Parse(s));
4. 委托的可读性
// 复杂逻辑使用命名方法而不是 Lambda
Func<int, int, int> complexCalculation = CalculateComplexValue;
static int CalculateComplexValue(int x, int y)
{
// 复杂的计算逻辑
int result = 0;
// ... 更多代码
return result;
}
总结
- 委托提供了类型安全的函数指针机制
- 多播委托允许一个委托调用多个方法
- 匿名方法和 Lambda 表达式提供了更灵活的代码组织方式
- Action 和 Func 是 .NET 提供的最常用的泛型委托
- 委托广泛应用于事件处理、LINQ、异步编程等场景
掌握委托和 Lambda 表达式是编写现代 C# 代码的基础技能!



1016

被折叠的 条评论
为什么被折叠?



