编程技术是改变世界的力量。
本站
当前位置:网站首页 > 后端语言 > 正文

C#中如何在匿名函数输出变量,你了解多少?

gowuye 2024-05-16 14:30 3 浏览 0 评论


概述:早在 2005 年,随着 C# 2.0 标准的发布,我们可以通过从当前上下文中捕获变量来将变量传递给匿名委托的正文。2008 年,C# 3.0 为我们带来了 lambda、用户匿名类、LINQ 请求等等。现在是 2017 年 1 月,大多数 C# 开发人员都期待着 C# 7.0 标准的发布,它应该为我们提供一系列新的有用功能。但是,仍有一些旧功能需要修复。这就是为什么有很多方法可以搬起石头砸自己的脚。今天我们将讨论其中之一,它与 C# 中匿名函数主体中一个非常不明显的变量捕获机制有关。介绍正如我上面所说,我们将讨论 C# 中匿名函数主体中变量捕获机制的特殊性。我应该提前警告一下,这篇文章将包含

早在 2005 年,随着 C# 2.0 标准的发布,我们可以通过从当前上下文中捕获变量来将变量传递给匿名委托的正文。2008 年,C# 3.0 为我们带来了 lambda、用户匿名类、LINQ 请求等等。现在是 2017 年 1 月,大多数 C# 开发人员都期待着 C# 7.0 标准的发布,它应该为我们提供一系列新的有用功能。但是,仍有一些旧功能需要修复。这就是为什么有很多方法可以搬起石头砸自己的脚。今天我们将讨论其中之一,它与 C# 中匿名函数主体中一个非常不明显的变量捕获机制有关。

介绍

正如我上面所说,我们将讨论 C# 中匿名函数主体中变量捕获机制的特殊性。我应该提前警告一下,这篇文章将包含大量的技术细节,但我希望有经验的程序员和初学者都会发现我的文章有趣且易于理解。

但说得够多了。我将给你一个简单的代码示例,你应该告诉,控制台中将打印什么。

所以,我们开始吧。

