C#函数式程序设计初探——基础理论篇
篇首语
近来发现园子里有不少人在讨论函数式相关的问题,从个人性格来讲,我不爱看学术气氛太强的东西,从责任上来讲,我认为也有必要写一篇“干货”把函数式这个问题说得明白一些,也作为自己的一个知识沉淀,于是便有了此文。
个人认为,C#语言的某些设计并不非常适合函数式开发,比如它的类型推断并不是很近乎人意,我们知道C#还是主打面向对象的,不过这并不妨碍我们用C#来讨论函数式,至少可以借鉴函数式的一些思路来优化我们的代码。
我希望通过这篇文章让读者通过简单的例子,在短时间内掌握基本函数式编程方法,了解Action与Func类型的使用。同时我希望读者对C#泛型集合、Linq、lambda表达式和yield关键字有所了解。
主要内容
Action与Func类型介绍,在函数内部定义函数与返回函数,闭包与函数柯里化,高阶函数与Linq应用。
第一部分 Action与Func类型介绍
近来有一些人问我Action和Func类型是什么意思,为了整篇文章知识体系的完整性,先来给大家做一番介绍(如果你熟悉这两个类型,请跳过这部分)。
首先来看这样一个JavaScript函数:
function sum(n1, n2) { return n1 + n2; }
我们知道,在JavaScript当中,函数是可以赋值为一个变量的,即:
var sum = function(n1, n2) { return n1 + n2; }
定义这个“变量”之后,我们可以通过sum(1,2)的方式调用这个函数。那么,如果javaScript是一种强类型语言的话,这个var是什么类型呢?
来看一下这个函数的C#代码:
static int Sum(int n1, int n2) { return n1 + n2; }
注意到这个函数接收了两个int型参数,返回了一个int值。那么,它的类型就是Func<int,int,int>,即它的等效代码为:
Func<int,int,int> Sum = (int n1, int n2) => { return n1 + n2; };
我们可以F12一下,看到Func类的定义如下:
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
这个类型实质上是一个委托,返回值是个泛型的TResult,从定义的参数表可以看出,前两个类型T1和T2是传入参数的类型,第三个类型是返回值类型。
根据这个道理,假设有一个Func<int,string,bool>型的变量,它表示一个委托,这个委托内包含了这样一个函数:该函数的两个参数是int和string类型,返回值为bool。当Func<TResult>只有一个类型参数时,TResult表示返回值类型,即Func<bool>表示一个委托,它的参数表为空,返回值为bool类型。为了方便说明,下文将委托与函数两个概念通通使用“函数”来表示。
猜猜看Func<object, Func<string,bool>>表示什么呢?它表示一个函数,接受一个object类型的参数,返回一个Func<string,bool>。这里可以看出,函数也是可以作为函数的返回值的。
接下来看Action,我们F12一下看Action<T>的定义:
public delegate void Action<in T>(T obj);
注意到委托的返回值为void,那么实际上Action就是一个没有返回值,只有参数表的委托,即Action<T1,T2>等价于Func<T1,T2,void>。
最后来说一下Predicate<T>,当我们写Linq方法的Where()时,可以看到它要求传入了一个Predicate类型的参数,它实际上就是一个bool型委托,等价于Func<T,bool>。
这里只是对这几个委托关键字做一个铺垫性的介绍,大家可以去网上搜这两个关键字的用法相关的帖子,如果没搞懂请不要往下看。
第二部分 在函数内部定义函数与返回函数
那么有人该问了,好好的一个函数,干嘛非写成Func这样蹩脚的形式呢?下面来看一个例子:
static void DoSth() { //前置逻辑 if (Validate()) { //后续逻辑 } } static bool Validate() { //校验逻辑 return true; }
也许这个例子不够恰当,但是足以说明问题,我想略有经验的程序员都明白将校验方法(或者说,比较方法,嗯)重构到一个新函数里,这样能让程序脉络清晰,《重构》当中也提到了这一手段,但是有没有意识到这种做法有一个诟病:这两个方法处于一个类环境当中,通常来说DoSth方法是publish的,那么为了重构,我们不得不在这个类环境当中搞出一个private方法来支撑这个public方法,显然这个private方法没有什么可复用性可言,而且它污染了整个类空间,再说从面向对象的角度来看,校验成了我这个类要承担的职责,这岂不是很诡异?
那么我要做的,就是在提取这个Validate方法的前提下,保证这个方法别污染类空间。那么一个切实可行的办法,就是把这个校验函数定义在DoSth的内部,代码如下:
static void DoSth() { Func<bool> Validate = () => { //校验逻辑 return true; }; //前置逻辑 if (Validate()) { //后续逻辑 } }
这段代码把校验函数定义为了DoSth内部的一个变量,它的生存期就在DoSth内部,这样一来就丝毫不会影响类的结构了。这就是Func的应用之一——在函数内部定义局部函数。
但是这样还是让人觉得很啰嗦,这个Validate完全可以在其他地方定义,然后作为参数传进来,比如这样:
static void DoSth(Func<bool> Validate) { //前置逻辑 if (Validate()) { //后续逻辑 } }
如此一来,这个校验方法就可以定义在其他地方了,这就给我们做一些面向对象方面的方便(比如通过依赖注入搞到这个函数),当然也可以在调用的时候直接在参数里写lambda表达式:
static void Main(string[] args) { DoSth(() => { //校验逻辑 return true; }); }
可能这个“校验”的例子举得不是很恰当,但是这已经足够说明Func作为参数的用法。
如果你怀疑这种手段的实际价值,想想JavaScript里的SetTimeout的第二个参数吧!所谓的回调函数,就是一种由框架调用由客户端实现的函数,用这种写法可以大大增加客户端代码的直观性与灵活性!
既然Func类型可以作为函数的参数,那么它可不可以作为函数返回值呢?答案必然是肯定的,我们还是来看一个加法例子:
static Func<int, Func<int, int>> Sum = n1 => { return n2 => n1 + n2; };
观察返回值类型Func<int, Func<int,int>>,它表示这个函数接受一个int型参数,返回一个Func<int,int>,也就是返回一个接受int类型参数,返回int类型值的函数。即,Sum是一个返回函数的函数。
那么这个函数如何使用呢?观察下列主函数:
static void Main(string[] args) { var Sum5 = Sum(5); int result = Sum5(10); Console.WriteLine(result); Console.ReadKey(); }
首先,我们通过Sum(5)的方式,返回了一个Sum5变量,这个变量的类型是Func<int,int>,也就是说,我们通过Sum函数返回了Sum5函数。接下来调用这个新函数Sum5(10),得到了答案15。当然,接下来我还可以调用Sum5(20)得到25。
自然地,这个调用可以写成Sum(5)(10),与原本的Sum(5,10)相比,新的写法将两个参数拆解到了多个括号之中分部调用。聪明的你一定能发现这么做的好处,就是把这个参数解耦,让各个算法(函数)之间有更高的灵活性和可复用性。但是要注意的是,要得到最终的结果,参数的数量依旧是一个都不能少的。
另外,你有没有从这里嗅出一些“重载”的味道?
第三部分 闭包与函数柯里化
不要被这个标题吓倒,嗯!我们来改写一下刚才的代码:
static void Main(string[] args) { var Sum5 = Sum(); int result = Sum5(10); Console.WriteLine(result); Console.ReadKey(); } static Func<Func<int, int>> Sum = () => { int n1 = 5; return n2 => n1 + n2; };
这次我们让Sum不再接收第一个参数了,而把n1定义在Sum方法的内部,调用就变成了Sum()(10),大家可以试一下,结果依旧输出15,一切看似很自然,不过请你反复读一读Sum的定义,是不敢觉得似乎少了点什么?希望你停下来多读几遍再往下看!
问题就出在n1的定义,请回答一个问题,变量n1的生存范围是多大?Sum函数返回的时候,n1既然是Sum的内部的局部变量,应该就被释放掉了,那么我调用Sum5(10)的时候,被释放掉的5是从哪里来的呢?
在解释这个问题之前,我想你应该可以理解“Func<Func<int,int>> Sum = xxx”这种写法,等价于“Func<int,int> Sum() { xxx }”,如果不理解,请停下来,把上面的部分再看一遍。
我们打开反编译器对这个Sum的定义,可以看到:
[CompilerGenerated] private static Func<int, int> <.cctor>b__0() { <>c__DisplayClass3 CS$<>8__locals4; return new Func<int, int>(CS$<>8__locals4, (IntPtr) this.<.cctor>b__1); }
奇怪的是,在这个函数的第一句话,定义了一个“<>c__DisplayClass3”匿名类的对象,也就是说,Sum5这个函数的内部携带着这个对象,想必5这个数字就保存在这个类里,来看这个类的定义:
[CompilerGenerated] private sealed class <>c__DisplayClass3 { public int n1; public int <.cctor>b__1(int n2) { return (this.n1 + n2); } }
看到这里我想我不用再解释什么了吧。
观察我们的函数n2=>n1+n2,它能够拿到外部函数Sum中的n1,而Sum却不能拿到它内部的n2,这一类的函数,起个名字——闭包。于是现在你稍微理解JavaScript中那个叫作用域链的东西了吗?
嗯,这部分的标题上提到了函数的柯里化,那什么是柯里化呢?其实刚才已经看过了,把Sum(5,10,15,20)写成Sum(5)(10)(15)(20)就叫柯里化,或者说把Func<int,int,int>搞成Func<int, Func<int,int>>就叫柯里化,也是起个名字唬人的,就像“面向切面编程”这个名字一样!
第四部分 高阶函数与Linq应用
现在进入理论篇的最后一部分,神马叫高阶函数?还就是起个名字而已,以其他函数做参数、或者返回一个函数的函数,就叫高阶函数,刚才的Sum就是高阶函数。至此大家已经了解了如何在函数中调用一个作为参数的函数,为了给后面的应用篇做铺垫,这里介绍几个经典的高阶函数,希望大家都能理解。
(1)Map函数:接受一个转换函数和一个集合,对这个集合中的每个元素,延迟返回它执行转换函数后的值。
static IEnumerable<TR> Map<T, TR>(Converter<T, TR> select, IEnumerable<T> list) { foreach (T val in list) { yield return select(val); } }
其中Converter是一个委托,它接受一种类型的参数,返回另一种类型的参数,也就是说如果有一个Converter类型的函数,其作用就是将一种类型转换为另一种类型,当然,在使用的时候,我们可以传递一个很复杂的类,返回其中的某个字段。
public delegate TOutput Converter<in TInput, out TOutput>(TInput input);
(2)Filter函数:接受一个布尔函数作为判断条件,作用在一个集合上,延迟返回这个集合当中满足条件的元素。
static IEnumerable<T> Filter<T>(Predicate<T> selector, IEnumerable<T> list) { foreach (T val in list) { if (selector(val)) { yield return val; } } }
(3)Fold函数:接受一个返回TR类型的算法函数,一个TR类型的起始值,及一个集合,对这个集合中的所有值应用这一算法,并”折叠“到返回值上返回。
static TR Fold<T, TR>(Func<TR, T, TR> accumulator, TR startVal, IEnumerable<T> list) { TR result = startVal; foreach (T val in list) { result = accumulator(result, val); } return result; }
大家有没有看出这三个函数有什么猫腻?它们都有一个IEnumerable<T>的参数,那么下面我们就把他们改造为扩展方法,并且改个名:
public static partial class Enumerable { public static IEnumerable<TR> Select<T, TR>(this IEnumerable<T> list, Converter<T, TR> selectField) { foreach (T val in list) { yield return selectField(val); } } public static IEnumerable<T> Where<T>(this IEnumerable<T> list, Predicate<T> selector) { foreach (T val in list) { if (selector(val)) { yield return val; } } } public static TR Sum<T, TR>(this IEnumerable<T> list, Func<TR, T, TR> accumulator, TR startVal) { TR result = startVal; foreach (T val in list) { result = accumulator(result, val); } return result; } }
我们可以这样使用:
static void Main(string[] args) { IEnumerable<int> list = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8 }; list.Where(num => num % 2 == 0) .Select(num => num) .ToList().ForEach(num => { //这里就直接调用Linq了 Console.WriteLine(num); }); int sum = list.Where(num => num % 2 == 0) .Sum((x, y) => x + y, 0); Console.WriteLine("sum=" + sum); Console.ReadKey(); }
这基本和Linq没有什么差别了,嗯,其实Linq里就是这么搞的,只是它更加丰富和严谨,依旧不用多解释了。
后记
相信大家读完这篇文章之后已经对函数式编程有了一个初步的认识,函数式还有很多精彩的应用,请关注下回分解!