利用Reflector把"闭包"看清楚

今天老赵在园子里发了一篇文章"警惕匿名方法造成的变量共享",立即引起了大家的广泛关注(老赵就是园子的"人气天王",呵呵),而且这个问题园子里也有其它几篇文章做了研究
比如"闭包""《你不常用的c#之三》:Action 之怪状 "

如果只是停留在c#2.0/3.0的"简捷且优雅"的代码风格上,初 学者确实难理解这个"怪"现象,前二天买了本anytao的“你必须知道的.net”,里面提供了一种研究这类表面"怪"现象的基本方法--IL分析,并 推荐了大名鼎鼎的反编译工具"Reflector",下面利用这个工具对其分析一二(高手就不必看了,权当给初学者一些参考)

原始代码一(摘自"《你不常用的c#之三》:Action 之怪状"一文):


代码1

using System;
using System.Collections.Generic;

namespace ConsoleTest
{
    class Program
    {
        static void Main(string[] args)
        {
            List<Action> ls = new List<Action>();
            for (int i = 0; i < 10; i++)
            {                
                ls.Add(() => Console.WriteLine(i));                
            }

foreach (Action action in ls)
            {
                action();
            }
            System.Console.Read();
        }       
    }

}

结果:一连输出了10行完全相同的"10"(可能并没有按代码编写者的"意图",输出0到9),why?

