中国领先的IT技术网站
|
|

C#2.0之殇,以及函数式编程的未来

本文通过对C#2.0和C#3.0的比较,探讨了函数式语言的优势。有人说函数式编程有什么用,C#3.0就是个很好的证明。

作者:来源:博客园|2009-07-24 17:31


似乎还有不少项目在用C#2.0,但是C#2.0的生产力实在不如C#3.0——如果您不信,那么一会儿就会意识到这一点。有朋友认为语言能力不重要,有了好用的框架/类库也可以有很高的生产力。所以这篇文章,我们就设法使用“类库”来弥补C#2.0的缺陷。

但是,我们真做的到吗?

C#2.0之殇

C#2.0较C#1.0来说是一个突破,其中引入了泛型,以及匿名方法等新特性。如果前者还可以说是平台的增强,而语言只是个“辅助”的话,而后者则百分之一百是编译器的魔法了。别小看这个特性,它为C#3.0的高生产力踏出了坚实的一步——不过还是差了很多。例如,我们有一个要求:“把一个字符串数组中的元素转化为整数,再将其中的偶数放入一个List< int>容器中”。如果是C#3.0,这是再简单不过的功能:

  1. string[]strArray={"1","2","3","4"};  
  2. vareven=strArray.Select(s=>Int32.Parse(s)).Where(i=>i%2==0).ToList();  

那么对于C#2.0(当然对于C#1.0也一样),代码又该怎么写呢?

  1. List< int>even=newList< int>();  
  2. foreach(stringsinstrArray)  
  3. {  
  4. inti=Int32.Parse(s);  
  5. if(i%2==0)  
  6. {  
  7. even.Add(i);  
  8. }  
  9. }  

有人说函数式编程有什么用,C#3.0就是个很好的证明。C#3.0中引入了Lambda表达式,增强了在语言中构造匿名方法的能力——这是一个语言中函数式编程特性的必备条件。C#3.0的实现与C#2.0相比,可读性高,可以直接看出转化、过滤,以及构造容器的过程和标准。由于语言能力的增强,程序的表现能力得到了很大的提高,在很多时候,我们可以省去将一些代码提取为独立方法的必要。当然,即使您将其提取为额外的方法,C#3.0也可以让您写出更少的代码。

如果您觉得以上代码的差距还不是过于明显的话——那么以下功能呢?

  1. int[]intArray={1,2,3,4,5,6,7,8,9,10};  
  2.  
  3. //所有偶数的平均数  
  4.  
  5. varevenAverage=intArray.Where(i=>i%2==0).Average();  
  6.  
  7. //都是偶数?  
  8.  
  9. varallEven=intArray.All(i=>i%2==0);  
  10.  
  11. //包含偶数?  
  12.  
  13. varcontainsEven=intArray.Any(i=>i%2==0);  
  14.  
  15. //第4到第8个数  
  16.  
  17. varfourthToEighth=intArray.Skip(3).Take(5);  

如果您使用C#2.0来写,您会怎么做?

拯救C#2.0

C#3.0通过引入了函数式编程特性大幅增强了语言的生产力。如果说C#2.0和Java还没有太大差距的话,那么C#3.0已经将Java甩开地太远太远。不过真要说起来,在Java中并非不可以加入函数式编程的理念。只不过,如果没有足够的语言特性进行支持(如快速构造匿名函数、闭包、一定程度的类型推演等等),函数式编程对于某些语言来说几乎只能成为“理念”。不过现在,我们暂且先放下对“函数式编程”相关内容的探索,设法拯救C#2.0所缺失的生产力吧。

C#3.0中可以使用Lambda表达式构造一个匿名函数,这个能力其实在C#2.0中也有。我们姑且认为这点不是造成差距的主要原因,那么有一点是C#2.0绝对无法实现的,那就是“扩展方法”。C#3.0中的扩展方法,可以“零耦合”地为一个,甚至一系列类型添加“实例方法”。当然,这也是编译器的功能,实际上我们只是定义了一些静态方法而已。这一点在C#2.0中还是可以做到的:

  1. publicclassEnumerable  
  2.  
  3. {  
  4.  
  5. publicstaticIEnumerable< T>Where< T>(Func< T,bool>predicate,IEnumerable< T>source)  
  6.  
  7. {  
  8.  
  9. foreach(Titeminsource)  
  10.  
  11. {  
  12.  
  13. if(predicate(item))  
  14.  
  15. {  
  16.  
  17. yieldreturnitem;  
  18.  
  19. }  
  20.  
  21. }  
  22.  
  23. }  
  24.  
  25. publicstaticIEnumerable< TResult>Select< T,TResult>(Func< T,TResult>selector,IEnumerable< T>source)  
  26.  
  27. {  
  28.  
  29. foreach(Titeminsource)  
  30.  
  31. {  
  32.  
  33. yieldreturnselector(item);  
  34.  
  35. }  
  36.  
  37. }  
  38.  
  39. publicstaticList< T>ToList< T>(IEnumerable< T>source)  
  40.  
  41. {  
  42.  
  43. List< T>list=newList< T>();  
  44.  
  45. foreach(Titeminsource)  
  46.  
  47. {  
  48.  
  49. list.Add(item);  
  50.  
  51. }  
  52.  
  53. returnlist;  
  54.  
  55. }  
  56.  
  57. }  

