首页 > 编程语言 >C# 中的闭包一个小问题

C# 中的闭包一个小问题

时间:2023-01-15 12:34:02浏览次数:52  
标签:闭包 __ DisplayClass0 C# 问题 _. num Action new

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# 不是的,他是明确有块级作用域的概念,所以不知道在这里又突然表现不同。

标签:闭包,__,DisplayClass0,C#,问题,_.,num,Action,new
From: https://www.cnblogs.com/freesfu/p/17053318.html

相关文章

  • elasticsearch实现基于拼音搜索
    目录1、背景2、安装拼音分词器3、拼音分词器提供的功能4、简单测试一下拼音分词器4.1dsl4.2运行结果5、es中分词器的组成6、自定义一个分词器实现拼音和中文的搜索1、创......
  • docker中离线安装nginx
    注:默认已经安装好docker。 为什么要离线安装?其实离线安装是建立在在线安装的基础上的;因为有可能我们的服务器由于安全问题无法访问外网,自此我们需要将镜像手动上传至服......
  • 关于查询maven依赖排查的问题
    背景公司扫描服务依赖项的时候,发现服务中有引用了logback的包,因版本过低,需要升级才能修复风险。通过maven的DependencyAnalyzer工具,确实找到了一些,排掉后,扫描发现,还存......
  • vbNullChar
    当在VB中声明变量为如下形式,运行时将给szPath分配空间,且初使化值均为:vbNullChar,而不是空格.DimszPathAsString*255szPath=vbNullChar&vbNullChar&vbNullChar&...255......
  • AtCoder Regular Contest 153
    喜提全场独一无二的score!ATC还是很友善的,如果每题等分就寄了A签到B真的是凭着实力不会做的呀。。。太菜了发现两维可以分别做,所以考虑一维的情况,然而并不会对于两......
  • SVN pre-commit钩子检测文件名称合法性
    需求这里题目起的比较大,实际笔者这里的需求,仅仅是检测提交的文件名称以及目录名称是否包含空格而已,不过回头想想也不算大,我能够检测空格,同时也就能检测其他的元素,进而实现......
  • ORB-SLAM2: an Open-Source SLAM system for Monocular,Stereo and RGB-D Camera
    摘要本文提出ORB-SLAM2一个完整的SLAM系统,用于单目,双目以及RGB-D相机,包括地图重用,回环检测以及重定位能力。本系统工作在实时的标准CPU,在更宽泛的环境中,来自于手持的室内......
  • expect远程ssh连接linux终端
    前提条件,需要安装expect,安装方法请参考​​http://tongzidane.blog.163.com/blog/static/5816589220118161127811/​​#!/usr/bin/expect #设置超时时间为60秒 set......
  • 写了个下载程序 支持Socks5代理
    抽了点时间,写了这个示例程序,功能很简单.通过公司代理下载速度可以达到200多KB/s运行界面如下:可惜不支持多线程,以后再改进了.'-------------------------------------------'......
  • 关于前后端分离,后端返回了set-cookie,但是浏览器没有设置成功(未解决,折腾一天了,没找到
    问题描述:Angular,http://localhost:4200Java,http://localhost:8080/test1在前端发起对/test1的请求,test1对HttpServletResponse进行addCookie(newCookie("test1","te......