写点什么

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

2009 年 4 月 17 日

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

复制代码
Function.createDelegate = <span>function</span>(instance, func) {
<span>return function</span>() {
<span>return </span>callback.apply(a, arguments);
}
}

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

复制代码
<span>private static </span><span>DateTime </span>StringToDateTime(<span>string </span>s) {
<span>return </span><span>DateTime</span>.ParseExact(s, <span>"yyyy-MM-dd"</span>, <span>null</span>);
}
<span>static void </span>Main(<span>string</span>[] args) {
<span>string</span>[] dateStrings = <span>new string</span>[] {
<span>"2009-01-01"</span>, <span>"2009-01-02"</span>, <span>"2009-01-03"</span>,
<span>"2009-01-04"</span>, <span>"2009-01-05"</span>, <span>"2009-01-06"</span>,
};
<span>DateTime</span>[] dates = <span>Array</span>.ConvertAll<<span>string</span>, <span>DateTime</span>>(
dateStrings, <span>new </span><span>Converter</span><<span>string</span>, <span>DateTime</span>>(StringToDateTime));
}

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

复制代码
<span>string</span>[] dateStrings = <span>new string</span>[] {
<span>"2009-01-01"</span>, <span>"2009-01-02"</span>, <span>"2009-01-03"</span>,
<span>"2009-01-04"</span>, <span>"2009-01-05"</span>, <span>"2009-01-06"</span>,
};
<span>DateTime</span>[] dates = <span>Array</span>.ConvertAll<<span>string</span>, <span>DateTime</span>>(
dateStrings,
<span>delegate</span>(<span>string </span>s) {
<span>return </span><span>DateTime</span>.ParseExact(s, <span>"yyyy-MM-dd"</span>, <span>null</span>);
});

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

复制代码
<span>IEnumerable</span><<span>DateTime</span>> dates = dateStrings.Select(
s => <span>DateTime</span>.ParseExact(s, <span>"yyyy-MM-dd"</span>, <span>null</span>));

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

复制代码
<span>void </span>Process(<span>object </span>dataThatIsExpensiveToGet) {
<span>bool </span>canProcess = GetWhetherDataCanBeProcessedOrNot();
<span>if </span>(canProcess) {
DoSomeThing(dataThatIsExpensiveToGet);
}
}

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

复制代码
<span>void </span>Process(<span>Func</span><<span>object</span>> expensiveDataGetter) {
<span>bool </span>canProcess = GetWhetherDataCanBeProcessedOrNot();
<span>if </span>(canProcess) {
<span>object </span>dataToProcess = expensiveDataGetter();
DoSomeThing(dataToProcess);
}
}

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

复制代码
<span>// Process(GetExpensiveData(args));<br></br></span>Process(() => GetExpensiveData(args));

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

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

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

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

复制代码
<span>public interface </span>Func<T> {
T execute();
}
<span>public interface </span>Action<T> {
<span>void </span>execute(T data);
}
<span>public class </span>CacheHelper {
<span>public static</span><T> T get(
Func<T> cacheGetter,
Func<T> sourceGetter,
Action<T> cacheSetter)
{
T data = cacheGetter.execute();
<span>if </span>(data == <span>null</span>) {
data = sourceGetter.execute();
cacheSetter.execute(data);
}
<span>return </span>data;
}
}

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

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

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

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

结论:

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


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

2009 年 4 月 17 日 18:359546
用户头像

发布了 157 篇内容, 共 43.4 次阅读, 收获喜欢 2 次。

关注

评论

发布
暂无评论
发现更多内容

关于HSTS - 强制浏览器使用HTTPS与服务器创建连接

遇见

https 安全 浏览器 TLS 证书

公司大了,人多事杂,如何落地项目制?

树上

项目制 落地 公司管理 业务线 考核

一个值得推荐的人才测量标准

Selina

回"疫"录(1):口罩危机也许是一种进步

小天同学

疫情 回忆录 现实纪录

像黑客一样思考

Fooying

黑客思维 黑客 安全攻防

常用手机软件清单

彭宏豪95

效率工具 App 手机 移动应用

太慢是不行的

池建强

创业 产品

媒体的经营 01 | 媒体/内容行业投资分析的维度

邓瑞恒Ryan

创业 内容 重新理解创业 媒体 投资

过滤数组中重复元素,你知道最优方案吗?

麦叔

数据结构 数组 数组去重

Fire Fast 再深一层的是什么?

树上

管理 考核 Fire Hire 用人

个人知识管理精进指南

非著名程序员

学习 读书笔记 知识管理 认知提升

【SpringBoot】为什么我的 CommandLineRunner 不 run ?

遇见

Java Spring Boot

【SpringBoot】给你的 CommandLineRunner 排个序

遇见

Java Spring Boot

给业务线的总经理多交代了几句

泰稳@极客邦科技

创业 效率 团队管理

机房运维需要了解东西

Spider man

像经营咖啡店一样扩容 Web 系统

Rayjun

Web 扩容

我敢说 80% 的程序员都掉进了「老鼠赛跑」的陷阱

非著名程序员

读书笔记 程序员 程序人生 提升认知

国内10大前端团队网站

有思且行

技术 前端 大前端

复用到何种程度

孙苏勇

Java 程序设计 复用 面向对象 抽象

当我们在说5G网络安全的时候,究竟在说什么?

石君

5G 5G网络安全 5G安全 网络安全

Windows环境MySql8.0忘记root密码重置

玏佾

MySQL

死磕Java并发编程(3):volatile关键字不了解的赶紧看看

七哥爱编程

Java Java并发 volatile

Nginx代理Oracle数据库连接

遇见

MySQL nginx oracle 反向代理

【SpringBoot】为什么我的定时任务不执行?

遇见

Java Spring Boot 定时任务 debug

Java中的Stream用还是不用

孙苏勇

Java 流计算 程序设计 性能

Idea工程启动时报错:Command line is too long

玏佾

intellij-idea

程序员陪娃看绘本之启示

孙苏勇

生活 程序员人生 读书 成长 陪伴

dubbo-go 中如何实现路由策略功能

joe

golang Apache 开源 微服务架构 dubbo

用python爬虫保存美国农业部网站上的水果图片

遇见

Python GitHub 爬虫

如何画一个闹钟

池建强

视觉笔记

如果明天没有恐惧——两小时看完余欢水后想到的……

伯薇

个人成长 心理学 小说 恐惧

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