Lambda表达式

Lambda表达式

Lambada表达式是一种可以替代委托实例的匿名方法。编译器会立即将Lambda表达式转换为一下两种形式之一:

  • 一个委托实例
  • 一个类型为Expression的表达式树(这个后面将)

匿名方法

上面说Lambada是一种匿名方法,那么就要先了解一下什么是匿名方法

匿名方法是C#2.0引入的特性

匿名方法的写法实在delegate关键字后面跟上参数的声明(可选),然后是方法体

using System;
class Program
{
    delegate void Example();
    static void Main(string[] args)
    {
        // Example e = delegate(){ Console.WriteLine("一个匿名方法的实现"); };
        // 如果没有参数,可以省略参数的括号
        Example e = delegate { Console.WriteLine("一个匿名方法的实现"); };
        e();
    }
}

这里声明了一个匿名方法,匿名方法解决的问题,是有时候想用委托,就必须需要一个方法,但是这个方法只是在这个委托中使用一下子,不需要在其他地方复用,于是引入了匿名方法

delegate(){ Console.WriteLine("一个匿名方法的实现"); };

匿名方法的写法其实与普通方法并无异样,只是用delegate关键字在前标注,省略掉方法名(如果不好理解,可以理解成方法名为delegate,没有参数可以省略括号,只能用于注册进委托)

匿名方法使用情况不多,因为C#3.0引入的Lambda更加强大,也是后面要讲的重点

匿名方法目前最广泛的用法,是用于声明空事件处理器的事件

public event EventHandler Clicked =delegate {  };

Clicked事件不会进行任何操作,因为没有定义任何操作,但是Clicked不为空,不会抛异常,在用户层面,就是点击了某个按钮后没有任何变化,但是如果Clicked为空,就会抛异常

完全省略参数的声明是匿名方法独有的特性,即使委托需要这些参数声明,如上面声明空事件处理器的事件,EventHandler其实需要一个object的参数和一个EventArgs类型的参数

Lambda表达式

Lambda是一种更强大匿名方法,前面讲了匿名方法,先来看看Lambda如何替代匿名方法

using System;
class Program
{
    delegate void Example();
    static void Main(string[] args)
    {
        // Example e = () => { Console.WriteLine("一个Lambda表达式"); };
        // 当方法体只有一句时可以省略大括号
        Example e = () =>  Console.WriteLine("一个Lambda表达式"); 
        e();
    }
}

从代码中可以看到,匿名方法被替换成了这样一句

() => { Console.WriteLine("一个Lambda表达式"); }

在Lambda表达式中=>之前的是方法的参数,=>之后是方法体

参数和方法体的编写规则
  • 编译器通常可以根据上下文推断出Lambda表达式的类型,但是当无法推断的时候则必须显式指定每一个参数的类型
// 能够推断参数类型
(x) => { return x; }
// 不能推断参数类型
(int x) => { return x;}
  • 没有参数,一个参数和多个参数时的写法
// 没有参数时小括号不能省略
() => { Console.WriteLine("一个Lambda表达式"); }

(x) => { return x; }
// 只有一个参数时可以省略小括号
x => { return x; }

// 多个参数时小括号不能省略
(x,y,z) => { return x+y+z; }
  • 方法体只有一条语句的时候,可以省略大括号,return也可以省略;方法体有多条语句时大括号不能省略
x => { return x; }
// 一条语句可以省略大括号
x => return x;
// 一条语句可以省略return
x => x;

// 多条语句时不能省略大括号和return
x =>
{
    Console.WriteLine("看看");
    return x;
}; 

Lambda表达式的闭包和foreach

Lambda表达式可以引用方法内定义的局部变量和方法的参数(外部变量)

Lambda表达式所引用的外部变量称为捕获变量,捕获变量的表达式称为闭包

using System;
class Program
{
    static void Main(string[] args)
    {
        int x = 2;
        Func<int, int> sum = n => n + x;
        Console.WriteLine(sum(10));    // 输出12
    }
}

在这个例子中,x就是被捕获的变量

捕获变量的值

Lambda表达式捕获的变量是在调用委托时赋值,而不是在捕获时赋值

using System;
class Program
{
    static void Main(string[] args)
    {
        int x = 2;
        // 捕获外部变量x,但此时并没有赋值
        Func<int, int> sum = n => n + x;
        x = 10;
        // 调用个委托时才赋值,此时x是10
        Console.WriteLine(sum(10));    // 输出20
    }
}

捕获变量的生命周期会延伸到和委托的生命周期一致

Lambda表达式foreach的两个版本

如果Lambda捕获迭代变量,最后会有怎样的结果

using System;
class Program
{
    static void Main(string[] args)
    {
        Action[] actions = new Action[3];
        for (int i = 0; i < 3; i++)
        {
            actions[i] = () => Console.WriteLine(i);
        }

        foreach (var a in actions)
        {
            a();
        }
        // 输出333
    }
}

先来看这个例子,利用Lambda表达式捕获了for循环的i变量,但此时i的值并没有确定,前面说过,Lambda捕获的变量在调用时才赋值,所以这虽然捕获了三次i,但这三次都是捕获的同一个i,所以最后在调用时赋值了i的最后的值3(前面说过,捕获变量的生命周期会延伸到和委托的生命周期一致,虽然for循环结束了,但是因为Lambda的捕获延长了生命周期,3这个值保留了下来),所以最后输出的是333

如果要解决这个问题,只需要将循环变量指定到内部的变量中,即

using System;
class Program
{
    static void Main(string[] args)
    {
        Action[] actions = new Action[3];
        for (int i = 0; i < 3; i++)
        {
            int temp = i;
            actions[i] = () => Console.WriteLine(temp);
        }

        foreach (var a in actions)
        {
            a();
        }
        // 输出012
    }
}

对于lambda表达式来说,捕获了三次temp,但是每一次都是新定义的temp,所以不受影响

下面来看一个foreach的“BUG"

using System;

class Program
{
    static void Main(string[] args)
    {
        Action[] actions = new Action[3];
        int i = 0;
        foreach (char c in "abc")
        {
            actions[i++] = () => Console.WriteLine(c);
        }

        foreach (Action action in actions)
        {
            action();
        }
        // 输出abc
    }
}

这里输出结果是abc,这是因为foreach的每一个迭代变量都是不可变的,所以可以理解为循坏体中的局部变量,也就是类似于上面的temp,但是,在C#5.0之前,结果并不是这样的,foreach会像前面的for语言一样解析,如果遇到老版本的代码,一定要特别注意

posted @ 2020-08-07 12:13  吴俊城  阅读(471)  评论(0编辑  收藏  举报