有人留意到我左边的阅读栏,发现我一直在阅读非技术书籍,于是来信询问我的书架都有哪些书籍。你是否也想好奇一下?

 

posted @ 2008-09-07 12:06 Allen Lee 阅读(80) | 评论 (1)编辑

从C# 3.0到F#

 

Written by Allen Lee

 

缘起

当你看到这篇文章的标题时,你有什么感觉?是不是很想脱口而出:"到底搞什么飞机啊,我C#还没来得及用好,现在又搞个F#,还让不让人活啊?"《程序员修炼之道》曾经建议我们"learn at least one new language every year",但Gustavo Duarte却对这种建议提出质疑,并宣称"learning new programming languages is often a waste of time for professional programmers"。面对这种争论,你可能会显示出某种理性:除非我有需要(学习新的语言),否则我认为够用就可以了。那么,你什么时候会有需要?回想一下你的项目经历,是否发现,有权提出这种需要的往往不是你,而是你的项目,只要项目有需要,即使是老掉牙的语言你也得学。根据马斯洛的需要层次理论,如果你的项目已经让你忙得一塌糊涂了,那么你根本不会有闲情和兴致学习新的语言,而在现今这个讲求快速见效的社会里,或许只有专门研究语言的人才支付得起学习新的语言的代价了,但我并非专门研究语言的人,至少现在不是,那么,我为何要学习新的语言呢?

我曾经在杰拉尔德·温伯格(Gerald M. Weinberg)的《咨询的奥秘——成功提出和获得建议的指南》里读到一个有趣的"锤子法则"(The Law of The Hammer):

在圣诞节收到锤子做礼物的孩子会发现每样东西都需要敲打。

读完上面这句话之后,你的脑子里想着什么?或许你已经猜到我想说什么了,工具在为使用者带来便利的同时也会约束使用者解决问题的思路和方法,编程语言直接体现了对问题的抽象和表达,不同范式的编程语言则协助程序员从不同的角度把握问题,这也正是我学习新的语言的主要原因。要有持久的学习行为,学习动机应该是指向内部的,其道理和《七个心理寓言》的"动机的寓言:孩子在为谁而玩"所说的是一样的。那么,我又为何选择F#呢?其实,这完全是因为C# 3.0,我们知道C# 3.0向函数式编程借鉴了不少,所以在学习C# 3.0的时候,我突然萌生了想了解函数式编程语言的念头,后来,在一次偶遇中,我邂逅了F#。在学习F#的过程中,我发现许多C# 3.0的新功能的"影子"(有人说C# 3.0的新功能是从F#那里借鉴过来的,是真的吗?),于是萌生了写下这篇文章的念头。

 

如何创建类型?

人们在接触新事物时通常不会抛开现有的积累,换句话说,你的知识和经验会影响你如何接受新事物。如果你是一个有使用面向对象编程语言经验的程序员,那么你第一个想问的问题很可能就是:"我如何创建类型?"

F#支持一种叫做Record Type的类型,它和C#里使用自动属性定义的类有点像:

代码 1

而Book的实例化也是非常直观的:

代码 2

F#会根据给出的属性名字以及值的类型推断出你要实例化的类型是Book。读到这里,你可能会问:"如果有两个不同的类型定义了相同的属性呢?"虽然出现这种情况的概率不大,但若真的让你碰上了,你可以使用显式语法来实例化它:

代码 3

面向对象编程的一个特征是封装,狭义的封装是指封装对象的内部状态(广义的封装则是指封装系统的变化因素),而对象的内部状态在对象的生命周期里发生改变是很常见的,但当我们试图在F# Interactive(类似于Python的交互式控制台)里修改Price属性时却报告错误("<-"用于赋值,相当于C#的"="):

图 1

为什么会这样呢?原来,在F#里,对象默认是不可变的(immutable),就像.NET的字符串那样。修改Price属性可以看作创建一个新对象,把原对象的Title、Authors和Tags属性的值复制到新对象对应的属性,并为新对象的Price属性设置新的值:

代码 4

读到这里,你可能在想:虽然现在内存很便宜了,但也不至于要用这种方法来耗啊?在面向对象编程里,拥有和维护可变的内部状态是对象的一个很重要的特征,正因为这样我们得以完成许多复杂的操作,但也正是可变的内部状态提高了并发操作的复杂程度和处理代价。泛泛而谈可变对象和不可变对象孰优孰劣是没有意义的,对于一个给定的系统,一些对象适合设计成可变的,另一些则应该考虑设计成不可变的,从而使两者达到一定的平衡。