void Foo()
{
var actions = new List<Action>();
for (int i = 0; i < 10; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach(var a in actions)
{
a();
}
}

现在请注意,这是答案。控制台将打印数字 10 十次。

10 
10
10
10
10
10
10
10
10
10

这篇文章是为那些不这么认为的人准备的。让我们试着梳理一下,这种行为的原因是什么。

为什么会这样?

在类中声明匿名函数(可以是匿名委托或 lambda)后,将在编译期间声明另一个容器类,该类包含所有捕获变量的字段和一个包含匿名函数主体的方法。上面给出的代码片段的程序的反汇编结构如下:

在本例中,此片段中的 Foo 方法在 Program 类中声明。编译器为 lambda () => Console.WriteLine(i) 生成了一个容器类_c__DisplayClass1_0,并在类容器内部生成了一个字段 i,该字段具有一个具有相同名称和方法 b__0 的捕获变量,_其中包含 lambda 的主体。

让我们考虑一下 b__0 方法(lambda 正文)的反汇编 IL 代码以及我的评论:

.method assembly hidebysig instance void '<Foo>b__0'() cil managed
{
.maxstack 8
// Puts the current class item (equivalent to 'this')
// to the top of the stack.
// It is necessary for the access to
// the fields of the current class.
IL_0000: ldarg.0

// Puts the value of the 'i' field to the top of the stack
// of the current class instance
IL_0001: ldfld int32
TestSolution.Program/'<>c__DisplayClass1_0'::i

// Calls a method to output the string to the console.
// Passes values from the stack as arguments.
IL_0006: call void [mscorlib]System.Console::WriteLine(int32)

// Exits the method.
IL_000b: ret
}

没错,这正是我们在 lambda 中所做的,没有魔法。让我们继续。

众所周知,int 类型(全称 Int32)是一个结构体,这意味着它通过值传递,而不是通过引用传递。

在创建容器类实例期间,应复制 i 变量的值(根据逻辑)。如果您错误地回答了我在文章开头的问题,那么您很可能期望容器将在代码中声明 lambda 之前创建。

实际上,在 Foo 方法中编译后根本不会创建 i 变量。取而代之的是,将创建容器类 c__DisplayClass1_0 的实例,并且其字段将使用 0 而不是 i 变量进行初始化。此外,在我们使用局部变量 i 的所有片段中,都会有一个使用容器类的字段。

重要的一点是,容器类的实例是在循环之前创建的,因为它的字段 i 将在循环中用作迭代器。

因此,我们为 for 循环的所有迭代获得一个容器类的实例。在每次迭代时向_操作_列表添加一个新的 lambda,我们实际上添加了对之前创建的容器类实例的相同引用。因此,当我们使用 foreach 循环遍历_操作_列表的所有项时,它们都具有相同的容器类实例。我们考虑到 for 循环在每次迭代后(甚至在最后一次迭代之后)递增迭代器的值,那么在执行 for 循环后,退出循环后容器类内 i 字段的值等于 10。

您可以通过查看 Foo 方法的反汇编 IL 代码来确保它(带有我的评论):

.method private hidebysig instance void Foo() cil managed
{
.maxstack 3

// -========== DECLARATION OF LOCAL VARIABLES ==========-
.locals init(
// A list of 'actions'.
[0] class [mscorlib]System.Collections.Generic.List'1
<class [mscorlib]System.Action> actions,

// A container class for the lambda.
[1] class TestSolution.Program/
'<>c__DisplayClass1_0' 'CSlt;>8__locals0',

// A technical variable V_2 is necessary for temporary
// storing the results of the addition operation.
[2] int32 V_2,

// Technical variable V_3 is necessary for storing
// the enumerator of the 'actions' list during
// the iteration of the 'foreach' loop.
[3] valuetype
[mscorlib]System.Collections.Generic.List'1/Enumerator<class
[mscorlib]System.Action>
V_3)

// -================= INITIALIZATION =================-
// An instance of the Actions list is created and assigned to the
// 'actions' variable.
IL_0000: newobj instance void class
[mscorlib]System.Collections.Generic.List'1<class
[mscorlib]System.Action>::.ctor()
IL_0005: stloc.0

// An instance of the container class is created
// and assigned to a corresponding local variable
IL_0006: newobj instance void
TestSolution.Program/'<>c__DisplayClass1_0'::.ctor()
IL_000b: stloc.1

// A reference of the container class is loaded to the stack.
IL_000c: ldloc.1

// Number 0 is loaded to the stack.
IL_000d: ldc.i4.0

// 0 is assigned to the 'i' field of the previous
// object on the stack (an instance of a container class).
IL_000e: stfld int32
TestSolution.Program/'<>c__DisplayClass1_0'::i



// -================= THE FOR LOOP =================-
// Jumps to the command IL_0037.
IL_0013: br.s IL_0037

// The references of the 'actions'
// list and an instance of the container class
// are loaded to the stack.
IL_0015: ldloc.0
IL_0016: ldloc.1

// The reference to the 'Foo' method of the container class
// is loaded to the stack.
IL_0017: ldftn instance void
TestSolution.Program/'<>c__DisplayClass1_0'::'<Foo>b__0'()

// An instance of the 'Action' class is created and the reference
// to the 'Foo' method of the container class is passed into it.
IL_001d: newobj instance void
[mscorlib]System.Action::.ctor(object, native int)

// The method 'Add' is called for the 'actions' list
// by adding an instance of the 'Action' class.
IL_0022: callvirt instance void class
[mscorlib]System.Collections.Generic.List'1<class
[mscorlib]System.Action>::Add(!0)

// The value of the 'i' field of the instance of a container class
// is loaded to the stack.
IL_0027: ldloc.1
IL_0028: ldfld int32
TestSolution.Program/'<>c__DisplayClass1_0'::i

// The value of the 'i' field is assigned
// to the technical variable 'V_2'.
IL_002d: stloc.2

// The reference to the instance of a container class and the value
// of a technical variable 'V_2' is loaded to the stack.
IL_002e: ldloc.1
IL_002f: ldloc.2

// 1 is loaded to the stack.
IL_0030: ldc.i4.1

// It adds two first values on the stack
// and assigns them to the third.
IL_0031: add

// The result of the addition is assigned to the 'i' field
// (in fact, it is an increment)
IL_0032: stfld int32
TestSolution.Program/'<>c__DisplayClass1_0'::i

// The value of the 'i' field of the container class instance
// is loaded to the stack.
IL_0037: ldloc.1
IL_0038: ldfld int32
TestSolution.Program/'<>c__DisplayClass1_0'::i

// 10 is loaded to the stack.
IL_003d: ldc.i4.s 10

// If the value of the 'i' field is less than 10,
// it jumps to the command IL_0015.
IL_003f: blt.s IL_0015


// -================= THE FOREACH LOOP =================-
//// The reference to the 'actions' list is loaded to the stack.
IL_0041: ldloc.0

// The technical variable V_3 is assigned with the result
// of the 'GetEnumerator' method of the 'actions' list.
IL_0042: callvirt instance valuetype
[mscorlib]System.Collections.Generic.List'1/Enumerator<!0> class
[mscorlib]System.Collections.Generic.List'1<class
[mscorlib]System.Action>::GetEnumerator()
IL_0047: stloc.3

// The initialization of the try block
// (the foreach loop is converted to
// the try-finally construct)
.try
{
// Jumps to the command IL_0056.
IL_0048: br.s IL_0056

// Calls get_Current method of the V_3 variable.
// The result is written to the stack.
// (A reference to the Action object in the current iteration).
IL_004a: ldloca.s V_3
IL_004c: call instance !0 valuetype
[mscorlib]System.Collections.Generic.List'1/Enumerator<class
[mscorlib]System.Action>::get_Current()

// Calls the Invoke method of the Action
// object in the current iteration
IL_0051: callvirt instance void
[mscorlib]System.Action::Invoke()

// Calls MoveNext method of the V_3 variable.
// The result is written to the stack.
IL_0056: ldloca.s V_3
IL_0058: call instance bool valuetype
[mscorlib]System.Collections.Generic.List'1/Enumerator<class
[mscorlib]System.Action>::MoveNext()

// If the result of the MoveNext method is not ,
// then it jumps to the IL_004a command.
IL_005d: brtrue.s IL_004a

// Finishes the try block execution and jumps to finally.
IL_005f: leave.s IL_006f
} // end .try
finally
{
// Calls the Dispose method of the V_3 variable.
IL_0061: ldloca.s V_3
IL_0063: constrained. Valuetype
[mscorlib]System.Collections.Generic.List'1/Enumerator<class
[mscorlib]System.Action>

IL_0069: callvirt instance void
[mscorlib]System.IDisposable::Dispose()

// Finishes the execution of the finally block.
IL_006e: endfinally
}

// Finishes the execution of the current method.
IL_006f: ret
}

结论

Microsoft 的人说这是一个功能,而不是一个错误,这种行为是故意的,旨在提高程序的性能。您将通过此链接找到更多信息。实际上,它会导致新手开发人员的错误和困惑。

一个有趣的事实是,foreach 循环在 C# 5.0 标准之前具有相同的行为。Microsoft 被关于错误跟踪器中非直观行为的抱怨轰炸,但随着 C# 5.0 标准的发布,通过在每个循环迭代中声明迭代器变量来改变这种行为,而不是在编译阶段之前,但对于所有其他结构,类似的行为保持不变。有关详细信息,请参阅_“中断性变更_”部分中的链接。

你可能会问如何避免这样的错误?其实答案很简单。您需要跟踪捕获的位置和变量。请记住,容器类将在您声明要捕获的变量的位置创建。如果捕获发生在循环的主体中,并且变量是在循环外部声明的,则有必要在循环主体内将其重新分配给新的局部变量。开头给出的示例的正确版本可以如下所示:

void Foo()
{
var actions = new List<Action>();
for (int i = 0; i < 10; i++)
{
var index = i; // <=
actions.Add(() => Console.WriteLine(index));
}
foreach(var a in actions)
{
a();
}
}

如果执行此代码,控制台将显示从 0 到 9 的数字,如预期所示:

0 
1
2
3
4
5
6
7
8
9

从此示例中查看 for 循环的 IL 代码,我们将看到在循环的每次迭代中都会创建一个容器类的实例。因此,操作列表将包含对具有正确迭代器值的各种实例的引用。

// -================= THE FOR LOOP =================-
// Jumps to the command IL_002d.
IL_0008: br.s IL_002d
// Creates an instance of a container class
// and loads the reference to the stack.
IL_000a: newobj instance void
TestSolution.Program/'<>c__DisplayClass1_0'::.ctor()
IL_000f: stloc.2
IL_0010: ldloc.2
// Assigns the 'index' field in the container class
// with a value 'i'.
IL_0011: ldloc.1
IL_0012: stfld int32
TestSolution.Program/'<>c__DisplayClass1_0'::index
// Creates an instance of the 'Action' class with a reference to
// the method of a container class and add it to the 'actions' list.
IL_0017: ldloc.0
IL_0018: ldloc.2
IL_0019: ldftn instance void
TestSolution.Program/'<>c__DisplayClass1_0'::'<Foo>b__0'()
IL_001f: newobj instance void
[mscorlib]System.Action::.ctor(object, native int)
IL_0024: callvirt instance void class
[mscorlib]System.Collections.Generic.List'1<class
[mscorlib]System.Action>::Add(!0)

// Performs the increment to the 'i' variable
IL_0029: ldloc.1
IL_002a: ldc.i4.1
IL_002b: add
IL_002c: stloc.1
// Loads the value of the 'i' variable to the stack
// This time it is not in the container class
IL_002d: ldloc.1
// Compares the value of the variable 'i' with 10.
// If 'i < 10', then jumps to the command IL_000a.
IL_002e: ldc.i4.s 10
IL_0030: blt.s IL_000a

最后,让我提醒您,我们都是人类,我们都会犯错误,这就是为什么在寻找错误和错别字时只希望人为因素是不合逻辑的,并且通常是漫长且资源密集型的。因此,使用技术解决方案来检测代码中的错误始终是一个好主意。机器不会感到疲倦,并且完成工作的速度要快得多。


如果你喜欢我的文章,请给我一个赞!谢谢

相关推荐

Nginx 响应提速10倍,你需要知道的缓存性能优化——FastCGI调优
Nginx 响应提速10倍,你需要知道的缓存性能优化——FastCGI调优

Nginx缓存优化是帮助大家提升网站性能的重要操作之一,proxy_cache主要用于反向代理时,对后端内容源服务器进行缓存;fastcgi_cache主要用于...

2024-05-20 14:44 gowuye

王者荣耀天魔缭乱和逐梦之音返场活动地址 3月22日开启返场活动
王者荣耀天魔缭乱和逐梦之音返场活动地址 3月22日开启返场活动

王者荣耀官方终于确定了天魔缭乱和逐梦之音的返场活动,这让不少小伙伴乐开了花,返场活动将会在3月22日开启,下面就带来王者荣耀天魔缭乱和逐梦之音返场活动地址!王者...

2024-05-20 14:44 gowuye

常见的嵌入式web服务器有哪些?

嵌入式WEB服务器常见的有:Lighttpd,Shttpd,Thttpd,Boa,Mini_httpd,Appweb,Goahead。Lighttpd地址:http://www.light...

简述几款常见的嵌入式web服务器
简述几款常见的嵌入式web服务器

嵌入式web服务器,是web服务器当中的一种,是基于嵌入式系统而实现的web服务器。指的是在嵌入式系统(通俗点就是单片机系统)上实现的一个web服务器,可以通过...

2024-05-20 14:44 gowuye

教你如何利用fastcgi_cache缓存加速WordPress

在使用nginx缓存之前,必须在nginx里面加载专门的模块,这个模块叫做ngx_cache_purge。添加ngx_cache_purge模块下载ngx_cache_purge模块ngx_cache...

扫描WordPress漏洞

检测已知漏洞WPScan是一款广泛使用的WordPress安全扫描工具,它的一项重要功能是检测已知漏洞。在这篇文章中,我们将深入探讨WPScan如何检测已知漏洞,并结合实际示例,帮助读者更好地理解和应...

消灭 Bug!推荐几个给力的开源 Bug 跟踪工具
消灭 Bug!推荐几个给力的开源 Bug 跟踪工具

在这个充满bug的世界里,最遥远的距离不是生与死,而是你亲手制造的bug就在你眼前,你却怎么都找不到它。因此本文准备了7款优秀的开源bug跟踪系...

2024-05-20 14:43 gowuye

生物信息分析入门全攻略

生物信息学是生命科学研究的重大前沿领域,未来将占据生命科学研究的半壁江山。已经有越来越多的小伙伴投入到生物信息的学习中,但是入门难、深入慢、摸不到方向等都成为持续学习的拦路虎。本文根据生物信息技术大牛...

elkb实践经验,再赠送一套复杂的配置文件
elkb实践经验,再赠送一套复杂的配置文件

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。宝剑锋从磨砺出,梅花香自苦寒来。诗人白居易,三月下江南,看到沿路开放的桃花,心潮澎湃...

2024-05-20 14:43 gowuye

超详细从0到1 搭建ELK监控
超详细从0到1 搭建ELK监控

监控分类?Metrics用于记录可聚合的数据。例如,1、队列的当前深度可被定义为一个度量值,在元素入队或出队时被更新;HTTP请求个数可被定义为一个计数器,...

2024-05-20 14:42 gowuye

嵌入式开发 之Web配置页面开发
嵌入式开发 之Web配置页面开发

1.PHP是最好的语言??开发动态页面首选的语言是PHP,村村不能在这里忽悠人,如果你的硬件性能允许切略懂PHP,看到这里就可以退出了。本文面向的受众是Linu...

2024-05-20 14:42 gowuye

Python开发一个网站目录扫描工具用来检测网站是否有漏洞?
Python开发一个网站目录扫描工具用来检测网站是否有漏洞?

开发一个网站目录扫描工具是用来检测网站是否有非法目录请求的一个常见需求之一,我们要通过这个扫描工具来找到通过某个域名可以访问到的网站路径,可能对于有些系统来讲,...

2024-05-20 14:42 gowuye

创建一个类似Youtube的Id——使用PHP/Python/JS/Java/SQL

id通常都是用数字,不巧的是只有10个数字来使用,所以如果你有很多的记录,id往往变得非常冗长。当然对于计算机来说无所谓,但我们更希望id尽可能短。所以我们如何能使id变短?我们可以利用字母让它们附加...

快速云:有助于移动应用安全开发的五条妙计
快速云:有助于移动应用安全开发的五条妙计

许多企业不断地向其开发团队提供培训。但是某些漏洞,如早在十多年前就发现的SQL注入,如今仍广泛存在于各种应用中。因而,安全培训永不过时。在开发移动应用时,开发者...

2024-05-20 14:41 gowuye

洛杉矶国际电影节最佳动画短片奖影片《G’DAY》正式全网上映
洛杉矶国际电影节最佳动画短片奖影片《G’DAY》正式全网上映

7月2日,由M&CSaatchi创作,由深受好评的澳大利亚导演迈克尔·格雷西执导的动画短片《G’day》,正式在全网上映。该影片因其出色的创意赢得了洛...

2024-05-20 14:41 gowuye

取消回复欢迎 发表评论: