高阶函数、委托与匿名方法
作者 赵劼 发布于 2009年4月17日 下午6时35分
高阶函数(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; } }
CacheHelper
的
Get
方法接受三个委托对象作为参数,只有当第一个方法(从缓存中获取对象)返回为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开 发人员的必备技术。