C#中闭包的实现和注意事项详解
作者:啸猫先生
闭包的定义
闭包并不是某一个语言中特有的概念,在主流的编程语言中都有这个特性。闭包可以让一个内部方法可以访问它所在外部方法中的变量,并可以对变量的值进行修改,即使在外部方法的生命周期已经结束后。
不知道在看完上面这一串定义之后,可以明白闭包是个什么东西吗,反正我在一开始看完是不知道这是在说什么,不过没关系,我们可以先往下具体看闭包的一个例子。
C#中闭包的实现
下面是一个最简单的闭包,这里使用了委托和Lambda表达式,不过使用局部函数也是一样的效果,这两者之间的区别我们之后去讨论,这里就以使用委托的版本来看一下后台干了什么。
class Program
{
// 使用委托和 Lambda 表达式
static void Main(string[] args)
{
int num = 0;
Action test = () =>
{
num++;
Console.WriteLine(num);
};
test();
}
}
class Program
{
// 使用局部函数
static void Main(string[] args)
{
int num = 0;
Test();
return;
void Test()
{
num++;
Console.WriteLine(num);
}
}
}
我们看构建后的低级别C#可以看出来,在Main方法中new了一个类,这个类中有一个num的成员变量,我们形成闭包的方法实际操作的是这个成员变量,这就解释了为什么外部方法的生命周期结束后,仍然可以访问到在其定义的变量。

闭包的注意事项
Lambda表达式和局部方法
我们先看下面这个场景,在这个场景我们使用了一个类中的某个方法并想要在完成后执行一个回调。
这里使用Lambda表达式和使用局部函数有两种情况,一是会捕获封闭范围内的变量时,如下面这段代码,这时使用局部函数和使用Lambda表达式一样,都会形成一个闭包,并使用类的成员变量来实现,如我在解释C#中闭包的实现时举得那个例子。
class Program
{
static void Main(string[] args)
{
int num = 0;
TestClass test = new TestClass();
for (int i = 0; i < 3; i++)
{
test.DoSomething(() =>
{
num++;
Console.WriteLine($"DoSomething finish {num}");
});
}
}
}
class TestClass
{
public void DoSomething(Action callback)
{
Console.WriteLine("DoSomething");
// 执行回调
callback.Invoke();
}
}
但是如果没有捕获任何变量时,如下面这种情况,可以看到使用局部函数的方式没有形成闭包,局部函数最终变成了一个静态函数。而使用委托和 Lambda 表达式的尽管没有捕获任何变量,但还是创建了一个类,虽然这个类只会实例化一次。
class Program
{
// 使用局部函数,且没有捕获封闭范围内的变量
static void Main(string[] args)
{
TestClass test = new TestClass();
for (int i = 0; i < 3; i++)
{
test.DoSomething(DoSomethingCallBack);
}
return;
void DoSomethingCallBack()
{
Console.WriteLine($"DoSomething finish");
}
}
}
class Program
{
// 使用委托和 Lambda 表达式,且没有捕获封闭范围内的变量
static void Main(string[] args)
{
TestClass test = new TestClass();
for (int i = 0; i < 3; i++)
{
test.DoSomething(() =>
{
Console.WriteLine("DoSomething finish");
});
}
}
}


配合for循环返回多个函数
闭包和for循环这也是一个经典的错误了,让我们来看下面这段代码,这里乍一看没什么问题,我们预期输出的结果是0,1。
class Program
{
static void Main(string[] args)
{
int count = 2;
Action[] fun = CreateActions(count);
for (int i = 0; i < count; i++)
{
fun[i].Invoke();
}
return;
Action[] CreateActions(int count)
{
Action[] actions = new Action[count];
for (int i = 0; i < count; i++)
{
actions[i] = () => Console.WriteLine(i);
}
return actions;
}
}
}
但是我们实际运行一下,可以发现输出的结果是2,2。

这里我们看编译之后的代码,可以看出我们本质操控的其实是同一个<>c__DisplayClass0_0对象的成员变量i,所以自然输出都都是i自增之后的值。

这里改起来也比较容易,只需将for循环改成下面这样就行了。
for (int i = 0; i < count; i++)
{
int j = i;
actions[i] = () => Console.WriteLine(j);
}
这里要注意,int j = i;一定要在for循环里面,这样闭包才会创建3个不同的对象。如果像下面这样写,本质和最开始没有太大区别,还是操纵着一个对象。
int j;
for (int i = 0; i < count; i++)
{
j = i;
actions[i] = () => Console.WriteLine(j);
}
以上就是C#中闭包的实现和注意事项详解的详细内容,更多关于C#闭包实现和注意事项的资料请关注脚本之家其它相关文章!