在C#里,对象默认是可变的,但你可以通过readonly关键字使某个(些)数据"固定"下来;F#刚好相反,对象默认是不可变的,但你可以通过mutable关键字使某个(些)数据"活动"起来。如果我把Book重新定义为:

代码 5

那么修改Price属性就不会报错了。如果你决定使对象可变,那么你就应该做好并发处理的工作。什么?你的程序是单机单核单线程的?那你可以掷硬币决定对象是可变的还是不可变的。

除了Record Type,F#还支持Discriminated Union、Tuple和Constructed Class Type,有兴趣的话不妨到F# Home看看。

 

如何初始化对象?

C# 3.0引入了对象初始化器和集合初始化器,F#也提供了类似的功能。举个例子,假设我想初始化System.Windows.Forms.ListViewItem,并且设置它的Text、Selected和ToolTipText属性,我可以这样:

代码 6

F#把这个功能叫做初始属性设置(initial property settings)或者可选属性设置(optional property settings)。上面代码等效于:

代码 7

从这里可以看出,使用这个功能的前提条件是要初始化的属性必须具有set访问器。读到这里,你可能会问:"如果我要调用的构造函数是有参数的呢?"那也没问题,举个简单的例子,假设你要调用接受一个字符串作为参数的那个构造函数,你可以这样:

代码 8

或者这样:

代码 9

第一种方法就是简单地把你要初始化的属性追加到构造函数的参数后面;而第二种方法则使用了F#的命名参数(Named Argument)功能。命名参数可以放在任何位置,例如Selected和ToolTipText之间,但匿名参数就必须按顺序放在要初始化的属性前面。读到这里,你可能会问:"如果我要调用的构造函数的参数和我要初始化的属性重名了呢?"你在考验F#的忍耐力吗(笑)?当然,这种情况是有可能出现,首先,F# 不允许同一个名字出现两次,不管它是构造函数的参数的名字还是属性的名字,所以你不可能鱼与熊掌兼得(即构造函数的参数和属性同时初始化);其次,一旦出现这种情况了,F#会优先考虑构造函数的参数。

我们探讨了如何初始化一个对象,那么初始化一组对象又是怎样的呢?在F#里,说到集合类型就不得不提Microsoft.FSharp.Collections.List<'a>了("'a"是F#的类型参数表示法)。假设我要实例化一组Book对象(Book的定义参见代码1),并把它们储存在List<'a>里,我可以这样:

代码 10

列表里的每个元素通过";"分割。F#能够结合元素的类型推断出列表的完整类型,在这里是List<Book>(也可以表示为Book list)。F#的List<'a>通常只在F#里使用,如果要访问.NET的类库或者和其他语言交互,那么你通常会考虑使用数组(F#的List<'a>和数组的语法非常接近,能看出其中的区别吗?):

代码 11

而对于整数列表,F#还支持区间表达式,你可以指定起始值和终止值:

图 2

甚至指定递增值(步长):

图 3

如果你有兴趣进一步了解F#的List<'a>,可以阅读Dustin Campbell《Why I Love F#: Lists - The Basics》Chris Smith《Mastering F# Lists》

 

如何外包逻辑?

有一次,我和两个朋友到东方既白吃饭,选餐的时候,其中一个考虑了很久,终于发话了:"椰香咖喱牛肉饭可不可以不要椰香?"服务员看着我的朋友,非常不好意思地说:"这是不可以的。"看到服务员的表情,我猜她应该是苦于不知如何向一个12岁的小朋友解释"烧饭的工作遵循了一套标准化的流程,这个流程是不能随意更改的"。此时,我的朋友大概在想:同样的钱,不能加东西可以理解,为什么连减东西也不可以呢?虽然他最后还是选了椰香咖喱牛肉饭,但我猜他心里肯定觉得东方既白做得太呆板了。试想一下,如果你打算使用我提供的Sort方法排序books2数组(参见代码11),却发现这个方法只接受一个数组作为参数,你肯定会问:"我如何告诉这个方法我要根据价格进行排序?"接着,我告诉你:"不好意思,这是不可以的,这个方法会自行选择合适的排序依据。"此时,你会有什么感觉?

