代码改变世界

也谈语言特性——回应老赵的《语言特性与API设计》

2009-11-03 15:09  JimLiu  阅读(2779)  评论(15编辑  收藏  举报

个多月前读到了老赵的《语言特性与API设计》一文,深有感触,思绪涌上心头,无奈手头无闲,未能记录。今日撰文一篇,与大家分享下,但求抛砖引玉。

与老赵不同的是,老赵的文章更多的讨论的是一些相对比较前沿的话题,比如functional、比如Fluent Interface。比较的对象也是C#/Java这样的大路货与F#这样的明星新秀。而与此并立,我也从更基础的话题去谈,比较的对象也是C++这样的老牌劲旅。

class GenericClass<T>

泛型,天天都在用,但是却值得思考。

C++中的模板,改变了很多程序员的思维。“类型参数话”这个概念劈头盖脸地来了,而略显晦涩的语法又让这个东西显得既“高端”又“奇怪”。我没有生在那个年代,我也不知道当时的编程语言界有多大的风浪,但是就我自己而言,从一个懵懂的C语言学生到这里,我很惊讶。模板的出现让程序的抽象性大大的提高,我们再也不用去用那个让人摸不着头脑的丑陋的void*来达到代码的“复用”了。STL的出现更是让C++如日中天,这一切看起来如此美好。

而我接触了C#后,我也忍不住比较二者的泛型机制,巨大的差异让我不得不好奇STL到底是怎么实现的,看了源代码我才有了这么多想法。

先来看一段STL代码:

 

reverse

这的确是一段让人摸不着头脑的代码,如果在C#中,我相信多数人会采用下面的定义:

 

reverse

这其实是很顺理成章的,对于泛型方法,我们更愿意去传递元素类型,而不愿意也不会想到去传递迭代器的类型,但是STL是这么写的,为什么要用这种绕弯子的办法呢?

我认为原因有两个:

第一,STL要顾忌到C程序员的习惯,他们也许不会为了使用一个Library而让他们的类型继承于类库中的类型,也就是说如果用户自己编写了一个容器CustomCollection<T>,也许不会愿意再写一个迭代器去继承RandomAccessIterator<T>的。为什么呢?因为C++中是没有interface的,继承一个class会从逻辑上给人一种巨大的强迫感——“类的继承应该是严格的is-a关系”,这种思维指导着几乎所有现代OO程序员。而在C#中这种顾虑是不存在的,因为设计上RandomAccessIterator<T>只是一个interface而已,让一个类去实现一个接口,从而获得library/framework的优惠照顾,在我们看来是一件非常轻松惬意的事情。

第二,C++编译器的模板机制是相当相当弱的——当然这是相对于C#编译器的泛型机制。因为C#的泛型机制在编译阶段会进行类型检查,而C++的则是进行代码检查,二者有天壤之别。STL的迭代器设计原则当中非常重要的一条就是“指针必须能充当RandomAccessIterator”,如果我们用类型继承的观念来设计代码,必然会受阻——因为指针类型T*并没有继承自RandomAccessIterator<T>,这是完完全全由语言本身所决定的,不可逆转的。而反过来用STL中的思路来设计C#代码,也会罢工——因为没有类型约束,编译器是不会让不合适的类型通过编译的。其实这完全取决于编译器的脾气。C++的模板编译器检查的是“template被替换之后,有没有对应的一段代码存在”,而C#的泛型编译器则检查“有没有符合要求的类型和接口继承关系”。就是因为编译器的脾气和视角,造成了类库设计的时候思维的差别

再回头看C#,C#中的泛型让人相当的满意。定义的时候多种约束条件增加了语言的严谨性;编译器基于泛型的类型推导给我们带来了语句上的省略、IDE的帮助和var编译时推断类型这样的方便之门。而在不久的将来,C# 4.0对泛型的进一步加强也是很值得期待的。

Predicate

有的朋友可能有意见了:“谓词不是functional programming中的重要概念吗?文章开始不是说谈基础的东西吗?”

没错,谓词的确是函数式编程中的重要概念,但其实追根究底的话,谓词应该是离散数学的重要概念,后者不正是计算机科学的理论基础的一个重要部分吗?

predicate
var persons = new List<Person>();
// 
var result = persons.Where(p => p.Age >= 20)
                    .OrderBy(p 
=> p.Age)
                    .Select(p 
=> new
 { p.FirstName, p.LastName });

C# 3.0给我们带来了lambda expression和LINQ,这是让人欢欣鼓舞的语言特性,这让C#这门传统OO语言有了更多函数式语言的味道。

好吧,回归正题,这和谓词有什么关系呢?——在Visual Studio里对一个集合.Where(...)看看那个Where的参数叫什么名字吧?

Func<T, bool> predicate——没错,谓词。

我曾经觉得这是语言巨大进步的一个体现,但是我在了解到更多关于STL的东西之后,我立即为我的无知感到羞耻了。

sort
struct Person{
    
string FirstName, LastName;
    
int Age;
};

vector
<Person> list;

sort(list,begin(), list.end(), personCmp);



bool personCmp(const Person& a, const Person& b){
    
return a.Age < b.Age;
}

这是一段平淡无奇的C++代码,但是如果真的结合C++的模板机制去想一想,却会发现其中的奥妙。

 

sort-2
struct Person{
    
string FirstName, LastName;
    
int Age;
};

class PersonComparer{
public:
    
bool operator ()(const Person& a, const Person& b){
        
return a.Age < b.Age;
    }
};

int main(){
    vector
<Person> list;
    PersonComparer personCmp;
    sort(list.begin(), list.end(), personCmp);
}

上文说过C++的编译器检查代码而不是类型,所以上文的代码被编译后会遇到形如personCmp(a, b)的调用。如果personCmp只是一个平淡无奇的函数,那么我们当然没必要大惊小怪,而这里personCmp却是一个实实在在的类的实例,因为C++提供重载()运算符的功能,所以这里调用personCmp(a, b),对C++编译器来说是合法的

因为类的实例是有自己的上下文和this环境的,这样我们就可以构造一个有上下文环境的比较器来完成这个比较,而不是一个孱弱的比较函数。

从来没有官方言论,也没有人为它正名过,但是我认为,这就是C++中对谓词概念的体现,C++非常高的灵活性给一些geeks带来了很棒的语言玩具。虽然相比之下使用匿名函数甚至lambda表达式构造谓词不论从便捷性还是可读性都高了一个等级,但是比起java中的类似API,我却觉得一点也不差了——当然,利用匿名内部类,JAVA可以实现一个看起来更好的调用方式(注1)。

小结

在我看来,我们常接触到的,无外乎三种语言——编程语言、数学语言、自然语言,编程语言对程序的逻辑和流程有绝对的控制力,数学语言有三者当中最佳的严谨和最低的歧义,而自然语言则有最佳的可理解性。函数式编程让编程中有更多数学语言的影子,Fluent API让程序有更好的可理解性,也就是向自然语言靠拢。编程语言的发展就是在保持自己对逻辑和流程的控制力的基础上吸收另外二者的优点。不仅C#如此,动态语言和函数式语言的军团中的年轻小伙子们在这方面还更惹人注目。

另一方面我也有更多思考,为什么JAVA语言本身的特性这么尴尬,却还是能成为用户最庞大的语言呢?这一方面当然是有历史原因,但是从另一个角度看,JAVA的语言特性传统而且稳定,这样学习JAVA并不用掌握很多相对较新的东西,诸如闭包、谓词这些概念,比起继承、接口、多态、匿名内部类这样的概念,的确是要难一些。这样让JAVA程序员的水平更容易控制在一个接近的水平线上,我想相比于geeks追求语言中的奇技淫巧,也许这样更适合实际中工业生产吧。

注解

1、在C# 2.0时代我曾经很羡慕JAVA的匿名内部类这个特性,因为当时的C#中虽然已经有了匿名委托,但是类库中很多地方还是需要使用类的实例作为参数——比如

Array.Sort<TKey, TValue>(TKey[], TValue[], IComparer<TKey>)

这个函数居然没有提供Comparision<TKey>(这个东东是个委托,所以可以用匿名函数来构造)的重载(要知道List<T>.Sort和Array.Sort<T>都是提供了的)。这种情况下如果C#也拥有匿名内部类有的代码会变得漂亮不少——虽然遇到的情况似乎并不多。

但是到C# 3.0的时候我的看法有所改变了,对于简单情况,如果能用匿名委托来构造那么很显然可以用lambda表达式构造匿名函数,既方便又优雅。而对于

Array.Sort<TKey, TValue>(TKey[], TValue[], IComparer<TKey>)

这种尴尬的情况,再不济写个扩展方法也是能接受的方案吧?就好比

TKey[].SortArrayAsKeys<TValue>(TValue[], Comparision<TKey>)

再甚就直接把Comparision<TKey>换成了Func<TKey, TKey, int>,再利用上编译器的类型推断给我们提供的类型参数省略,不是很轻松惬意吗?