C#高级编程(第10版) C# 6 & .NET Core 1.0 (.NET开发经典名著)
上QQ阅读APP看书,第一时间看更新

9.3 lambda表达式

自C# 3.0开始,就可以使用一种新语法把实现代码赋予委托:lambda表达式。只要有委托参数类型的地方,就可以使用lambda表达式。前面使用匿名方法的例子可以改为使用lambda表达式。

        using System;
        using static System.Console;
        namespace Wrox.ProCSharp.Delegates
        {
          class Program
          {
            static void Main()
            {
              string mid = ", middle part, ";
              Func<string, string> lambda = param =>
                {
                  param += mid;
                  param += " and this was added to the string.";
                  return param;
                };
              WriteLine(lambda("Start of string"));
            }
          }
        }

lambda运算符“=>”的左边列出了需要的参数,而其右边定义了赋予lambda变量的方法的实现代码。

9.3.1 参数

lambda表达式有几种定义参数的方式。如果只有一个参数,只写出参数名就足够了。下面的lambda表达式使用了参数s。因为委托类型定义了一个string参数,所以s的类型就是string。实现代码调用String.Format()方法来返回一个字符串,在调用该委托时,就把该字符串最终写入控制台(代码文件LambdaExpressions/Program.cs):

        Func<string, string> oneParam = s =>
                $"change uppercase {s.ToUpper()}";
        WriteLine(oneParam("test"));

如果委托使用多个参数,就把这些参数名放在圆括号中。这里参数x和y的类型是double,由Func<double, double, double>委托定义:

        Func<double, double, double> twoParams = (x, y) => x * y;
        WriteLine(twoParams(3, 2));

为了方便起见,可以在圆括号中给变量名添加参数类型。如果编译器不能匹配重载后的版本,那么使用参数类型可以帮助找到匹配的委托:

        Func<double, double, double> twoParamsWithTypes = (double x, double y) => x * y;
        WriteLine(twoParamsWithTypes(4, 2));

9.3.2 多行代码

如果lambda表达式只有一条语句,在方法块内就不需要花括号和return语句,因为编译器会添加一条隐式的return语句:

        Func<double, double> square = x => x * x;

添加花括号、return语句和分号是完全合法的,通常这比不添加这些符号更容易阅读:

        Func<double, double> square = x =>
          {
            return x * x;
          }

但是,如果在lambda表达式的实现代码中需要多条语句,就必须添加花括号和return语句:

        Func<string, string> lambda = param =>
          {
            param += mid;
            param += " and this was added to the string.";
            return param;
          };

9.3.3 闭包

通过lambda表达式可以访问lambda表达式块外部的变量,这称为闭包。闭包是非常好用的功能,但如果使用不当,也会非常危险。

在下面的示例中,Func<int, int>类型的lambda表达式需要一个int参数,返回一个int值。该lambda表达式的参数用变量x定义。实现代码还访问了lambda表达式外部的变量someVal。只要不假设在调用f时,lambda表达式创建了一个以后使用的新方法,这似乎没有什么问题。看看下面这个代码块,调用f的返回值应是x加5的结果,但实情似乎不是这样:

        int someVal = 5;
        Func<int, int> f = x => x + someVal;

假定以后要修改变量someVal,于是调用lambda表达式时,会使用someVal的新值。调用f(3)的结果是10:

        someVal = 7;
        WriteLine(f(3));

同样,在lambda表达式中修改闭包的值时,可以在lambda表达式外部访问已改动的值。

现在我们也许会奇怪,如何在lambda表达式的内部访问lambda表达式外部的变量。为了理解这一点,看看编译器在定义lambda表达式时做了什么。对于lambda表达式x => x + someVal,编译器会创建一个匿名类,它有一个构造函数来传递外部变量。该构造函数取决于从外部访问的变量数。对于这个简单的例子,构造函数接受一个int值。匿名类包含一个匿名方法,其实现代码、参数和返回类型由lambda表达式定义:

        public class AnonymousClass
        {
          private int someVal;
          public AnonymousClass(int someVal)
          {
            this.someVal = someVal;
          }
          public int AnonymousMethod(int x) => x + someVal;
        }

使用lambda表达式并调用该方法,会创建匿名类的一个实例,并传递调用该方法时变量的值。

注意:如果给多个线程使用闭包,就可能遇到并发冲突。最好仅给闭包使用不变的类型。这样可以确保不改变值,也不需要同步。

注意:lambda表达式可以用于类型为委托的任意地方。类型是Expression或Expression<T>时,也可以使用lambda表达式,此时编译器会创建一个表达式树。该功能的介绍详见第11章。