无可否认,我们已经进入了一个个性化的时代,用户不再像从前那样满足于你所提供的普遍适用的标准化软件,他们希望你的软件是可配置的,必要时还能够扩展,也就是可以满足他们的个性化需求。然而,把逻辑外包出去并不只是为了满足用户的个性化需求,为什么这样说?试想一下,你可不可以写出这样一个Sort方法,每次调用时都能"猜中"用户的排序依据?很明显,当我把books2数组传给Sort方法时,如果我不说,它不可能知道我想按书名排序还是按价格排序,是升序还是降序。换句话说,把逻辑外包出去其实就是把这种不稳定的因素封装起来,再转嫁给用户,然后美其名曰"用户参与",当然,由于用户认为你不是把麻烦抛给他,而是为他带来灵活性,于是造就了"双赢"。

考察System.Array.Sort方法的众多重载版本,不难发现.NET外包逻辑的两种主要方式是:委托和接口。在F#里,我们可以通过Lambda表达式向接受委托作为参数的重载版本注入逻辑(compare函数是F#提供的通用比较函数):

代码 12

当然,使用命名函数注入逻辑也是可以的:

代码 13

代码13除了向我们示范如何在F#里定义函数,还向我们展示了一个有趣的东西,留意comparePrice函数的定义,我并没有为x和y这两个参数指定类型,但F#却从函数体以及上下文推断出它们的类型是Book!

另外,F#并没有刻意区分命名函数和Lambda表达式,代码13的comparePrice函数也可以这样定义:

代码 14

代码13和代码14定义的两个comparePrice函数是等效的,使用上也没有区别,从代码14可以看出,在F#里,函数其实就是值,而我们在代码12里使用的Lambda表达式只不过是代码14定义的comparePrice函数的函数体。

Lambda表达式使你能够以一种紧凑的方式注入逻辑,但如果别人外包逻辑的方式是接口而不是委托呢?这个时候就轮到对象表达式(Object Expression)出场了:

代码 15

在这里,我通过"_"告诉F#我希望它帮我推断IComparer<'a>的类型变量,而F#也不负所托,成功推断出它的类型是Book。F#的对象表达式也算是一种匿名类型,但它和C# 3.0的匿名类型是不同的。在F#的对象表达式里,你可以实现接口的成员或者重写基类的成员,但不能添加任何新的成员;而C# 3.0的匿名类型则只允许属性的存在。

 

如何扩展类型?

假设我要把一组Book对象添加到System.Windows.Forms.ListView上,我应该怎样?对于习惯运用命令式编程方式思考问题的人,他可能会首先想到创建一个ToListViewItem函数:

代码 16

然后"foreach"那组Book对象,对每个Book对象应用ToListViewItem函数,并把函数返回的结果添加到ListView。由于ToListViewItem函数是一个和Book对象相关的操作,你也可能会考虑把它纳入Book类型的定义,使它变成Book类型的实例成员函数:

代码 17

需要说明的是,ToListViewItem成员函数前面的"b"指代当前的对象实例,相当于C#的"this"关键字和VB.NET的"Me"关键字,但因为F#没有强制使用特定的关键字,所以你可以根据具体的需要选择合适的符号,当然,你也可以通过制定命名规范强制使用特定的符号。现在,你可以"foreach"那组Book对象,对每个Book对象调用它的ToListViewItem成员函数,并把函数返回的结果添加到ListView。这个时候,可能会有人提出质疑:"Book类型本来是一个中立的数据载体,现在你在它的成员函数里使用了ListViewItem类型,无疑强化了Book类型和Windows Forms框架之间的关系,有损它本身的纯粹性。"面对这样的质疑,你有什么想法?

事实上,我们可能希望保留Book类型和ToListViewItem函数之间的"所属"关系,但这种关系会一直处于封印状态,直到我们通过某种仪式将它解封才可以使用。这个时候,我们可以考虑通过F# 的类型扩展(Type Extension)把ToListViewItem成员函数分离到一个单独的模块里(假设Book类型位于Lib命名空间里):

代码 18

这样,如果我们没有引用Ext模块,Book类型和ToListViewItem函数之间的关系就会继续保持封印状态:

图 4

而当我们在代码里引用Ext模块时,他们之间的关系就会解封:

图 5

