随笔-341  评论-2670  文章-0  trackbacks-0

面向对象这个抽象的特例总是有说不完的话题,更糟糕的是很多语言都错误地实现了面向对象——class居然可以当一个变量类型什么的这只是让人们写代码写的更糟糕而已。当然这个话题第三篇文章已经说过了,现在来谈谈人们喜欢拿来装逼的另一个话题——消息发送。

按照惯例先来点题外话。说到消息发送,有些人喜欢跳出来说,objective-c的消息做得多优雅啊,代码都可以写成一句话[golang screw:you you:suck]之类的。其实这个还做得不够彻底。在几年前易语言曾经火了一阵,但是为什么大家这么讨厌他呢?其实显然不是因为每个token都是汉字,而是因为他做的一点都不像中文,谁会说话的时候带那么多符号呀。其实objective-c也一样,没人会因为想在一句英语里面用冒号来分割短语的。

当我还在读大三的时候,我由于受到了Apple Script(也是苹果做的)的启发,试图发明一门语言,让他可以尽量写起来像自然语言——当然他仍然是严格的编程语言。但是这门语言因为其奇特的语法结构,我只好自己想出了一个两遍parse地方法。第一遍parse出所有函数头,让后用这些函数头临时组成一个parser,用它来parse语句的部分。后面整个也实现出来了,然后我就去做了一下调查,发现大家不喜欢,原因是要输入的东西太多了。不过我这里还是贴一下当初是怎么设计的:

phrase print(content) is
    external function "writeln"
end phrase

phrase first (count) items of fibonacci sequence is
    if count equals to 1 then
        result is [1]
    else if count equals to 2 then
        result is [1,1]
    else
        let list be [1,1]
        repeat with i from 3 to count
            let list be list joins with item length of list - 1 of list + item length of list - 2 of list
        end
        result is list
    end
end phrase

phrase (number) is odd is
    result is number mod 2 is 0
end phrase alias odd number

phrase append (item) after (list) is
    let 0 element of list from length of list be [item]
end phrase

phrase ((item) is validated) in (list) is
    let filtered list be []
    repeat with item in list
        append item after filtered list if item is validated
    end
    result is filtered list
end phrase

phrase main is
    print odd number in first 10 items of fibonacci sequence
end phrase

倒数第二个函数声明甚至连函数指针的声明也如此的优雅(我自己认为的),整个程序组织起来,我们要输出斐波那契数列里面前10个数字中间的奇数,于是就写成了

print odd number in first 10 items of fibonacci sequence

看起来比objective-c要漂亮把。其实如果想把所有的东西换成中文,算法也不需要变化。现在用空格来分割一个一个的词,中文直接用字符就好了,剩下的都一样。要parse这个程序根本没办法用流行的那些方法来parse。当然我知道大家也不会关心这些特别复杂的问题,于是题外话就到这里结束了,这个语言的实现的代码你们大概也永远都不会看到的,啊哈哈哈哈。

为什么要提这件事情呢?我主要是想告诉大家,就算你在用面向对象语言,想在程序里面给一个对象发送一条消息,这个对象并不是非得写在最前面的。为什么呢?有的时候对象不止一个——这个东西叫multiple dispatching,著名的问题就是如何给一堆面向对象的几何体类做他们的求交函数——用面向对象的惯用做法做起来会特别的难受。不过现在我们先来看一下普通的消息发送是什么样子的。

对于一个我们知道他是什么类型的对象来说,发送一个消息就跟直接调用一个函数一样,因为你不需要去resolve一下这个函数到底是谁。譬如说下面的代码:

class Language
{
public:
    void YouSuck(){ ... }
};

Language golang;
golang.YouSuck();

最终翻译出来的结果会近似

struct Language
{
};

void Language_YouSuck(Language* const this)
{
    ...
}

Language golang;
Language_YouSuck(&golang);

很多人其实并不能在学习面向对象语言的时候就直接意识到这一点。其实我也是在高中的时候玩delphi突然就在网上看见了这么一篇文章,然后我才明白的。看起来这个过渡并不是特别的自然是不是。

当你要写一个独立的class,不继承自任何东西的时候,这个class的作用只有两个。第一个是封装,这个第三篇文章已经提到过了。第二个作用就是给里面的那些函数做一个匿名的namespace。这是什么意思呢?就像上面的代码一样,你写golang.YouSuck(),编译器会知道golang是一个Language,然后去调用Language::YouSuck()。如果你调用lisp.YouSuck()的时候,说不定lisp是另一个叫做BetterThanGolangLanguage的类型,然后他就去里面找了YouSuck。这里并不会因为两个YouSuck的名字一样,编译器就把它搞混了。这个东西这跟重载也差不多,我就曾经在Microsoft Research里面看见过一个人做了一个语言(主要是用来验证语言本身的正确性的),其中a.b(c, d)是b(a, c, d)的语法糖,这个“.”毫无特别之处。

有一天,情况变了。专门开发蹩脚编译器的AMD公司看见golang很符合他们的口味,于是也写了一个golang的实现。那这个事情应该怎么建模呢?因为golang本身是一套标准,你可也可以称呼他为协议,然后下面有若干个实现。所以Language本身作为一个category也只好跟着golang变成interface了。为了程序简单我们只看其中的一个小片段:

class IGolang
{
public:
    virtual void YouSuck()=0;
};

class GoogleGolang : public IGolang
{
public:
    void YouSuck()override{ /*1*/ }
};

class AmdGolang : public IGolang
{
public:
    void YouSuck()override{ /*2*/ }
};

IGolang* golang = new GoogleGolang;
golang->YouSuck();

我很喜