于是现在,我们便可以换种写法来实现相同的功能了:

  1. string[]strArray={"1","2","3","4"};  
  2.  
  3. List< int>even=  
  4.  
  5. Enumerable.ToList(  
  6.  
  7. Enumerable.Where(  
  8.  
  9. delegate(inti){returni%2==0;},  
  10.  
  11. Enumerable.Select(  
  12.  
  13. delegate(strings){returnInt32.Parse(s);},  
  14.  
  15. strArray)));  

即使您可以接受delegate关键字构造匿名函数的能力,但是上面的做法还是有个天生的缺陷:逻辑与表现的次序想反。我们想表现的逻辑顺序为:转化(Select)、过滤(Where)、及容器构造(ToList),C#3.0所表现出的顺序和它相同,而C#2.0的顺序则相反。由于语言能力的缺失,这个差距无法弥补。很多时候,语言的一些“小功能”并不能说是可有可无的特性,它很可能直接决定了是否可以用某种语言来构造InternalDSL或进行BDD。例如,由于F#的灵活语法,FsTest使得开发人员可以写出"foobar"|>shouldcontains"foo"这样的语句来避免机械的Assert语法。同样,老赵也曾经使用actor< =msg这样的逻辑来替代actor.Post(msg)的显式调用方式。

封装逻辑

既然没有“扩展方法”,我们要避免静态方法的调用形式,那么就只能在一个类中定义逻辑了。这点并不困难,毕竟在API的设计发展至今,已经进入了关注FluentInterface的阶段,这方面已经积累了大量的实践。于是我们构造一个Enumerable< T>类,封装IEnumerable< T>对象,以此作为扩展的入口:

  1. publicclassEnumerable< T>  
  2.  
  3. {  
  4.  
  5. privateIEnumerable< T>m_source;  
  6.  
  7. publicEnumerable(IEnumerable< T>source)  
  8.  
  9. {  
  10.  
  11. if(source==null)thrownewArgumentNullException("source");  
  12.  
  13. this.m_source=source;  
  14.  
  15. }  
  16.  
  17. ...  
  18.  
  19. }  
  20.  
  21. 并以此定义所需的Select和Where方法:  
  22.  
  23. publicEnumerable< T>Where(Func< T,bool>predicate)  
  24.  
  25. {  
  26.  
  27. if(predicate==null)thrownewArgumentNullException("predicate");  
  28.  
  29. returnnewEnumerable< T>(Where(this.m_source,predicate));  
  30.  
  31. }  
  32.  
  33. privatestaticIEnumerable< T>Where(IEnumerable< T>source,Func< T,bool>predicate)  
  34.  
  35. {  
  36.  
  37. foreach(Titeminsource)  
  38.  
  39. {  
  40.  
  41. if(predicate(item))  
  42.  
  43. {  
  44.  
  45. yieldreturnitem;  
  46.  
  47. }  
  48.  
  49. }  
  50.  
  51. }  
  52.  
  53. publicEnumerable< TResult>Select< TResult>(Func< T,TResult>selector)  
  54.  
  55. {  
  56.  
  57. if(selector==null)thrownewArgumentNullException("selector");  
  58.  
  59. returnnewEnumerable< TResult>(Select(this.m_source,selector));  
  60.  
  61. }  
  62.  
  63. privatestaticIEnumerable< TResult>Select< TResult>(IEnumerable< T>source,Func< T,TResult>selector)  
  64.  
  65. {  
  66.  
  67. foreach(Titeminsource)  
  68.  
  69. {  
  70.  
  71. yieldreturnselector(item);  
  72.  
  73. }  
  74.  
  75. }  

这些扩展都是些高阶函数,也都有延迟效果,相信很容易理解,在此就不多作解释了。在这里我们直接观察其使用方式:

  1. List< int>even=newEnumerable< string>(strArray)  
  2.  
  3. .Select(delegate(strings){returnInt32.Parse(s);})  
  4.  
  5. .Where(delegate(inti){returni%2==0;})  
  6.  
  7. .ToList();  

不知道您对此有何感觉?

老赵对此并不满意,尤其是和C#3.0相较之下。我们虽然定义了Enumerable封装类,并提供了Select和Where等逻辑,但是由于匿名函数的构造还是较为丑陋。使用delegate构造匿名函数还是引起了不少噪音:

与JavaScript的function关键字,和VB.NET的Function关键字一样,C#2.0在构造匿名函数时无法省确delegate关键字。

与C#3.0中的Lambda表达式相比,使用delegate匿名函数缺少了必要的类型推演。

使用delegate构造匿名函数时必须提供完整的方法体,也就是只能提供“语句”,而不能仅为一个“表达式”,因此return和最后的分号无法省确。

我们设法拯救C#2.0,但是我们真的做到了吗?

框架/类库真能弥补语言的生产力吗?

【编辑推荐】

  1. 浅谈CLR 4.0安全模型的运作机制
  2. 探秘CLR 4.0中的代码契约
  3. CLR线程池的作用与原理浅析
  4. SQL Server 2005中的CLR集成
  5. CLR 4.0中的新内容 状态错乱异常
【责任编辑:杨赛 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

读 书 +更多

安全模式:J2EE、Web服务和身份管理最佳实践与策

本书全面阐述Java应用安全的基本知识并介绍一种强大的结构化安全设计方法;介绍独立于厂商的安全架构;列出详细的评估核对表以及23种经过实...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊
× 学习达标赢Beats耳机