分类目录

链接

2013 年 4 月
1234567
891011121314
15161718192021
22232425262728
2930  

近期文章

热门标签

新人福利,免费薅羊毛

现在位置:    首页 > .NET > 正文
C#函数式程序编程2
.NET 暂无评论 阅读(2,070)

主要内容

  ActionFunc类型介绍,在函数内部定义函数与返回函数,闭包与函数柯里化,高阶函数与Linq应用。

 

第一部分 ActionFunc类型介绍

  近来有一些人问我Action和Func类型是什么意思,为了整篇文章知识体系的完整性,先来给大家做一番介绍(如果你熟悉这两个类型,请跳过这部分)。

首先来看这样一个JavaScript函数:

  1. function sum(n1, n2) {
  2.     return n1 + n2;
  3. }

我们知道,在JavaScript当中,函数是可以赋值为一个变量的,即:

  1. var sum = function(n1, n2) {
  2.     return n1 + n2;
  3. }

定义这个“变量”之后,我们可以通过sum(1,2)的方式调用这个函数。那么,如果javaScript是一种强类型语言的话,这个var是什么类型呢?

来看一下这个函数的C#代码:

  1. static int Sum(int n1, int n2)
  2. {
  3.     return n1 + n2;
  4. }

注意到这个函数接收了两个int型参数,返回了一个int值。那么,它的类型就是Func<int,int,int>,即它的等效代码为:

  1. Func<int,int,int> Sum = (int n1, int n2) => {
  2.     return n1 + n2;
  3. };

我们可以F12一下,看到Func类的定义如下:

  1. 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>的定义:

  1. 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这样蹩脚的形式呢?下面来看一个例子:

复制代码
  1. static void DoSth()
  2. {
  3.     //前置逻辑
  4.     if (Validate())
  5.     { 
  6.         //后续逻辑
  7.     }
  8. }
  9.  
  10. static bool Validate()
  11. {
  12.     //校验逻辑
  13.     return true;
  14. }
复制代码

也许这个例子不够恰当,但是足以说明问题,我想略有经验的程序员都明白将校验方法(或者说,比较方法,嗯)重构到一个新函数里,这样能让程序脉 络清晰,《重构》当中也提到了这一手段,但是有没有意识到这种做法有一个诟病:这两个方法处于一个类环境当中,通常来说DoSth方法是publish 的,那么为了重构,我们不得不在这个类环境当中搞出一个private方法来支撑这个public方法,显然这个private方法没有什么可复用性可 言,而且它污染了整个类空间,再说从面向对象的角度来看,校验成了我这个类要承担的职责,这岂不是很诡异?

那么我要做的,就是在提取这个Validate方法的前提下,保证这个方法别污染类空间。那么一个切实可行的办法,就是把这个校验函数定义在DoSth的内部,代码如下:

复制代码
  1. static void DoSth()
  2. {
  3.     Func<bool> Validate = () => {
  4.         //校验逻辑
  5.         return true;
  6.     };
  7.  
  8.     //前置逻辑
  9.     if (Validate())
  10.     { 
  11.         //后续逻辑
  12.     }
  13. }
复制代码

这段代码把校验函数定义为了DoSth内部的一个变量,它的生存期就在DoSth内部,这样一来就丝毫不会影响类的结构了。这就是Func的应用之一——在函数内部定义局部函数。

但是这样还是让人觉得很啰嗦,这个Validate完全可以在其他地方定义,然后作为参数传进来,比如这样:

复制代码
  1. static void DoSth(Func<bool> Validate)
  2. {
  3.     //前置逻辑
  4.     if (Validate())
  5.     {
  6.         //后续逻辑
  7.     }
  8. }
复制代码

如此一来,这个校验方法就可以定义在其他地方了,这就给我们做一些面向对象方面的方便(比如通过依赖注入搞到这个函数),当然也可以在调用的时候直接在参数里写lambda表达式:

复制代码
  1. static void Main(string[] args)
  2. {
  3.     DoSth(() => { 
  4.         //校验逻辑
  5.         return true;
  6.     });
  7. }
复制代码

可能这个“校验”的例子举得不是很恰当,但是这已经足够说明Func作为参数的用法。

如果你怀疑这种手段的实际价值,想想JavaScript里的SetTimeout的第二个参数吧!所谓的回调函数,就是一种由框架调用由客户端实现的函数,用这种写法可以大大增加客户端代码的直观性与灵活性!

既然Func类型可以作为函数的参数,那么它可不可以作为函数返回值呢?答案必然是肯定的,我们还是来看一个加法例子:

  1. static Func<int, Func<int, int>> Sum = n1 => {
  2.     return n2 => n1 + n2;
  3. };

观察返回值类型Func<int, Func<int,int>>,它表示这个函数接受一个int型参数,返回一个Func<int,int>,也就是返回一 个接受int类型参数,返回int类型值的函数。即,Sum是一个返回函数的函数。

那么这个函数如何使用呢?观察下列主函数:

复制代码
  1. static void Main(string[] args)
  2. {
  3.     var Sum5 = Sum(5);
  4.     int result = Sum5(10);
  5.  
  6.     Console.WriteLine(result);
  7.     Console.ReadKey();
  8. }
复制代码

首先,我们通过Sum(5)的方式,返回了一个Sum5变量,这个变量的类型是Func<int,int>,也就是说,我们通过Sum 函数返回了Sum5函数。接下来调用这个新函数Sum5(10),得到了答案15。当然,接下来我还可以调用Sum5(20)得到25。

