好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

C#函数式程序设计初探基础理论篇

C#函数式程序设计初探基础理论篇

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>>就叫柯里化,也是起个名字唬人的,就像“面向切面编程”这个名字一样!为什么叫柯里化?因为它是一个叫Curry的家伙发明的!

第四部分 高阶函数与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里就是这么搞的,只是它更加丰富和严谨,依旧不用多解释了。

后记

  相信大家读完这篇文章之后已经对函数式编程有了一个初步的认识,函数式还有很多精彩的应用,请关注下回分解!

作者: Leo_wl

    

出处: http://HdhCmsTestcnblogs测试数据/Leo_wl/

    

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

版权信息

查看更多关于C#函数式程序设计初探基础理论篇的详细内容...

  阅读:54次