装箱百万奖金,第六届全国工业互联网数据创新应用大赛火热报名中! 了解详情
写点什么

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

  • 2009-04-17
  • 本文字数:4380 字

    阅读完需:约 14 分钟

高阶函数(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-04-17 18:359857
用户头像

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

关注

评论

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

mysql数据表查询

乌龟哥哥

7 月月更

初学者如何快速的上手Linux命令,这34条新手必会的命令一定得会!

wljslmz

Linux 7月月更

关于目前流行的 Redis 可视化管理工具的详细评测

宁在春

redis 7月月更 Redis 可视化工具

Flutter开发:运行flutter upgrade命令报错Exception:Flutter failed to create a directory at…解决方法

三掌柜

7月月更

SDL图像显示

柒号华仔

7月月更

值得收藏的ArkUI框架三方组件【系列1】

坚果

HarmonyOS Open Harmony 7月月更

为什么加工数据指标

奔向架构师

数据仓库 7月月更

jQuery 操作元素

Jason199

jquery js 7月月更

通过Dao投票STI的销毁,SeekTiger真正做到由社区驱动

股市老人

分库分表

ES_her0

7月月更

Android实现无序树形结构图,类似思维导图和级联分层图(无序,随机位置)

芝麻粒儿

android 7月月更

函数初认识-上

芒果酱

C语言 7 月月更

数据库的主从分离

ES_her0

7月月更

深度学习-多维数据和tensor

AIWeker

7月月更 多维数据

常见链表题及其 Go 实现

宇宙之一粟

链表 7月月更

JavaScript DOM编程艺术笔记

程序员海军

前端 DOM 7 月月更

【萌新解题】三数之和

面试官问

面试 LeetCode

Android ANR和OOM

沃德

android 程序员 7月月更

CodeTON Round 1 (Div. 1 + Div. 2, Rated, Prizes)(A-C)

KEY.L

7月月更

通过Dao投票STI的销毁,SeekTiger真正做到由社区驱动

鳄鱼视界

队列的链式表示和实现

秋名山码民

算法 7 月月更

Block 的分类

NewBoy

ios 前端 移动端 iOS 知识体系 7月月更

Android热更新调研汇总

沃德

android 程序员 7月月更

【Docker 那些事儿】容器网络(上篇)

Albert Edison

Docker Kubernetes 容器 云原生 7月月更

解读《深入理解计算机系统(CSAPP)》第12章并发编程

小明Java问道之路

Java 后端 并发 csapp 7月月更

带领全网朋友,完成粉笔登录加密分析,再次换种玩法

梦想橡皮擦

Python 爬虫 7月月更

zookeeper-认识watcher

zarmnosaj

7月月更

LeetCode 242:有效的字母异位词

武师叔

7月月更

软核微处理器

贾献华

7月月更

还在为处理事务烦恼吗,要不试试Spring是如何处理业务的

Java学术趴

7月月更

通过Dao投票STI的销毁,SeekTiger真正做到由社区驱动

威廉META

高阶函数、委托与匿名方法_.NET_赵劼_InfoQ精选文章