7.2 克服方法解析的怪癖
到目前为止,我们已经自由地使用方法、lambdas和委托来表示函数。然而,对于编译器来说,这些都是不同的东西,而且方法的类型推理并不像我们希望的那样好。
让我们首先看看当一切顺利时会发生什么,比如当我们使用 Option.Map 时:
Some(9.0).Map(Math.Sqrt) // => 3.0
这里,Math.Sqrt这个名字标识了一个方法,而Map期望一个Func<T,R>类型的委托。 更确切地说,Math.Sqrt标识了一个 “方法组”;由于方法重载,可能会有几个同名的方法。 编译器很聪明,不仅选择了正确的重载(在这种情况下,只有一个),而且还推断出了Func的通用类型,因此我们不需要向Map指定类型参数:
Some(9.0).Map<double, double>(Math.Sqrt)
这一切都非常好。它使我们不必在方法(或者lambdas)和委托之间进行转换,也不必指定通用类型,因为这些类型可以从方法签名中推断出来。 不幸的是,对于有两个或更多参数的方法,所有这些好处都消失了。
让我们看看如果我们尝试将 greet 函数重写为一个方法会发生什么——这里称为 GreeterMethod。我们想写的就是这个。
清单 7.2 多参数方法的类型推断失败
PersonalizedGreeting GreeterMethod(Greeting gr, Name name) // 如果我们将问候函数编写为一种方法......
=> $"{gr}, {name}";
Func<Name, PersonalizedGreeting> GreetWith(Greeting greeting)
=> GreeterMethod.Apply(greeting); //... 那么这个表达式不能编译。
在这里,我们已经把greeter函数写成了一个方法,现在我们想要一个GreetWith方法来把它部分地应用到一个给定的问候语中。不幸的是,这段代码不能编译,因为GreeterMethod这个名字标识了一个MethodGroup,而Apply期望的是一个Func,编译器并没有为我们做出推断。
局部函数中的类型推断 C# 7 引入了“本地函数”——在方法范围内声明的函数——但它们实际上应该被称为“本地方法”。在内部,它们被实现为方法(尽管这没有任何好处——你不能重载它们),所以在类型推断方面,它们具有与普通方法相同的特性。
如果你想使用通用的Apply来为一个方法提供参数,你必须使用以下形式之一。
清单7.3 使用多参数方法作为HOF的参数需要混乱的语法
PersonalizedGreeting GreeterMethod(Greeting gr, Name name)
=> $ "{gr}, {name}";
Func < Name, PersonalizedGreeting > GreetWith_1(Greeting greeting)
=> FuncExt.Apply < Greeting, Name, PersonalizedGreeting > //放弃了扩展方法的语法,明确提供了所有的通用参数。
(GreeterMethod, greeting);
Func < Name, PersonalizedGreeting > GreetWith_2(Greeting greeting)
=> new Func < Greeting, Name, PersonalizedGreeting >(GreeterMethod)//在调用Apply之前,明确地将方法转换为delegate。
.Apply(greeting);
我个人认为这两种情况下的句法噪音是不可接受的。幸运的是,这些问题是针对方法解析的。如果你使用委托(想想Func),它们就会消失。
有多种方法可以创建委托。
清单 7.4 获取委托实例的不同方式
public class TypeInference_Delegate {
string separator = "! ";
// 1. field //委托字段的声明和初始化;请注意,您不能在此处引用分隔符。
Func < Greeting, Name, PersonalizedGreeting > GreeterField
= (gr, name) => $ "{gr}, {name}";
// 2. property //一个getter-only属性的主体是由=>引入的。
Func < Greeting, Name, PersonalizedGreeting > GreeterProperty
=> (gr, name) => $ "{gr}{separator}{name}";
// 3. factory //一个作为函数工厂的方法可以有通用参数。
Func < Greeting, T, PersonalizedGreeting > GreeterFactory < T > ()
=> (gr, t) => $ "{gr}{separator}{t}";
}
让我们简单地讨论一下这些选项。声明一个委托字段似乎是最自然的选择。不幸的是,它并不十分强大。例如,如果你把声明和初始化结合起来,如清单7.4所示,你不能在委托体中引用任何实例变量,如分离器。
这个问题可以通过使用属性来解决。在暴露委托的类中,这相当于只是用 => 替换 = 来声明一个 getter-only 属性,这对客户端代码是完全透明的。但最强大的方法是拥有一个工厂方法:一个用来创建你想要的委托的方法。这里最大的区别是你也可以有泛型参数,这对于字段或属性是不可能的。
无论您以哪种方式获取委托实例,类型解析都可以正常工作,因此在所有情况下您都可以像这样提供第一个参数:
GreeterField.Apply("Hi");
GreeterProperty.Apply("Hi");
GreeterFactory<Name>().Apply("Hi");
本节的启示是,如果你想使用以多参数函数为参数的HOF,有时最好不要使用方法,而是写Funcs,或者写返回Funcs的方法。虽然没有方法那么直白,但Funcs为你省去了明确指定类型参数的语法开销,使代码更易读。
现在你知道了部分应用,让我们继续讨论一个相关的概念:currying。这是一种假设并可以说是简化了部分应用的技术。