有没有觉得F#的类型扩展和.NET Framework 3.5的扩展方法很像? 事实上,F#的类型扩展有两种形态:当类型扩展的代码和相关的类型处在相同的命名空间里时,它等效于分部类,F#把这种扩展称为固有扩展(Intrinsic Extension);而当两者处于不同的命名空间里时,它相当于扩展方法,F#把这种扩展称为可选扩展(Optional Extension)。但由于类型扩展和扩展方法在IL层面的实现方式是不同的,于是F#的类型扩展无法被C# 3.0/VB.NET 9.0识别,而C# 3.0/VB.NET 9.0的扩展方法也无法被F#识别。你可能会感到很奇怪:既然.NET 3.5已经提供了现成的实现方案,为什么F#还要另外弄一个出来呢?其实,现在的F#(1.9.4)是基于.NET Framework 2.0而不是3.5的,所以同时存在两种不同的实现方案并非有意的,不过Don Syme说将来F#会改用.NET Framework 3.5的实现方案,即代码18定义的类型扩展也能被C# 3.0/VB.NET 9.0识别。

那么,我现在是否可以在F#里定义能被C# 3.0/VB.NET 9.0识别的扩展方法?当然可以,虽然F#现在无法识别扩展方法,但这并不妨碍你在F#里定义这种方法:

代码 19

在F#里定义扩展方法的语法和在VB.NET 9.0里的语法相似,都是显式使用ExtensionAttribute的。接着,把代码编译成DLL,在C# 3.0或者VB.NET 9.0项目里引用一下就可以使用了,编译的时候记得在F#的项目属性里设置.NET Framework 3.5的相关DLL的引用。

噢,说着说着,差点忘记原本的目的了,我们做了这么多,最终还是难逃"foreach"那组Book对象,对每个对象应用变换操作,并把变换结果添加到ListView,那么,F#有没有提供更简单的方法可以用来完成这项任务呢?其实,F#更倾向于通过函数的组合来完成相关的工作:

代码 20

上面这行代码向我们揭示了数据的流动:books2是数据源,它的数据流经Array.map函数和ToListViewItem函数组合而成的变换函数,然后流向listView.Items.AddRange方法。我们也可以这样理解这行代码:用ToListViewItem函数对books2数组的每个元素做变换操作,然后把结果传给listView.Items.AddRange方法。从这行代码可以看出,运用函数式编程方式来思考这个问题,我们最初定义的ToListViewItem函数就已经足够了。

 

如何查询数据?

C# 3.0最引人注目的地方莫过于使用LINQ查询数据了,前面提到,目前的F#是基于.NET Framework 2.0的,那么它又如何查询数据呢?我们知道,IEnumerable<'a>是LINQ的核心接口,也是执行任何查询操作的必要条件。在F#里,你可以使用seq<'a>或者IEnumerable<'a>,seq<'a>是F#为IEnumerable<'a>提供的类型缩写(Type Abbreviations),相当于C++的typedef,而适用于seq<'a>的函数则位于Microsoft.FSharp.Collections.Seq模块里,例如Seq.filter函数、Seq.map函数、Seq.orderBy函数等。

假设我现在想用F#对代码10的books做一个查询,找出Tags属性里包含"F#"字眼的书,然后根据Price属性进行排序,那么我该如何做呢?

要判断某本书是否符合我的条件,可以把它的Tags属性按";"符号分割成一个字符串数组,然后判断这个数组里面是否包含"F#"字眼。要把一个字符串按指定的符号分割成一个字符串数组,可以使用System.String.Split实例方法,但要判断一个集合是否包含指定的元素,除了通过Lambda表达式,似乎没有更加直接的方法,所以我仿照System.Linq.Enumerable.Contains方法定义了一个contains函数:

代码 21

contains函数使用了Seq.exists函数,并且通过Lambda表达式告知判断条件为是否相等。你可能会感到很奇怪:为什么没有给contains函数的参数指定类型却可以通过编译?如果这个代码是合法的,那么value和source参数的类型是什么?当F#的编译器看到这段代码时,它发现value和source参数将会用作Seq.exists函数的参数,于是便试图从Seq.exists函数的签名推断value和source参数的类型,由于Seq.exists函数是一个泛型函数,而我们又没有在定义contains函数时给出进一步的约束,于是便断定contains函数也是一个泛型函数,并自动为value和source参数添加类型参数, F#把这个过程叫做自动泛型化(automatic generalization)。另外,你也可能感到奇怪:contains函数的参数顺序和Enumerable.Contains方法恰好相反,为什么呢?我们知道, Enumerable.Contains方法把表示集合的参数放在第一个位置是为了满足扩展方法的要求;而我在这里把表示集合的参数放在contains函数的参数列表的最后一个位置则是为了满足"|>"运算符的要求。我们在代码20已经见识过"|>"运算符了,它的作用是可以把目标函数的最后一个参数提到运算符的前面,换句话说,如果我要判断coll集合里是否包含elem元素,contains函数就可以这样用:coll |> contains elem。如果你有兴趣进一步了解"|>"运算符,可以阅读Brian《Pipelining in F#》

有了上面的准备,我们就可以开始查询数据了:

代码 22

Seq.filter函数、Seq.map函数和Seq.orderBy函数分别相当于Enumerable.Where方法、Enumerable.Select方法和Enumerable.OrderBy方法。如果我们把Seq.filter函数、Seq.map函数和Seq.orderBy函数分别定义为where函数、select函数和orderby函数:

代码 23

那么代码22就可以写成这样了:

代码 24

我们来看看对应的C# 3.0代码:

代码 25

是否感到有点喜出望外?什么?感到很失望?因为我写了一个多余的select?囧。。。

读到这里,你可能会问:"如果我只想获取Title和Price属性呢?"这个时候就轮到F#的Tuple类型出场了:

代码 26

读到这里,你可能会问:"如果我想从q里提取所有的书名,生成一个新的集合呢?"你可以通过select函数做到,但现在我想换个玩法,试一下F#的序列表达式(Sequence Expression):

代码 27

我们知道,q里的每个元素都是包含两个数据的Tuple类型,也就是说,每个元素都能匹配"(a, b)"模式,其中,书名会匹配到a,而价格则匹配到b,但由于我只关心书名,于是在b的位置上使用"_",表明我不关心b位置的数据。同样地,如果我想从q里提出所有的价格,然后对它们求和,我可以这样:

代码 28

如果你读过我的《我眼中的C# 3.0》,你应该会记得C# 3.0还提供了字典初始化语法,那么,F#是否也支持类似的语法?很遗憾,不支持,至少目前还没看到这种语法,然而,要在F#里把List<'a>变成Map<'key, 'a>却是非常容易的:

代码 29

上面这句话可以这样理解:把books按照"fun b -> (b.Title, b)"方法进行映射,然后把所得结果用作Map.of_list函数的输入。

 

业余研究语言的人

我们经常使用编程语言,但我们是否曾经停下来想一下:编程语言究竟是什么?或许对你来说,编程语言只不过是用来编写程序的一种工具,所以根本用不着耗费心思去想这个问题,但对我来说,它并非只是一种工具。我有一个好朋友很喜欢研究股票市场,她说通过股票市场可以看到人性的种种;而我则喜欢研究编程语言,因为通过编程语言可以了解设计者和使用者的思维方式,就像其他研究语言的人通过自然语言可以了解使用者的种群文化一样。

当一种新的编程语言出现并且得到很多受众的支持时,可能就会有人出来说某种旧的编程语言要被取代,甚至宣称这种旧的编程语言的消亡,与此同时,也会有人站出来,对新的编程语言的必要性提出种种质疑……。没有人希望自己选择的编程语言失去活力,当你选择一种编程语言时,其实就接受这种编程语言背后的世界观,所以你会誓死捍卫你的选择,无怪乎每次编程语言之间的优劣争论都可以上升到宗教信仰的程度。面对这类争论,我觉得optionsScalper的态度比较实际(原贴):

I tend not to think of "vs." when doing this type of work. F#, C#, C++ and others are nothing more than tools. Used well, each is capable of yielding great results.

今天,我因为C# 3.0踏进了F#的大门;明天,我会因为C# 4.0踏进谁的大门呢?无论答案是什么,可能都是很久以后的事了……

posted @ 2008-07-25 19:18 Allen Lee 阅读(3105) | 评论 (100)编辑

我眼中的C# 3.0

 

Written by Allen Lee

 

缘起

每次有新技术发布时,我们总能感受到两种截然不同的情绪:一种是恐惧和抵抗,伴随着这种情绪的还有诸如"C# 2.0用的挺好的,为什么要在C# 3.0搞到那么复杂?"或者"我还在使用C# 1.0呢?"等言辞;另一种则是兴奋和拥抱,伴随着这种情绪的还有诸如"原来这个问题在C# 3.0里可以这么简单!"等言辞。

最近我在公司内部做一个LINQ的系列讲座,在我为其中C# 3.0新特性这一讲准备演示文稿时,突然萌生了写下这篇文章的念头。语言的特性乃至其本身并没有对错之分,是否接受在很大程度上是一个感性问题,即你是否喜欢这样的做事方式,我并没有打算说服任何人接受C# 3.0和LINQ,写这篇文章也只是想和大家分享一下我自己的感受。

