InfoQ

文章

高阶函数、委托与匿名方法

作者 赵劼 发布于 2009年4月17日 下午6时35分

社区
.NET
主题
编程,
.NET框架
标签
CLR,
生产力

高阶函数(higher-order function)是指把另一个函数作为参数或返回值的函数。例如在JavaScript语言中,Function是顶级类型。一个函数就是类型为 Function的顶级对象,自然就可以作为另一个函数的参数或返回值。例如在Microsoft AJAX Library(ASP.NET AJAX的客户端类库)中有一个被广泛使用的createDelegate方法。该方法接受一个对象A和一个函数F作为参数,并返回一个函数R。当调用函 数R时,F函数将被调用,并且保证无论在什么上下文中,F的this引用都会指向对象A:

Function.createDelegate = function(instance, func) {
 return function() {
 return callback.apply(a, arguments);
 } 
}

委托是.NET平台中一种特殊的类型。有人说,它是一种强类型的函数指针。这种说法虽然细节上略失偏颇,但是从功能和作用上讲不无道理。有了委 托类型,一个方法就能被封装成一个对象被作为另一个方法的参数或返回值,这自然就为.NET平台上的语言(例如C#,VB.NET)引入了对高阶函数的“ 原生支持”1。例如在System.Array类中就有许多静态的高阶函数,其中的ConvertAll方法可谓是最常用的高阶函数之一了:

private static DateTime StringToDateTime(string s) {
 return DateTime.ParseExact(s, "yyyy-MM-dd", null);
}

static void Main(string[] args) {
 string[] dateStrings = new string[] {
 "2009-01-01", "2009-01-02", "2009-01-03",
 "2009-01-04", "2009-01-05", "2009-01-06",
 };

 DateTime[] dates = Array.ConvertAll<string, DateTime>(
 dateStrings, new Converter<string, DateTime>(StringToDateTime));
}

ConvertAll将一个数组映射为另一个数组,就好像Ruby中array类型的map方法一样,但是如果您会发现ruby的“内联”写法会方便许多。于是在C# 2.0中,又引入了匿名方法这一构建委托对象的方式:

string[] dateStrings = new string[] {
 "2009-01-01", "2009-01-02", "2009-01-03",
 "2009-01-04", "2009-01-05", "2009-01-06",
};

DateTime[] dates = Array.ConvertAll<string, DateTime>(
 dateStrings,
 delegate(string s) {
 return DateTime.ParseExact(s, "yyyy-MM-dd", null);
 });

匿名方法并不只是“匿名”的方法,它甚至可以构造一个闭包给开发带来极大的便利。可见在2.0中已经为高阶函数在C#中的运用打下了坚实的基 础。而且,由于新增了Lambda表达式和扩展方法等语言特性,再加上范型类型的自动判断,在C# 3.0中使用匿名方法更是异常简洁,甚至与ruby的语法如出一辙:

IEnumerable<DateTime> dates = dateStrings.Select(
 s => DateTime.ParseExact(s, "yyyy-MM-dd", null));

从理论上说,委托从在.NET 1.x环境中即得到了完整的支持,但是直到C# 3.0之后高阶函数在.NET中的应用切实地推广开来。善于使用高阶函数的特性能够有效地提高开发效率,同时使代码变得优雅、高效。为了方便开 发,.NET 3.5中甚至定义了三种泛化的委托类型:Action<>、Predicate<>以及Func<>,让开发人员可 以在项目中直接使用。如今,微软官方的各种框架和类库(例如著名的并行库)中对于高阶函数的使用几乎将其变成了一种事实标准。在这一点上,Lambda表达式和匿名方法可谓居功至伟。

高阶函数的一个重要特点就是对参数方法的延迟执行。例如,对于普通的方法调用方式来说:

DoSomething(Method1(), Method2(), Method3());

代码中涉及到的四个方法调用顺序已经完全确定:只有当Method1、Method2和Method3三个方法依次调用完毕并返回之后,DoSomething方法才会执行。然而,如果我们改变DoSomething方法的签名,并使用这样的方式:

DoSomething(() => Method1(), () => Method2(), () => Method3());

这样,四个方法中DoSomething方法肯定首先被调用,然后根据方法体内的具体实现,其余三个方法可能被调用任意次数——甚至一次也不会 被调用。利用这个特性,即“提供方法体,但是不执行”,我们就可以在某些逻辑不确定的情况下避免不必要的开销。例如,如果不使用高阶函数,一段处理数据的 逻辑可能是这样的:

void Process(object dataThatIsExpensiveToGet) {
 bool canProcess = GetWhetherDataCanBeProcessedOrNot();
 if (canProcess) {
 DoSomeThing(dataThatIsExpensiveToGet);
 }
}

在上例中,Process方法只有在满足特定前提的情况下才对参数进行处理,而且在很多时候这个前提条件必须在Process方法中才能判断。 这时,如果参数本身需要昂贵的代价才能获得,那么获取参数的损耗就白白被浪费了。为了避免这种无谓的消耗,我们可以在设计Process方法API时使用 如下办法:

void Process(Func<object> expensiveDataGetter) {
 bool canProcess = GetWhetherDataCanBeProcessedOrNot();
 if (canProcess) {
 object dataToProcess = expensiveDataGetter();
 DoSomeThing(dataToProcess);
 }
}

这样,我们就可以使用如下的方式来调用Process方法:

// Process(GetExpensiveData(args));
Process(() => GetExpensiveData(args));

与注释掉的代码相比,消耗巨大GetExpensiveData方法并不会被直接调用,而只有在Process方法内满足前提条件时才会执行。有时候,我们甚至可以在第一个参数方法满足特定条件时才执行另一个参数方法。在《您善于使用匿名函数吗?》一文中的CacheHelper便是这样一个例子:

public static class CacheHelper
{ public delegate bool CacheGetter<TData>(out TData data); public static TData Get<TData>( CacheGetter<TData> cacheGetter, /* get data from cache */
Func<TData> sourceGetter, /* get data from source (fairly expensive) */
Action<TData> cacheSetter /* set the data to cache */) { TData data; if (cacheGetter(out data)) { return data; } data = sourceGetter(); cacheSetter(data); return data; } }

CacheHelperGet方法接受三个委托对象作为参数,只有当第一个方法(从缓存中获取对象)返回为False时,才会执行第二个(从 相对昂贵的数据源获取数据)和第三个方法(将数据源中得到的数据放入缓存)。同时,这个示例也展示了高阶方法的另一个常用特点:封装一段通用的逻辑,将逻 辑中特定部分的交由外部实现——这不就是“模板方法(Template Method)模式”吗?高阶函数从某个角度可以看成是一种轻量级的模板方法实现,它提供了模板方法中的主要特性,但是不需要使用“继承”这种耦合性很高 的扩展方式。而且,由于可以为一个委托参数提供任意的实现,我们也可以在某些场景下用它来代替“策略(Strategy)模式”的使用。

  不过也由此可见,高阶函数并不一定需要“函数指针”或“委托类型”的支持。事实上,面向对象语言中的对象可以携带方法,而一个方法可以接受另一 个对象作为参数(或返回一个对象),那么这个方法自然也就相当于一个接受或返回方法的“高阶函数”了。例如,我们可以使用Java来实现如上的CacheHelper辅助类:

public interface Func<T> {
 T execute();
}

public interface Action<T> {
 void execute(T data);
}

public class CacheHelper {
 public static<T> T get(
 Func<T> cacheGetter,
 Func<T> sourceGetter,
 Action<T> cacheSetter)
 {
 T data = cacheGetter.execute();
 if (data == null) {
 data = sourceGetter.execute();
 cacheSetter.execute(data);
 }
 
 return data;
 }
}

不过从C#的演变过程中可以看出,高阶函数的特性要真正得到推广,也必须由“匿名方法”等更多特性加以辅佐才行。Java中的“匿名类”与C#中的“匿名方法”有异曲同工之处,例如,开发人员同样可以使用内联的写法来调用CacheHelper

public Object getData()
{
 return CacheHelper.get(
 new Func<Object>() {
 public Object execute() {
 /* get data from cache */
return null; } }, new Func<Object>() { public Object execute() { /* get data from source (fairly expensive) */
return null; } }, new Action<Object>() { public void execute(Object data) { /* set the data to cache */
} }); }

可惜,有些时候类似的代码在Java语言中相对并不那么实用。其原因可能是因为Java中“匿名类”语法较为复杂,且匿名类的内部逻辑无法修改调用方法里的局部变量——由此也可对比出C#中匿名函数这一特性的美妙之处。

注1:严格来说,.NET只是提供了一个平台,一个“运行时(CLR)”,但“高阶函数”其实是个语言方面的概念。我们可以在.NET上实现任意一种语 言,而这种语言就算没有得到平台的直接支持,也能够实现“高阶函数”这个特性。因此,之所以是“原生支持”,其实指的是.NET平台对高阶函数所需的特性 有着直接的支持,它使得C#或VB.NET等语言中能够直接使用高阶函数这一功能。

结论:

.NET 3.5对于创建委托对象的良好支持使得高阶函数在.NET平台上的使用得到了卓有成效的推广。从微软新发布的框架和类库中来看,高阶函数几乎已经成为了一 种事实标准。善于使用高阶函数的特性能够有效地提高开发效率,同时使代码变得优雅、高效。可以料想的到,善于使用高阶函数会逐步成为一个优秀的.NET开 发人员的必备技术。


给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家加入到InfoQ中文站用户讨论组中与我们的编辑和其他读者朋友交流。

与STL的仿函数(Functor)有什么区别 发表人 Sam Song 发表于 2009年4月21日 上午1时20分
.Net3.5是一套昂贵的技术 发表人 皓月 江天 发表于 2009年5月17日 上午9时9分
Re: .Net3.5是一套昂贵的技术 发表人 An Han 发表于 2009年7月2日 上午2时9分
勘误? 发表人 airwolf2026 wolf2026 发表于 2009年7月21日 上午5时33分
Great 发表人 Albert Lee 发表于 2009年9月4日 上午4时44分
  1. 返回顶部

    与STL的仿函数(Functor)有什么区别

    2009年4月21日 上午1时20分 发表人 Sam Song

    仿函数就是重写了()运算的类,即将函数作为整体封装。感觉与代理的概念很类似。
    那么匿名函数只是方便了使用而已吧?

  2. 返回顶部

    .Net3.5是一套昂贵的技术

    2009年5月17日 上午9时9分 发表人 皓月 江天

    显然,微软从各个角度去推广.Net3.5,并且采取了一些策略,比如WCF,WPF不相容于.NET2.0,SliverLight、ASP.NET MVC也是如此,在围绕着这些漂亮的技术背后意味着--我们需要花钱升级我们的开发平台。

  3. 返回顶部

    Re: .Net3.5是一套昂贵的技术

    2009年7月2日 上午2时9分 发表人 An Han

    有同感,做技术储备将来用吧。另外随时接收一些新东西也不是坏事,用的时候再抓再怎么说也会晚。当升级的成本低于使用新技术的成本的时候,好日子就来了。

  4. 返回顶部

    勘误?

    2009年7月21日 上午5时33分 发表人 airwolf2026 wolf2026

    Process(() => GetExpensiveData(args));
    这个是不是错了?

  5. 返回顶部

    Great

    2009年9月4日 上午4时44分 发表人 Albert Lee

    FP 终于在MS 上开花结果了。我认为在这一点上 C# 真正把握了未来的方向。

深度内容

模块化Java:声明式模块化

本文是模块化Java系列文章的第4篇,介绍的是声明式模块化。文中描述了组件如何以声明的方式来定义并组织在一起,而无需让代码依赖于OSGI API。

Ian Robinson和Jim Webber谈论基于Web的整合

本采访是在伦敦举行的QCon2009上记录的,Ian Robinson和Jim Webber探讨了如何将Web作为整合平台以及REST在理论上和实践中的好处。

项目管理修炼之道(精选版)

项目管理对于项目成败至关重要,但实践中每个项目都有自己的独特性,没有现成的解决方案可以套用。书中从应对实际风险的角度出发,讲述了从项目启动、项目规划到项目结束的整个管理流程,展示了作者的思考过程。本迷你书从原书中精选出5个章节。

那是鸟,还是飞机?不,那是超人!

在这个演讲中,Fred将会揭示敏捷的一些外在因素,并会重点关注敏捷获得成功的内在原因。从案例研究和真实的项目经验来看,Fred认为:工具、管理体系都不能让你变得敏捷。敏捷的成功,植根于士气高涨、充分授权的工作者身上,他们能够以不同以往的方式思考问题。

访谈和书摘:Eben Hewitt的新书《Java SOA Cookbook》

Java SOA Cookbook

Eben Hewitt的新书《Java SOA Cookbook》从Java实现的角度讨论了面向服务架构。Eben在书中讨论了SOA基础、工具、最佳实践和SOA治理等主题。

Mark Richard的《Java消息服务》第二版

Mark Richards的新书《Java消息服务》第二版覆盖了JMS的许多主题, 包括发布和订阅模式以及点对点模式,消息过滤和事务等。InfoQ与Mark谈论了跟他的新作。

模块化Java:动态模块化

本文是“模块化Java”系列文章的第三篇,讨论动态模块化,内容涉及如何解析bundle类、bundle如何变化、以及bundle之间如何通信。

让测试也敏捷起来

对于测试组织来说,敏捷方法带来的快速迭代却让测试本身变得困难起来:缺乏“足够详细的文档”,缺乏“仔细设计用例的时间”等等。在本演讲中,段念将与大家探讨如何在敏捷过程中进行测试。