匿名方法中的变量

前面一篇文章看到了C# 2.0中通过匿名方法来简化委托,下面来看看匿名方法中的变量。

闭包和不同的变量类型

闭包的基本概念是:一个函数除了能够通过提供给它的参数与环境交互之外,还能同环境进行更大程度的互动。对于C# 2.0中出现的匿名方法的闭包表现为,匿名方法能使用在声明该匿名方法的方法内部定义的局部变量

在进一步了解闭包之前,我们先看看下面两个术语:

外部变量(outer variable):是指其作用域(scope)包括一个匿名方法的局部变量或参数(ref和out参数除外)

被捕捉的外部变量(captured outer variable):它是在匿名方法内部使用的外部变量

结合上面的解释,来看一个被捕获的变量的例子:

private static void EnclosingMethod()
{
    //未被捕获的外部变量
    int outerVariable = 2;
    //被匿名方法捕获的外部变量
    string capturedVariable = "captured variable";

    if (DateTime.Now.Hour == 23)
    {
        //普通局部变量
        int normalLocalVarialbe = 3;
        Console.WriteLine(normalLocalVarialbe);
    }

    Action x = delegate
    {
        //匿名方法的局部变量
        string anonymousLocal = "local variable of anonymous method";
        //获得被捕获的外部变量
        Console.WriteLine(capturedVariable);
        Console.WriteLine(anonymousLocal);
    };
    x();
}

一个变量被捕获之后,被匿名方法捕获的是这个变量,为不是创建委托实例时该变量的值。下面通过一个例子来看看这句描述。

private static void CapturedVariableTesting()
{
    string captured = "before x is created";

    Action x = delegate
    {
        Console.WriteLine(captured);
        captured = "changed by x";
    };

    captured = "changed before x is invoked";
    x();

    Console.WriteLine(captured);

    captured = "before second invocation";
    x();
}

代码的输出为:

在CapturedVariableTesting这个方法中,我们始终都是在使用同一个被捕获变量captured;也就是说,在匿名方法外对被捕获变量的修改,在匿名方法内部是可见的,反之亦然。

捕捉变量的用途

闭包的出现给我们带来很多的便利,直接利用被捕获变量可以简化编程,避免专门创建一些类来存储一个委托需要处理的信息。

看一个例子,我们给定一个上限,来获取List中所有小于这个上限的数字。

private static List<int> FindAllLessThan(List<int> numList, int upperLimitation)
{
    return numList.FindAll(delegate(int num)
    {
        return num < upperLimitation;
    });
}

由于闭包的出现,我们不用将upperLimitation这个变量以函数参数的形式传给匿名函数,在匿名方法中可以直接使用这个被捕获的变量。

捕获变量的工作原理

前面看到的例子都比较简单,下面我们看一个稍微复杂的例子:

static void Main(string[] args)
{
    Action x = CreateDelegateInstance();
    x();
    x();
    Console.Read();
}

private static Action CreateDelegateInstance()
{
    int counter = 5;
    Action ret = delegate
    {
        Console.WriteLine(counter);
        counter++;
    };

    ret();
    return ret;
}

代码输出为:

为什么结果是5,6,7?变量counter在CreateDelegateInstance方法结束后为什么没有被销毁?

当我们查看这个例子的IL代码时,发现编译器为我们创建了一个类"<>c__DisplayClass1"。

.class nested private auto ansi sealed beforefieldinit ‘<>c__DisplayClass1‘
    extends [mscorlib]System.Object
{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    // Fields
    .field public int32 counter

    // Methods
    .method public hidebysig specialname rtspecialname
        instance void .ctor () cil managed
    {
        // Method begins at RVA 0x2078
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [mscorlib]System.Object::.ctor()
        IL_0006: ret
    } // end of method ‘<>c__DisplayClass1‘::.ctor

    .method public hidebysig
        instance void ‘<CreateDelegateInstance>b__0‘ () cil managed
    {
        // Method begins at RVA 0x2080
        // Code size 28 (0x1c)
        .maxstack 8

        IL_0000: nop
        IL_0001: ldarg.0
        IL_0002: ldfld int32 AnonymousMethod.Program/‘<>c__DisplayClass1‘::counter
        IL_0007: call void [mscorlib]System.Console::WriteLine(int32)
        IL_000c: nop
        IL_000d: ldarg.0
        IL_000e: dup
        IL_000f: ldfld int32 AnonymousMethod.Program/‘<>c__DisplayClass1‘::counter
        IL_0014: ldc.i4.1
        IL_0015: add
        IL_0016: stfld int32 AnonymousMethod.Program/‘<>c__DisplayClass1‘::counter
        IL_001b: ret
    } // end of method ‘<>c__DisplayClass1‘::‘<CreateDelegateInstance>b__0‘

} // end of class <>c__DisplayClass1

而在CreateDelegateInstance方法的IL代码中可以看到,CreateDelegateInstance的局部变量counter实际上就是"<>c__DisplayClass1"对象的counter字段

IL_0000: newobj instance void AnonymousMethod.Program/‘<>c__DisplayClass1‘::.ctor()
IL_0005: stloc.1
IL_0006: nop
IL_0007: ldloc.1
IL_0008: ldc.i4.5
IL_0009: stfld int32 AnonymousMethod.Program/‘<>c__DisplayClass1‘::counter

通过上面的分析可以看到,编译器创建了一个额外的类来容纳变量,CreateDelegateInstance方法拥有该类的一个实例引用,并通过这个引用访问counter变量。counter这个局部变量并不是在"调用栈"空间上,这也就解释了为什么函数返回后,这个变量没有被销毁。