自然地,这个调用可以写成Sum(5)(10),与原本的Sum(5,10)相比,新的写法将两个参数拆解到了多个括号之中分部调用。聪明的你 一定能发现这么做的好处,就是把这个参数解耦,让各个算法(函数)之间有更高的灵活性和可复用性。但是要注意的是,要得到最终的结果,参数的数量依旧是一 个都不能少的。

另外,你有没有从这里嗅出一些“重载”的味道?

 

第三部分 闭包与函数柯里化

  不要被这个标题吓倒,嗯!我们来改写一下刚才的代码:

复制代码
  1. static void Main(string[] args)
  2. {
  3.     var Sum5 = Sum();
  4.     int result = Sum5(10);
  5.  
  6.     Console.WriteLine(result);
  7.     Console.ReadKey();
  8. }
  9.  
  10. static Func<Func<int, int>> Sum = () => {
  11.     int n1 = 5;
  12.     return n2 => n1 + n2;
  13. };
复制代码

这次我们让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的定义,可以看到:

复制代码
  1. [CompilerGenerated]
  2. private static Func<int, int> <.cctor>b__0()
  3. {
  4.     <>c__DisplayClass3 CS$<>8__locals4;
  5.    
 return new Func<int, int>(CS$<>8__locals4, (IntPtr) this.<.cctor>b__1);
}
复制代码

奇怪的是,在这个函数的第一句话,定义了一个“<>c__DisplayClass3”匿名类的对象,也就是说,Sum5这个函数的内部携带着这个对象,想必5这个数字就保存在这个类里,来看这个类的定义:

复制代码
  1. [CompilerGenerated]
  2. private sealed class <>c__DisplayClass3
  3. {
  4.     public int n1;
  5.  
  6.     public int <.cctor>b__1(int n2)
  7.     {
  8.         return (this.n1 + n2);
  9.     }
  10. }
复制代码

看到这里我想我不用再解释什么了吧。

观察我们的函数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函数:接受一个转换函数和一个集合,对这个集合中的每个元素,延迟返回它执行转换函数后的值。

复制代码
  1. static IEnumerable<TR> Map<T, TR>(Converter<T, TR> select, IEnumerable<T> list)
  2. {
  3.     foreach (T val in list)
  4.     {
  5.         yield return select(val);
  6.     }
  7. }
复制代码

其中Converter是一个委托,它接受一种类型的参数,返回另一种类型的参数,也就是说如果有一个Converter类型的函数,其作用就是将一种类型转换为另一种类型,当然,在使用的时候,我们可以传递一个很复杂的类,返回其中的某个字段。

  1. public delegate TOutput Converter<in TInput, out TOutput>(TInput input);

(2)Filter函数:接受一个布尔函数作为判断条件,作用在一个集合上,延迟返回这个集合当中满足条件的元素。

复制代码
  1. static IEnumerable<T> Filter<T>(Predicate<T> selector, IEnumerable<T> list)
  2. {
  3.     foreach (T val in list)
  4.     {
  5.         if (selector(val))
  6.         {
  7.             yield return val;
  8.         }
  9.     }
  10. }
复制代码

(3)Fold函数:接受一个返回TR类型的算法函数,一个TR类型的起始值,及一个集合,对这个集合中的所有值应用这一算法,并”折叠“到返回值上返回。

复制代码
  1. static TR Fold<T, TR>(Func<TR, T, TR> accumulator, TR startVal, IEnumerable<T> list)
  2. {
  3.     TR result = startVal;
  4.     foreach (T val in list)
  5.     {
  6.         result = accumulator(result, val);
  7.     }
  8.     return result;
  9. }
复制代码

大家有没有看出这三个函数有什么猫腻?它们都有一个IEnumerable<T>的参数,那么下面我们就把他们改造为扩展方法,并且改个名:

复制代码
  1. public static partial class Enumerable
  2. {
  3.     public static IEnumerable<TR> Select<T, TR>(this IEnumerable<T> list, Converter<T, TR> selectField)
  4.     {
  5.         foreach (T val in list)
  6.         {
  7.             yield return selectField(val);
  8.         }
  9.     }
  10.  
  11.     public static IEnumerable<T> Where<T>(this IEnumerable<T> list, Predicate<T> selector)
  12.     {
  13.         foreach (T val in list)
  14.         {
  15.             if (selector(val))
  16.             {
  17.                 yield return val;
  18.             }
  19.         }
  20.     }
  21.  
  22.     public static TR Sum<T, TR>(this IEnumerable<T> list, Func<TR, T, TR> accumulator, TR startVal)
  23.     {
  24.         TR result = startVal;
  25.         foreach (T val in list)
  26.         {
  27.             result = accumulator(result, val);
  28.         }
  29.         return result;
  30.     }
  31. }
复制代码

我们可以这样使用:

复制代码
  1. static void Main(string[] args)
  2. {
  3.     IEnumerable<int> list = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8 };
  4.  
  5.     list.Where(num => num % 2 == 0)
  6.         .Select(num => num)
  7.         .ToList().ForEach(num => {  //这里就直接调用Linq了
  8.             Console.WriteLine(num);
  9.         });
  10.  
  11.     int sum = list.Where(num => num % 2 == 0)
  12.         .Sum((x, y) => x + y, 0);
  13.     Console.WriteLine("sum=" + sum);
  14.  
  15.     Console.ReadKey();
  16. }
复制代码

这基本和Linq没有什么差别了,嗯,其实Linq里就是这么搞的,只是它更加丰富和严谨,依旧不用多解释了。

 

============ 欢迎各位老板打赏~ ===========

本文版权归Bruce's Blog所有,转载引用请完整注明以下信息:
本文作者:Bruce
本文地址:C#函数式程序编程2 | Bruce's Blog

发表评论

留言无头像?