有一次我观看一个关于Expression Blend的培训视频,里面说了一句让我印象非常深刻的话:

I know how it works because I know why it works.

细细品味这句话,你会感受到它所要传达的信息:理解为何需要这个功能可以帮助你更好地理解如何使用这个功能,而这也正是我要在这篇文章里采用的表达方式。

 

你是如何创建属性的?

如果你长期使用C#,相信你不会对属性这个东西感到陌生。一般地,属性是对私有字段的一个简单包装,就像这样:

代码 1

使用属性而不是直接公开私有字段的一个好处就是在属性的获取访问器或设置访问器里加入额外的逻辑并不会为客户端代码带来麻烦,例如你想在设置标题的时候做一些额外的检查。但如果你只是简单地包装一下,像上面的代码那样,就会发现你其实多写了不少可以省略的代码。既然Title属性和m_Title私有字段对应,获取访问器就肯定是返回m_Title的值,而设置访问器也肯定是把值设到m_Title。再者,如果你只通过Title属性来访问这个数据,那么m_Title私有字段就会变得无足轻重,这样的话,为什么不交给编译器代劳呢?这个时候,C# 3.0的自动属性就可以派上用场了:

代码 2

编译器会为你创建一个私有字段,并让获取访问器和设置访问器指向这个私有字段。当然,如果有需要,例如要在获取访问器或设置访问器里加入额外的逻辑时,你随时可以对获取访问器和设置访问器进行展开。

 

你是如何初始化对象的?

现在,假设我们有这样一个类:

代码 3

你会怎样初始化它?一种做法是用Book的默认构造函数创建对象实例,然后分别为每个属性赋值:

代码 4

另一种做法是使用C# 3.0对象初始化器:

代码 5

乍看一下,C# 3.0的做法似乎没有让人感到任何优越感,现在,请你仔细观察一下,这两份代码分别包含多少个";"?代码4有5个";",意味着它用了5个语句进行初始化;而代码5只有1个";",意味着它只用了1个语句进行初始化。从词法的角度来看,如果此刻我只能接受一个表达式,那么代码4的做法就帮不上忙了。一个变通的方法是为Book类提供带参的构造函数,但这种方法也有弊端,用户可能只想在初始化时为部分属性提供数据,而我们又无法确切预知用户会提供哪些属性的组合,于是,我们可能要为用户提供足够多的构造函数重载,嗯,有点无聊,也有点多余。另一个变通的方法是提供接受最多参数的构造函数,如果用户为某个参数传递null,那么就忽略与之对应的属性,这个方法比较接近代码5的做法,不同的是,如果你的属性很多,而用户关心的只是很少一部分,就可能不得不输入很多null了。

现在,假设你要实例化一组Book对象,并把它们储存在一个集合里,你会怎么做?下面是通常的做法:

代码 6

如果结合使用C# 3.0的对象初始化器和集合初始化器,你就可以把代码简化为:

代码 7

集合里的每个元素通过","分割,结合对象初始化器使用,整个集合的结构显得比较明晰。字典的初始化也可以同样简单:

代码 8

说到这里,我相信你也能感觉到,C#似乎正在表达式化,以前需要很多条语句才能做到的事情,现在却可以用单个表达式描述出来,而这种理念也渗透在整个C# 3.0的氛围里。

 

你是如何把运算逻辑外包出去的?

假设我现在得到了一组Book的实例对象,你要对它们进行排序,那么你如何告诉它你要按价格来排序呢?

代码 9

在C# 1.0里,我们需要特意为它提供一个独立的方法:

代码 10

然后向Sort()方法传入所需委托的实例:

代码 11

这在C# 2.0里可以进一步简化为:

代码 12

如果使用C# 2.0的匿名方法,我们可以省去很多不必要的代码:

代码 13

此外,使用匿名方法,Sort()方法和你希望它用来比较两个Book实例对象的逻辑可以放在同一个地方;而使用独立的命名方法,包含这个逻辑的方法可能会由于整理代码而被挪到别的地方。这样,当你看到代码12时,为了了解它内部的实现,就不得不花一些精力去寻找Compare()方法了。当然,你可以争辩说,我们可以制定一个编码规范,使得Compare()方法必须紧贴在Sort()方法的下方。是的,你可以,但如果这个逻辑并不需要重用,那么使用匿名方法还是具有明显的优势的。如果这个逻辑需要重用,那么匿名方法就无能为力了。

