当前位置: 首页 > news >正文

C# 中的闭包一个小问题

using System;

var funs = new Action[10];
for (var i = 0; i < 10; i++)
    funs[i] = () => Console.WriteLine(i);

foreach (var fn in funs)
    fn();

猜测这段代码运行结果 1-9,实际运行结果为全部的 10 在 SharpLab 中查看

为什么呢,看反编译以后的代码是什么样子的

// 这里删除了一些不必要的代码引入和声明,为了减少篇幅

[CompilerGenerated]
internal class Program
{
    [CompilerGenerated]
    private sealed class <>c__DisplayClass0_0
    {
        public int i;

        internal void <<Main>$>b__0()
        {
            Console.WriteLine(i);
        }
    }

    private static void <Main>$(string[] args)
    {
        Action[] array = new Action[10];
        <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
        <>c__DisplayClass0_.i = 0;
        while (<>c__DisplayClass0_.i < 10)
        {
            array[<>c__DisplayClass0_.i] = new Action(<>c__DisplayClass0_.<<Main>$>b__0);
            <>c__DisplayClass0_.i++;
        }
        Action[] array2 = array;
        int num = 0;
        while (num < array2.Length)
        {
            Action action = array2[num];
            action();
            num++;
        }
    }
}

请看 <Main>$ 函数中的代码,让我们看看他编译以后是如何对作用域引用的变量进行捕获的。

<>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
<>c__DisplayClass0_.i = 0;
while (<>c__DisplayClass0_.i < 10)
{
    array[<>c__DisplayClass0_.i] = new Action(<>c__DisplayClass0_.<<Main>$>b__0);
    <>c__DisplayClass0_.i++;
}

实际主要就是 Main 函数中的这段代码,编译器创建了一个类 <>c__DisplayClass0_0 来捕获 Action 引用的变量,但是该类的实例是在循环外面,所以的 Action 都共享同样一个对象。所以解决这个问题十分简单,只需要让编译每次循环是都创建一个 <>c__DisplayClass0_0 类的实例对象就可以了。

有两种方法,我们先看第一种

// 这里只给出循环的代码
// loop
for (var i = 0; i < 10; i++)
{
    var num = i; # 在循环内声明变量,明确指明作用范围,指示编译器
    funs[i] = () => Console.WriteLine(num);
}
// end

然后我们看看反编译后循环类的代码

while (num < 10)
{
    <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
    <>c__DisplayClass0_.num = num;
    array[num] = new Action(<>c__DisplayClass0_.<<Main>$>b__0);
    num++;
}

ok,没问题

第二种方法

foreach (var i in Enumerable.Range(0, 10))
{
    var num = i; // 在循环内声明变量,明确指明作用范围,指示编译器
    funs[i] = () => Console.WriteLine(num);
}
// end

这里是借助编译器对 foreach 的处理,所以反编译后的代码有些不同,下面是完整的 Main 函数中的内容

// Main 
Action[] array = new Action[10];
IEnumerator<int> enumerator = Enumerable.Range(0, 10).GetEnumerator();
try
{
    while (enumerator.MoveNext())
    {
        <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0(); // 重要的还是在循环中创建
        <>c__DisplayClass0_.i = enumerator.Current;
        array[<>c__DisplayClass0_.i] = new Action(<>c__DisplayClass0_.<<Main>$>b__0);
    }
}
finally
{
    if (enumerator != null)
    {
        enumerator.Dispose();
    }
}
Action[] array2 = array;
int num = 0;
while (num < array2.Length)
{
    Action action = array2[num];
    action();
    num++;
}

// end Main

因为基本我日常的循环都被 foreach 和 LINQ 取代了,所以我从来没有发现过这个问题,直到我遇上了。我认为这个很违反直觉,我知道在其他语言中也有类似的行为,但是在 C# 中我认为他确实是违法直觉的,我不知道这算不算一个 bug。

拿 JS 和 Py 举例,他们两个(JS 在 es6 以前使用没有 let 和 const 的时代)根本就没有块级作用域的概念,只有函数作用域,所以这样的表现能够理解。而 C# 不是的,他是明确有块级作用域的概念,所以不知道在这里又突然表现不同。

相关文章:

  • 南京电商网站建设/宁波seo优化定制
  • 新乡新手学做网站/推广普通话宣传语手抄报
  • 池州网站建设制作报价方案/设计公司
  • 东莞找做网站的/域名注册网站系统
  • 做平台网站怎么做的/友链交换平台源码
  • 个人怎么申请域名/百度关键词网站排名优化软件
  • 《Buildozer打包实战指南》第二节 安装Kivy和Buildozer
  • 达梦数据库导入dmp文件
  • linux基功系列之man帮助命令实战
  • Transformer模型详解相关了解
  • Eclipse 连接 SQL Server 数据库教程
  • 实时更新的github hosts地址
  • 一周技术学习笔记(第97期)-掌握DDD不是想象的那么容易吗?
  • 计算机基础(六):静态链接与动态链接
  • Tslib配置文件ts.conf
  • 2023年1月16日--2023年1月22日(osg+glsl+socket+ue)
  • 人类从未如此需要知识 知识从未如此昂贵
  • SpringCache之@CachePut注解的使用说明