打开Relector,先做一些设置:打开"View"菜单-->选择"Options",先去掉Show PDB symbols前的勾,然后把Optimization后的下拉框改为".Net 1.0"(众多的"语法糖",比如匿名方法,扩展方法等都是在1.0版本以后出现的,这样设置的目的是去掉这些华丽的外衣,直接反应出原始的c#代码),刚才的代码经过反编译后,大概如下:

[CompilerGenerated]

private
 
sealed
 
class
 
<>
c__DisplayClass2
{
    
//
 Fields

public
 
int
 i;

//
 Methods

public
 
void
 
<
Main
>
b__0()
    {
        Console.WriteLine(
this
.i);
    }
}

private
 
static
 
void
 Main(
string
[] args)
{
    List
<
Action
>
 list 
=
 
new
 List
<
Action
>
();
    Action item 
=
 
null
;
    
<>c__DisplayClass2 class2 = new <>
c__DisplayClass2();
    class2.i = 0
;
    while (class2.i < 10
)
    {
        if (item == null
)
        {
            item = new Action(class2.<Main>
b__0);
        }
        list.Add(item);
        class2.i++
;
    }
    
foreach
 (Action action2 
in
 list)
    {
        action2();
    }
    Console.Read();
}

可以看出,
1.编译器自动生成了一个密封类:<>c__DisplayClass2,里面有一个公有字段i,以及一个公共方法<Main>b__0()--用来输出i
2. 再看Main方法中的高亮部分,自始至终,<>c__DisplayClass2就只生成了一个实例class2,至于下面的while里变 来变去,也只不过在改变i这个变量(也就是实例class2的成员i),而我们知道“类(class)”是引用类型,实际上class2不过是个引用而 已,所以每次用new Action(class2.<Main>b__0)生成item,再list.Add(item)进去后,每个item调用的都是同一个引 用,因此最终一连输出10行相同的结果--即数字10,也就是理所当然了

把代码1,稍作修改,如下:

using
 System;

using
 System.Collections.Generic;

namespace
 ConsoleTest
{
    
class
 Program
    {
        
static
 
void
 Main(
string
[] args)
        {
            List
<
Action
>
 ls 
=
 
new
 List
<
Action
>
();
            
for
 (
int
 i 
=
 
0
; i 
<
 
10
; i
++
)
            {
                
int lp =
 i;
                ls.Add(() 
=>
 Console.WriteLine(lp));                
            }

foreach
 (Action action 
in
 ls)
            {
                action();
            }
            System.Console.Read();
        }       
    }

}

即在循环内部用一个临时变量lp做了一个中转,这次运行的结果,屏幕上输出了0-9共10行不相同的结果

why?

还是用Reflector来看看到底最终的代码是啥?

[CompilerGenerated]

private
 
sealed
 
class
 
<>
c__DisplayClass1
{
    
//
 Fields

public
 
int
 tp;

//
 Methods

public
 
void
 
<
Main
>
b__0()
    {
        Console.WriteLine(
this
.tp);
    }
}

private
 
static
 
void
 Main(
string
[] args)
{
    List
<
Action
>
 list 
=
 
new
 List
<
Action
>
();
    
for (int i = 0; i < 10; i++
)
    {
        <>c__DisplayClass1 class2 = new <>
c__DisplayClass1();
        class2.tp =
 i;
        list.Add(new Action(class2.<Main>
b__0));
    }
    
foreach
 (Action action 
in
 list)
    {
        action();
    }
    Console.Read();
}

同样,编译器还是自动为我们生成了一个密封类,这一点跟代码1 反编译后的一样,关注一下高亮部分,这回<>c__DisplayClass1 class2 = new <>c__DisplayClass1();是放在循环里写的,也就是说10次外循环走下来,一共创建了10个不同的 c__DisplayClass1()实例,剩下的就不用多说了,看明白了吧

关于对于这个现象,个人觉得老赵的建议很好:委托创建完后,即时使用--no problem!(其实代码1也可以改成这样)


代码1修改后

using System;
using System.Collections.Generic;
namespace ConsoleTest
{
    class Program
    {
        static void Main(string[] args)
        {
            List<Action> ls = new List<Action>();
            for (int i = 0; i < 10; i++)
            {                
                ls.Add(() => Console.WriteLine(i));
                ls[i]();
            }
           
            System.Console.Read();
        }       
    }  
}

结果正常,输出0到9,再一次验证了"立即使用"是没问题的,但如果不是立即使用,就得多想想了

最后,其实本文所说的现象老赵在文中已经讲得很明白了,我在这里只不过向初学者推荐了一下反编译的基本分析方法(当然你如果懂IL的话,可以分析得更透),很多情况下,光看程序表面的现象,是很难想明白的,利用一些工具,找到表象下的本质相对更容易把握。

作者:
菩提树下的杨过

出处:
http://yjmyzz.cnblogs.com

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

时间: 2024-10-22 06:19:31

利用Reflector把"闭包"看清楚的相关文章

javascript深入理解js闭包(看了挺多的,感觉这篇比较透彻)

闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. 一.变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域. 变量的作用域无非就是两种:全局变量和局部变量. Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量. Js代码 var n=999; function f1(){ alert(n); } f1(); // 999 另一方面,在函数外部自然无法读取函数内的局部变量. Js代码 function

(原创)JS闭包看代码理解

<html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>JS闭包</title> <script type="text/javascript" src="

利用js闭包获取索引号

以tab选项卡效果为例: 网页中的选项卡效果 如图. 在鼠标点击规则标题的时候下面内容就会切换成规则的内容,这就是一个选项卡切换效果了. 标题和其内容是一一对应的. 在点击的时候就需要得到当前点击的索引号,用jquery的话可以直接通过.index()获取当时元素的索引值,那么用js呢?下面我们来利用js的闭包来实现获取索引. 页面代码如下: <!DOCTYPE html><html lang="en"><head>    <meta char

javascript使用闭包模拟私有属性和方法

最近因为做了一个项目,其中涉及到了js的静态方法和私有方法,这个概念在其语言里面是很常见的,很多语言都有static这个关键字,只要在一个类的前面加上static就表示申明了一个静态类,但是javascript在面向对象的方面没有那么多的特征,他没有专门的static关键字,也没有其他语言的private这样的访问修饰符.要做到这一点就必须使用js自己的一些特性来变相的完成. 首先javascript里面有一个高级特性叫闭包,简单的说js的闭包可以理解成是一种现象或者特性,一般出现在两个函数嵌套

谈谈我对闭包知识的深刻理解

在javascript中闭包应该是最难理解的一部分内容.在我看来闭包就是和作用域之间的联系. 1.首先我们来了解一下javascript中的作用域知识. javascript中的作用域其实就指的函数作用域,因为只有函数在javascript中才能形成区域范围.而函数作用域有一下特点. 1.1 函数能访问到外部的变量.案例一: var num = 123; function fn() { console.log(num);//输出的值为123 } fn(); 1.2 函数内的变量不能被外部访问到.

Swift函数和闭包

函数 Swift 使用func关键字声明函数: 1 func greet (name: String, day: String) -> String { 2 return "Hello \(name), today is \(day)." 3 } 4 greet ("Bob", "Tuesday") 通过元组(Tuple)返回多个值: 1 func getGasPrices () -> (Double, Double, Double)

Swift学习2---函数和闭包

函数 Swift使用func关键字声明函数: func greet(name: String, day: String) -> String { return "Hello \(name), today is \(day)." } greet("Bob", "Tuesday") 通过元组(Tuple)返回多个值: func getGasPrices() -> (Double, Double, Double) { return (3.5

从几个简单的程序看PHP的垃圾回收机制

每一种计算机语言都有自己的自动垃圾回收机制,让程序员不必过分关心程序内存分配,php也不例外,但是在面向对象编程(OOP)编程中,有些对象需要显式的销毁,防止程序执行内存溢出. 一.PHP 垃圾回收机制(Garbage Collector 简称GC)永盈会娱乐城 在PHP中,没有任何变量指向这个对象时,这个对象就成为垃圾.PHP会将其在内存中销毁:这是PHP的GC垃圾处理机制,防止内存溢出.当一个PHP线程结束时,当前占用的所有内存空间都会被销毁,当前程序中所有对象同时被销毁.GC进程一般都跟着

JavaScript中作用域链和闭包

一.匿名函数 1.1 匿名函数的概念 ? 声明一个没有函数名的函数,就是匿名函数. ? 有函数名的函数就是具名函数. 看下面的代码: <script type="text/javascript"> /* //这里定义了一个函数,而且没有函数名.这样写语法是错误的,如果允许这样定义,那么根本就没有办法调用. //所以,我们可以用一个变量来存储一下 function(){ } */ // 声明了一个匿名函数,并把匿名函数赋值给变量f. 注意这个时候这个匿名函数并没有执行. va