在 C# 中, 委托与 Lambda 是最常用的功能之一, 诸如多线程, 可迭代类型的 LINQ 操作, 回调, 都是借助委托完成的. 它在 C 语言对应的是函数指针, 在 C++ 中对应的是 std::function
委托 / Delegate
在 C# 中, 委托是一种特殊的类型, 它用来存储一段逻辑(可执行的内容). 例如在声明字段或变量的时候, 我们指定类型为委托, 那么这个字段或变量就可以存下一段逻辑. 在你需要的时候, 就可以直接执行它.
void MethodA()
{
// 这是一个方法, 里面包含一些逻辑
// 这个方法没有参数, 也没有返回值
Console.WriteLine("Hello world");
}
void MethodB()
{
// 声明一个能存储下一段逻辑的变量
// Action 表示无参数无返回值的逻辑
Action someLogic;
// 为其赋值, 可以直接将方法赋值过去
someLogic = MethodA;
// 调用这段逻辑
// 因为它存储的就是 MethodA, 所以等同于调用 MethodA
someLogic.Invoke();
}
事实上, 在使用委托之前, 我们需要定义一个委托类型, 在上述代码中使用的 Action, 就是系统预定义好的一个委托类型. 如果我们需要为带有特定参数, 特定返回值的一段逻辑定义一个类型, 可以使用 delegate
关键字.
// 定义一个无参数, 无返回值委托类型
// 其中, MyDelegate 是委托类型名, public 是访问修饰
public delegate void MyDelegate();
// 定义一个带有 int 参数, 返回值为 string 的委托类型
public delegate string MyDelegate2(int);
// 定义一个带有 int, float 参数, 返回值为 string 的委托类型
public delegate string MyDelegate3(int, float);
// 如果你想, 你也可以为委托类型定义中的参数写上名字
// 这个名字也只是为了可读性, 让用户知道这个参数是做什么用的
// 除此之外, 就么有什么别的用处了
public delegate string MyDelegate4(int value1, float value2);
要使用定义好的委托类型, 和最开始给出的示例代码一样, 只需要把他当成一个普通类型直接使用即可.
// 定义一个 someLogic 变量, 类型为 MyDelegate
// 同时, 为其赋初始值 MethodA
MyDelegate someLogic = MethodA;
你可以通过委托类型来存下任何你想要存下的逻辑, 只要类型和对应逻辑的参数返回值是匹配的即可. 下面是定义一个委托类型, 然后将 Console.WriteLine
这个方法存入变量的完整示例代码.
namespace TestConsole
{
class Program
{
// 定义参数为 string, 无返回值的委托类型
delegate void MyDelegate(string text);
static void Main(string[] args)
{
// 定义变量
MyDelegate someLogic;
// 为其赋值
// 因为 Console.WriteLine 有匹配的重载, 所以可以存入
someLogic = Console.WriteLine;
}
}
}
不仅是诸如 Console.WriteLine
的静态方法可以存入, 非静态的方法也是可以存入的. 下面是一段将实例方法存入变量的完整示例代码.
namespace TestConsole
{
class TestClass
{
public void TestMethod()
{
var x = 1;
var y = 2;
var z = x + y;
System.Console.WriteLine(z);
}
}
delegate void TestDelegate();
class Program
{
static void Main(string[] args)
{
// 创建类型的实例
TestClass ins = new TestClass();
// 定义委托变量
TestDelegate someLogic;
// 将实例的方法存入到委托变量
someLogic = ins.TestMethod;
}
}
}
匿名函数 / Lambda
在上面的例子中, 我们将方法存入了委托变量中, 但如果每一段逻辑都需要定义一个方法, 那就太麻烦了. 使用 Lambda 表达式就是一个很不错的选择.
Action someLogic;
someLogic = () =>
{
Console.WriteLine("Hello world");
};
在上面的代码中, 我们定义了一个无参无返回值的委托变量, 并使用了一个奇怪的语法为其赋值. 这个语法, 就是 Lambda 表达式. 其中括号表示参数, =>
是 lambda 的符号, 后面的大括号是逻辑的内容. 因为无参数, 所以括号内是空的.
Lambda 语句的标准语法如下:
(输入参数) => { <多条语句> }
下面我们以一个执行加法计算的 Lambda 语句为例, 了解 Lambda 语句的具体写法.
// 定义一个传入两个 int 参数, 返回一个 int 结果的委托变量
Func<int, int, int> someLogic;
// 为委托变量赋值
someLogic = (int a, int b) =>
{
return a + b;
};
// 执行委托, 并打印返回值
// 输出内容是 114514
Console.WriteLine(someLogic.Invoke(114000, 514));
Lambda 语句可以理解为一个没有名字的方法, 在上面的代码中, 我们所写的 Lambda 表达式传入两个 int
, 并使用 return
返回他们的相加结果. 这个 Lambda 语句直接被赋值到 someLogic 变量中, 并在后续的代码中, 通过 someLogic 调用刚刚缩写的逻辑.
事实上, 因为编译器知道 someLogic 的委托类型需要传入两个 int
, 所以在后面直接赋值的 Lambda 语句中, 参数的类型是可以省略的. 编译器会自动推导他们的类型.
// 省略参数的类型, 直接写参数名
someLogic = (a, b) =>
{
return a + b;
};
如果 Lambda 语句的传入参数只有一个, 那么括号也是可以被省略的.
// 定义一个传入参数为 int, 无返回值的委托变量
Action<int> someLogic;
// 为其赋值
someLogic = integer =>
{
Console.WriteLine(integer);
};
如果你不需要使用传入的参数, 可以使用 _
将其丢弃:
// 定义一个传入参数为 int, 无返回值的委托变量
Action<int> someLogic;
// 为其赋值
// 这里的表达式没有使用传入的参数
someLogic = _ => Console.WriteLine("Hello world");
如果 Lambda 语句的方法体只有一个语句, 那么大括号是可以被省略的. 此时 Lambda 语句就叫做 Lambda 表达式了.
// 定义一个传入参数为 int, 无返回值的委托变量
Action<int> someLogic;
// 为其赋值
someLogic = integer => Console.WriteLine(integer);
如果你要在 Lambda 中使用 await, 可以在 Lambda 前添加 async 关键字, 使用方式和普通的方法是一样的.
// 定义一个无传入参数, 返回值为 Task<int> 的委托变量
Func<Task<int>> someLogic;
// 为其赋值, 并使用 async Lambda
someLogic = async () =>
{
// 延时 1s
await Task.Delay(1000);
// 返回值
return 114514;
}
如果只是单纯调用一个委托, 那么也可以不需要使用 Invoke
, 直接把他当成方法, 后跟括号和参数即可.
Action<string> someLogic = str => Console.WriteLine(str);
someLogic.Invoke("Hello world");
// 等同于上面的 Invoke
someLogic("Hello world");
捕获 / Capture
在使用 Lambda 语句/表达式的时候, Lambda 可以捕获到外部的变量, 字段, 或属性, 简单来讲就是, 你可以直接在 Lambda 内使用 Lambda 外的任何东西.
读取 Lambda 外的值, 并且 Lambda 能感知到外部对值的变更.
int num = 0;
Action action = () => Console.WriteLine(num);
// 输出 0
action.Invoke();
num += 123;
// 输出 123 (外部对变量变更时, lambda 仍然能输出正确的值)
action.Invoke();
对 Lamda 外部的值进行更改.
int num = 0;
Action action = () => num++;
for (int i = 0; i < 3; i++)
action.Invoke();
// 输出 3
Console.WriteLine(num);
使用 / Usage
下面列举几种使用 Lambda 语句/表达式的例子.
// 一句话启动新线程
new Thread(() =>
{
int i = 0;
while (true)
{
Console.WriteLine(i++);
Thread.Sleep(1000);
}
}).Start();
// 一句话启动新任务
Task.Run(async () =>
{
int i = 0;
while (true)
{
Console.WriteLine(i++);
await Task.Delay(1000);
}
});
// 生成 10 个随机数
List<int> numbers = Enumerable.Range(0, 10)
.Select(_ => Random.Shared.Next())
.ToList();
// 筛选上面随机数中大于 100 的
List<int> filteredNumbers = numbers
.Where(num => num > 100)
.ToList();
编译 / Compilation
Lambda 本质上是一个语法糖, 在编译时, Lambda 会被编译成方法, 或者一个类, 然后其中包含的一个方法.
以下实例, 为了可读性, 均在编译器编译后结果的基础上进行了命名上的优化, 以方便理解.
using System;
public class Program {
static void Main(string[] args) {
int num = 0;
Action action = () => Console.WriteLine(num);
// 输出 0
action.Invoke();
num += 123;
// 输出 123 (外部对变量变更时, lambda 仍然能输出正确的值)
action.Invoke();
}
}
编译后源码:
public class Program
{
[CompilerGenerated]
private sealed class LambdaClass
{
public int num;
internal void LambdaMethod()
{
Console.WriteLine(num);
}
}
private static void Main(string[] args)
{
LambdaClass lambdaInstance = new LambdaClass();
lambdaInstance.num = 0;
Action action = new Action(lambdaInstance.LambdaMethod);
action();
lambdaInstance.num += 123;
action();
}
}
可以看到, 在上面的示例中, Lambda 表达式被编译成了一个类, 并且捕获到的局部变量均变成了类的字段. 而不再是存储在栈上.
由此, 就诞生了一个问题, 当你尝试对一个被捕获的局部变量取地址的时候, 就会报错. 我们没办法取一个位于堆上的对象中. 堆上的对象会被 GC 移动. 或者说, 如果一个局部变量被取地址, 就意味着它需要在栈上, 而存储在栈上的值, 没办法被 Lambda 所捕获.
using System;
public class Program {
int num = 0;
void SomeMethod(string[] args) {
Action action = () => Console.WriteLine(num);
// 输出 0
action.Invoke();
num += 123;
// 输出 123 (外部对变量变更时, lambda 仍然能输出正确的值)
action.Invoke();
}
}
编译后源码:
public class Program
{
private int num = 0;
private void SomeMethod(string[] args)
{
Action action = new Action(LambdaMethod);
action();
num += 123;
action();
}
[CompilerGenerated]
private void LambdaMethod()
{
Console.WriteLine(num);
}
}