在上面的例子中只有一个委托实例,下面再看一个拥有多个委托实例的例子:

static void Main(string[] args)
{
    List<Action> list = new List<Action>();

    for(int index = 0; index < 5; index++)
    {
        int counter = index * 10;
        list.Add(delegate
        {
            Console.WriteLine(counter);
            counter++;
        });
    }

    foreach (Action x in list)
    {
        x();
    }

    list[0]();
    list[0]();

    list[1]();

    Console.Read();
}

代码输出为:

通过输出可以看到,每个委托实例将捕获不同的变量。

所以被捕获变量的声明期可以总结为:对于一个被捕获的变量,只要还有任何委托实例在引用它,它就会一直存在;当一个变量被捕获时,捕获的是变量的"实例"。

总结

本文介绍了闭包和不同的变量类型。在匿名方法中,通过被捕获变量,我们可以使用"现有"的上下文信息,而不必专门设置额外的类型来存储一些已知的数据

同时,介绍了被捕获变量的生命期,通过IL代码看到了被捕获变量的工作原理。

时间: 2024-10-29 05:03:02

匿名方法中的变量的相关文章

C#:在匿名方法中捕获外部变量

先来一段代码引入主题.如果你可以直接说出代码的输出结果,说明本文不适合你.(代码引自<深入理解C#>第三版) class Program { private delegate void TestDelegate(); static void Main(string[] args) { TestDelegate[] delegates = new TestDelegate[2]; int outside = 0; for(int i = 0; i < 2; i++) { int insid

javaz中向方法中传入变量,数据有的发生改变有的没有改变的原因

//在栈里面的数据不具备内存地址,方法运行创建,方法结束销毁.变量----也就是在栈中的基本数据 方法中的变量:    基本数据类型----变量==基本数据(8大类型)    引用数据类型----变量==内存地址                在方法中定义的变量,方法结束后都会销毁.(基本数据(8大类型)和内存地址被销毁)    in (Object    obj){        }        out(    ){        //进入的时候会创建一个与变量相同的变量(局部/成员/静态)

【.Net基础拾遗】匿名方法中的捕捉变量(探讨文)

最近在和朋友讨论的时候发现了一个很有意思的问题,就是捕捉变量,我现在下面把代码贴出来,大家可以先猜猜结果,然后我们在一起讨论为什么. 1 public class Test 2 { 3 private int k = 0; 4 private List<Action> dg1s = new List<Action>(), dg2s = new List<Action>(), dg3s = new List<Action>(); 5 public void f

5.5 匿名方法中的捕获变量

class Program { static void Main(string[] args) { MethodInvoker m = CreateInvoker(); m(); m(); Console.ReadKey(); } static MethodInvoker CreateInvoker() { int count = 5; MethodInvoker ret = delegate { Console.WriteLine(count); count++; }; ret(); retu

方法组转换和匿名方法

前面的文章介绍过,C# 1.0中出现委托这个核心概念,在C# 2.0中,委托得到了很大的改进.C# 2.0中委托的改进为C# 3.0中的新特性提供了铺垫,当我们了解了匿名方法后,Lambda的学习就会变得相对容易. 下面就看看C# 2.0中委托的改进. 方法组转换 在C# 1.0中,如果要创建一个委托实例,就必须同时指定委托类型和符合委托签名的方法.但是,在C# 2.0中,支持了方法组转换,也就是说我们可以从方法组到一个兼容委托类型的隐式转换.所谓"方法组"(method group)

Javascript中的方法和匿名方法

Javascript方法(函数) 声明函数 以function开头,后跟函数名,与C#.java不同,Javascript不需要声明返回值类型.参数类型.没有返回值就是undefined. 举个栗子更清楚:  无参数无返回值的方法: function f1(){ alert('这是一个方法'); } f1();//调用方法 无参数有返回值的方法: function f2(){ return 100; } var result=f2();//声明一个变量,接收f1()中的返回值 alert(res

C# 2.0 中的新增功能03 匿名方法

连载目录    [已更新最新开发文章,点击查看详细] 在 2.0 之前的 C# 版本中,声明委托的唯一方式是使用命名方法. C# 2.0 引入匿名方法,在 C# 3.0 及更高版本中,Lambda 表达式取代匿名方法作为编写内联代码的首选方式. 但是,本主题中有关匿名方法的信息也适用于 Lambda 表达式. 在有一种情况下,匿名方法提供 Lambda 表达式中没有的功能. 使用匿名方法可省略参数列表. 这意味着匿名方法可转换为具有多种签名的委托. Lambda 表达式无法实现这一点. 有关 L

不能用private修饰方法中定义的变量

在敲代码时发现了一个自己之前没有注意到点:(确切来说是没有认真理解权限修饰符)在方法中定义变量时,不能加权限修饰符原因如下: 原文地址:https://blog.51cto.com/14234228/2468546

C#基础—匿名方法(Anonymous Mehod)

1.引入匿名方法 早在C# 2.0中就提出了匿名方法,实现了以一种内联的方式声明委托,在此之前,声明委托唯一的方法是"命名方法",虽然 C# 3.0 里有了lambda ,使得写内联代码更加简洁和方法,但是匿名方法依然有他的用处,匿名方法提供了可以忽略参数列表的能力. 2.匿名方法的使用和注意点 什么匿名方法?简单的理解就是没有定义名字的方法(其实编译器还是帮我们生成了一个方法).代码的实现就是把方法的定义和方法的实现内联到了一起. 先看个演示例子: 1 class Program 2