现在,让我们来考察一下代码13,有没有发现匿名方法的表达方式还不够简练?我们知道,books集合里面只有Book的实例对象,所以Sort()方法传给我们两个参数的类型必定是Book,而Sort()方法期待的结果正是x.Price.CompareTo(y.Price)这个表达式的运算结果,至于delegate和return这样的字眼可以说在这里完全是多余的,那么为什么我们不直接这样表达呢:

代码 14

这就是C# 3.0引入的Lambda表达式语法。我见过一些人,他们通常强调尽可能简单,但若事情突然变得比他们预期的还要简单很多,他们就开始感到不适,甚至拒绝接受这种简单,其实即使事物的发展方向和你的前进方向相一致,但如果发展速度大大超越了你,仍然有可能引发你内心对失控的恐惧。我希望Lambda表达式语法不会让你感到太大的不适,当然我更希望你会喜欢上它。

Lambda表达式的理解其实可以很简单,就是"=>"左边的参数参与右边的表达式运算,而运算结果将会返回,这有点像化合反应,即两种或两种以上的物质(左边的参数)生成一种新物质(右边的表达式的运算结果),不同的是,Lambda可以不接收任何参数,也可以不返回任何结果。

"=>"右边除了可以放表达式之外,还可以放语句,像这样:

代码 15

我们把它称为Lambda语句(Lambda Statement),或许你已经发现,它和匿名方法相比只是不需要写delegate关键字和参数类型。

 

你是如何为对象扩展与之相关的功能的?

我一直在想,为什么String类没有提供一个Reverse()方法,把字符串翻转呢?我猜可能是因为这种操作没有什么现实意思,除非你要做一个文字游戏。实现Reverse()方法并不难,下面是其中一种做法:

代码 16

使用方法也非常简单:

代码 17

你甚至可以把Reverse()方法放到某个静态类里,例如Utils,这样,代码17就可以变成:

代码 18

在C# 3.0之前,你最多只能走到这里,而到了C# 3.0,你还可以使用扩展方法对它做进一步调整,使代码18变成:

代码 19

怎么样,看上去就像Reverse()方法是属于String的,而你所需要做的仅仅是在Reverse()方法的target参数前面加上"this"关键字:

代码 20

我们知道,计算机的底层世界并不知道什么是面向对象,而我们在对象里定义的实例方法都包含一个隐藏参数,这个参数就是指向当前对象实例的指针,C# 3.0的扩展方法在形式上模仿了这种做法,但由于扩展方法本质上并不属于与之相关的类,所以你无法在扩展方法里访问类内部的私有成员。

就上面的讨论来说,你可能认为,和代码18相比,代码19并没有太大的优势,那么为什么需要扩展方法呢?假设我们手头上有一堆书,我想找到最便宜的LINQ的书,使用标准查询运算符的话可以这样写:

代码 21

我们知道,Where()、OrderBy()和First()等都是扩展方法,如果C# 3.0不支持扩展方法,那么代码21就不得不写成这样了:

代码 22

代码21的可读性明显比代码22的高,也显得更自然,而此时我们只是使用了3个标准查询运算符,你可以想象一下,在没有扩展方法的支持下要表达更复杂的查询会是怎样一番情景?

 

你是如何表达你想要的东西的?

现在,假设我想找到最便宜的LINQ的书,使用C# 2.0的语法,我可能需要这样:

代码 23

虽然我已经使用了Array.IndexOf()方法、List<T>.Sort()方法和匿名函数来简化代码,但仍然无法掩盖一个事实,那就是我在讲述如何获取我想要的东西,而这也正是命令式编程(Imperative Programming)的核心思想。

如果使用C# 3.0的语法,情况将会大不一样:

代码 24

在这里,你表达了你想要的东西,而不是获取这些东西的具体步骤,这是声明式编程(Declarative Programming)的核心思想,这样做的好处是明显的,你的需求可以被重新解析并执行,必要时还可以对底层的实现进行优化,但由于你并不关心和牵扯到具体的实现上,所以那些优化并不会导致你修改代码。

