5.5.1 Lambda 函数
在 F# 中,lambda 函数创建的函数,与一般使用 let 绑定声明的相同。在 C# 中,没有任何内置的函数概念,所以,我们既可以使用方法,也可以使用委托。写的 lambda 函数,被转换为委托或表达式树 (在“C# 中从委托到函数"边栏中,可以找到有关表达式树的详细信息),但是,在 C# 中,不能使用 lambda 函数声明普通方法。委托可以像任何其他值一样使用,所以,可以把它们作为参数值传递给其他的方法,这意味着,我们可以用它们在 C# 中写高阶函数。让我们首先看一个 F# Interactive 会话,然后,在 C# 中,写出类似的代码。清单 5.15 显示了如何在 F# 中,可以写一个函数,使用 let 绑定和 lambda 函数语法。
Listing 5.15 Using lambda functions and let bindings (F# Interactive)
> let square1(a) = a * a;;
val square1 : int –> int
> let square2 = fun a -> a * a;;
val square2 : int –> int
> let add = fun a b -> a + b;;
val add : int -> int –> int
> add 2 3
val it : int = 5
我们首先写一个简单的函数,叫 square1,计算给定的数的平方,我们之前曾经见过几次,在以相同的方式。我们输入它之后,F# 打印其签名(值类型),告诉我们它取一个整数,并返回一个整数。接下来,我们声明另一个值,叫 square2,使用 lambda 符号,初始化为一个函数。如你所见,通过看输出, 两个声明是等价的。最后,我们声明另一个值,它显示了有两个参数的 lambda 函数的语法。看到这些示例后,你也许可以重写任意 F# 函数,使用有 let 绑定的 lambda 表示法,反之亦然。
现在,让我们看看如何写同样的东西,在 C# 中使用 lambda 函数:
Func square =
a => a * a;
Func add =
(a, b) => a + b;
我们使用一个委托类型,叫 Func,这在 .NET 3.5 中是可用的。此委托代表一个函数,其类型参数值指定参数的类型和返回的类型。从技术角度讲,Func 不是单个委托,而是由数目不同的类型参数重载的一族委托。每个代表函数有不同的参数数目。下面是 C# 和 F# 语法之间的显著差异:
■ F# lambda 表达式声明以 fun 关键字开始。
■ 在 C# 中,在括号内指定多个参数,用逗号分隔。在 F# 中,用空格分隔参数。
■ 在 C# 中,声明一个委托值时,必须显式指定类型。
C# 中从委托到函数
如前所述,在 C# 中的函数使用委托来表示,特别是新的 Func 委托族。在某种意义上,lambda 函数与此委托是革命性的变化,为 C# 增加了函数编程,但它也可以也被看作是C# 中已有功能的自然进化。本书通常取前一种看法,但接下来我们将看一下进化的表现。
在 C# 的第一个版本中,就已经有了委托,但没有泛型,我们不得不为返回和参数类型的每个组合单独声明委托。当创建委托时,我们还必须在命名方法内写代码,所以,写出的代码可能像这样:
delegate int FuncIntInt(int a, int b);
FuncIntInt add = new FuncIntInt(Add);
该代码假定有一个 Add 方法,有两个整数参数,返回一个整数类型。C# 2.0 向前迈出一大步,增加了泛型,所以,我们能声明泛型委托,像 Func (尽管它还并不包括在基类库中),创建它们,而使用新的匿名方法功能,而不要写命名方法了:
delegate R Func(T1 arg1, T2 arg2);
Func add = delegate(int a, int b) { return a + b; }
最后,.NET 3.5 和 C# 3.0 附带了几个其他的改变,Func 委托已添加到系统库中,C# 添加了 lambda 表达式,使我们以更简洁的方式写出相同的代码:
Func<int, int, int> add = (a, b) => a + b;
Lambda 表达式具有另一个有趣的特点:它们可以转换为表达式树(expression trees),当我们将它们声明为 Expression (表达式)类型时。这样,就可以把 lambda 表达式的代码当作数据看待,并获得 lambda 表达式的源代码一些表示。这对于使用 LINQ 处理数据库是重要的,但现在,对我们来说还不是主要功能。此外,因为这一功能,我们在声明 lambda 表达式时,不能使用 var 关键字,因为编译器需要决定是否把它编译成代理 (Func),或存储表达式树 (Expression)。
在 C# 中的 Func 委托和 lambda 表达式类似于 F# 中的函数,但是,F# 从一开始就有函数,所以,它几乎不需要为委托。它支持委托,主要是用于互操作性的原因,但是,更多的时候可能不会使用它们。
已经看了 F# 和 C# 中的几个 lambda 函数的例子,但还有几个重要的事情要去探索。
类型注释、动作和语句块
在前面的示例中,我们没有显式指定的参数类型。这在 F# 中是正常的行为,因为,其类型推断能力非常强大,在前面的示例中,它有足够的线索推断出类型。在 C# 中的情况,是另一种有趣的的方式:
Func toStr1 = num => num.ToString();
Func toStr2 = (int num) => num.ToString();
这两行显示相同的代码,唯一区别在于,第二行显式指定 num 参数的类型。这两行都是正确的,那么,C# 如何知第一行中的道 num 的类型的呢?答案是,它使用类型来自变量的声明。它知道,Func < int, string> 是一个委托,取一个整数作为参数值,因此,它推断 num 的类型应该为整数。
在 C# 中,很少需要显式参数键入。无论如何,不能使用 var 关键字声明 lambda 函数,因此,C# 通常能够推断出类型。一个例外情况是,我们将使用 lambda 函数作为一个参数值,给特定的泛型方法。即使在 F# 中,我们偶尔也可能需要给编译器以更多信息,即用类型注释(type annotations)。清单 5.16 显示了有类型注释的 lambda 函数,显式声明其参数的类型。
Listing 5.16 Advanced lambda functions (F# Interactive, C#)
// F# version of the code (using F# Interactive)
> let sayHello =
(fun (str:string) –>
let msg = str.Insert(0, "Hello ")
Console.WriteLine(msg)
)
val sayHello : string –> unit
// C# version of the code
Action sayHello =
str => {
var msg = str.Insert(0, "Hello ");
Console.WriteLine(msg);
};
这个示例显示了几个有趣的事情。第一个是在 F# 版本中使用类型注释。在 lambda 函数中的类型注释的语法与 F# 代码中的其他任何地方都相同。在这种情况下我们为什么要使用它的原因是,我们将调用值 str 的实例方法 Insert,它没给编译器足够的信息来确定值的类型。
另一个值得注意的事情是,lambda 函数的主体不只是一个表达式。在 F# 中,我们添加了一个 let 绑定,把整个 lambda 函数括在括号中。在 C# 版本中,我们添加了一个变量声明,改变语法为使用语句块(statement block)。语句块意味着,lambda 函数体被括在大括号内,它允许我们写在主体内多个语句。要从 lambda 函数返回结果,就使用语句块,用 return 关键字,就如同从方法返回的结果一样。
在此示例中,这个 lambda 函数不返回结果。在 F# 中,unit 是一个普通的类型,推导出的函数签名是 string -> unit。这是一个普通的 F# 函数,原则上,返回 unit 值(即,什么也没有)作为结果。在 C# 中,我们不能写成 Func<string, void>,因为,void 不是一个真正的类型。为此,C# 有另外一族委托类型,称为动作(Action),表示 lambda 函数没有返回类型。Action 和 Func 委托非常有用,对应于 F# 中的函数类型,因此,让我们更详细地看一下函数值的类型。