命令式编程就像过程管理,你深入执行的细节,继而对整个过程的执行实施控制;而声明式编程则像目标管理(MBO),你制定目标,并把任务分配下去执行。代码23给人的感觉就是整个执行过程都非常的清楚,你可以对任何一个步骤进行修改或者调优;而代码24给人的感觉就是你除了说出你想要什么,你什么也不能做,这对于那些过程管理拥戴者来说可能是不可接受的,他们感到对事物失去了控制,无法建立安全感,因而产生了焦虑。曾经有人向我抱怨:如果你使用了LINQ,你就只能迫使自己相信它的实现是很好的。想想看,如果你的公司把饭堂业务承包给一个餐饮公司,你的公司可以插手别人如何招聘厨师、如何采购食物、如何烧菜烧饭吗?选择LINQ意味着你愿意把执行细节交给别人去处理,从而脱离这些细节,如果你根本无法放下对这些细节的控制,那么LINQ可能并不适合你。

很难说这两种编程方式孰优孰劣,因为在某些场合下,善于过程管理的管理者确实更能让事态朝正确的方向发展;而在另一些场合下,目标管理为实现者提供足够的自由度,更能激励他们积极地进行思考。管理界对于过程管理和目标管理孰优孰劣之争论似乎从来没有停过,更何况编程界对于命令式编程和声明式编程孰优孰劣之争论,我个人倒是更倾向于把这看成是找出更适合你自己的风格,而不是盲目听信别人的说法。语言到底是发挥积极作用还是消极作用在很大程度上是取决于使用者的,我们应该使用语言有利的一面来协助我们的工作,而不是使用其有害的一面来伤害自己和别人。

回到代码24,它把满足条件的书的所有信息都返回给我,如果我只需要书名和作者名字呢?我们知道,在面向对象的世界里,信息储存在对象里,于是我们不得不走到一个尴尬的境地,那就是我们要为此创建一个临时类:

代码 25

噩梦正式开始了,如果我需要书名和价格呢?如果我需要书名、作者和价格呢?……(读者可以自行补全这个列表)这个时候就轮到C# 3.0的匿名类型和隐式类型化变量出场了:

代码 26

因为匿名类型是由编译器自动生成的,而在你写代码的时候它还没有名字,所以你无法用这个类型来声明这个变量,此时"var"关键字就派上用场了。这个是"var"关键字的最初目的,但得益于类型推断系统,我们还可以使用"var"关键字声明任何本地变量,只要我们在声明的同时给予它初始化,否则编译器无法进行推断。曾经有人问我:如果我想返回代码26里的wanted7怎么办?我们知道,方法的返回值需要明确给出类型,而在我们写下代码26时,编译器还没有给查询表达式里的匿名类型取名。如果你真的要把它返回,你只能把方法的返回值类型定为IEnumerable<object>,因为我们只能确定匿名类型是object的后代,但这样一来,客户端代码的日子就不太好过了,因为除了通过反射来访问你的对象,它别无他选。如果你真的要把它返回,那就意味着你和客户端代码有共享这个对象的需求,此时恰当的做法应该是使用命名类型。另外,代码26里构建匿名类型时的"book.Title"是"Title = book.Title"的简写,当你省略"Title ="时,编译器会假定你希望匿名类型的这个属性的名字和Book.Title的一样。

匿名类型还有一个有趣的地方,它曾经是可变的(mutable),后来却变成不可变的(immutable),Sree《Immutable is, the new Anonymous Type》一文中给出了这个转变的解释。我们知道,在面向对象的世界里,对象封装并维护自身的状态,我们通过调用对象的方法所产生的副作用来影响对象的状态,而不可变则是函数式编程(Functional Programming)的核心特征,或许你已经感受到了,C# 3.0引入了大量函数式编程的东西,而函数式编程语言似乎也要风生水起,这究竟意味着什么呢?

 

前路在何方?

无论你是否承认,C# 3.0在表达上比它之前的版本要来的简单,但要获得这种简单,你必须先用很多东西武装自己的脑袋,这使我想起曾经在一本书里看到的一句话:

简单是由复杂来支撑的。

不同语言之间的相互渗透已经不再是什么新奇之事了,引入其它语言的功能有时候甚至可以看作是在战略上入侵对手的市场,这在某种程度上有点像金融业的混业经营。下一个版本的C#将会是怎样的呢?或许这个问题令你兴奋不已,你甚至希望现在就让C# Team看看你的创造力;或许这个问题令你痛心不已,你害怕自己无法适应下一波的变革,因为变革可能导致动荡,动荡可能带来失控,失控可能引发焦虑。不管怎样,该来的是无法回避的,或许现在先让我们看看Matthew Podwysocki的《What Is the Future of C# Anyways?》是否有一些启示……

 

附:如果你有兴趣看看我的演示文稿,可以点击这里下载。

posted @ 2008-06-01 10:49 Allen Lee 阅读(6110) | 评论 (101)编辑