browser icon
You are using an insecure version of your web browser. Please update your browser!
Using an outdated browser makes your computer unsafe. For a safer, faster, more enjoyable user experience, please update your browser today or try a newer browser.

OOP 范型与语言边界

Posted by on 2014 年 01 月 05 日

你可以任意转载本文,但请在转载后的文章中注明作者和原始链接。
媒体约稿请联系 titilima_AT_163.com(把“_AT_”换成“@”)。

我大学读的是计算机系,对于干程序员这一行来讲,应该可以算是“科班出身”了。其实在刚入学的时候,除了少数有过些计算机经验的学生(这少数派的范围应该包括我,因为我是前有过半年可以用来充充门面的 Visual Basic 经历)之外,其他人的经历大抵也不过就是星际、CS 和 QQ 之类。懵懂必然带来浮躁,因此一些莫名其妙的技术言论便经常会突然间流传在学生之中。在这些有鼻子有眼儿的论调之中,我印象最为深刻的是这么一个段子:

C 是面向过程的语言,它是基于函数的;C++ 是面向对象的语言,它是基于消息的。

很显然,这个段子是经不起推敲的。它最大的问题,就是把编程范型和语言特性混为了一谈。因为从概念上来讲,编程范型是指从事软件工程的一类典型的风格(据 维基百科:编程范型),例如面向过程编程、面向对象编程(下文简称为“OOP”)都隶属于其中;而在这个概念之中,编程语言却是没有被严格限定的。对比下诺基亚除了打电话之外还可以用来砸核桃的特性,你就会发现给 C 扣上个“面向过程”的帽子其实是很不公平的。
另一方面,考察这个段子中所存在的合理性,我们也不能否认 C++ 较之 C 而言的确更适合用于 OOP,这是因为 C++ 语言本身提供了 OOP 所需的一些特性,例如类与对象、继承、多态等等。
言而总之,所谓“OOP”是指一种语言无关的程序设计方法,在使用某种语言来践行这个方法的时候,OOP 的有些要点和步骤是直接被语言特性所支持的,而其它的(语言本身无能为力的)部分则需要交给程序的框架(framework)来实现。接下来,我将会对几种我所熟悉的语言和框架进行 OOP 范型的边界考察,并简单说明它们是如何支持与实现 OOP 之中的各种要点的。

类与对象

类(class)描述了一件事物的属性和它的行为,如果把这一定义具体到编程语言的概念中,那么我们可以把“类”简单地看作数据及其操作被语法绑定为一体后所形成的数据结构,而对象(object)则是类的实例。目前的编程语言之中,但凡是提供了面向对象特性的语言都包含了对类的支持,所以不必细说。只有 C 在实现“类”这一概念的时候需要借由具体的框架来支持,例如将函数指针作为结构体(struct)的成员的方法,也就是像下面这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
 
struct foo {
    int member_;
    void (*show_member_)(struct foo *obj);
};
 
void show_member(struct foo *obj)
{
    printf("member = %d\n", obj->member_);
}
 
int main(void)
{
    struct foo obj;
    obj.member_ = 123;
    obj.show_member_ = show_member;
    obj.show_member_(&obj);
    return 0;
}

如你所见,当 OO 特性超越了语言边界之外的时候,我们便不得不依靠程序的框架来解决这个问题,而这往往会付出更大篇幅的代码,这也就是支持“C 不适合 OOP”的最主要论据了。但是,这个槽点与 C 的效率和可移植性相比,就完全可以忽略不计了。因此,类似上面的设计在实际的软件框架中比比皆是,其中最为著名的便可以算是 Windows 中的 WNDCLASS::lpfnWndProc 与 DRIVER_OBJECT::MajorFunction 了。

消息驱动

OO 的另一大特点就是所有对象都是消息驱动的。在编程语言之中,消息驱动往往以函数调用的形式来具象化。这样一来,对“函数”这个相对静态的概念的要求就相应地提高了——它需要能够表达出“消息”的动态特性,比如抽象、封装和多态等等。以下哥分而述之。

C

又是 C。可以说除了函数指针之外,C 几乎没有任何可以称得上是“动态”的特色。但仅靠这一点,它却又无所不能。从这一点上来讲,Windows 的消息框架是一个绝佳的例子:

  1. 每个窗口对象的表现体都是一个名为 HWND 的句柄(Handle to a WiNDow),无论它是 Button、Edit 还是 Static。这个类型的统一定义既保证了多态所需要的顶层抽象,又规避了 C 在语法上的类型限制。
  2. 定义为整型的消息标识和消息参数(WPARAM 与 LPARAM)使得消息的驱动具备完全的多态性和灵活性。

需要说明的是,以 C 来实现消息驱动的抽象和多态往往是借由类型转换和函数指针来实现的,这个灵活的福利是以牺牲类型检查为代价才得来的。因此,这就要求消息驱动的攻方与受方都必须严格地遵循他们自己的调用(参数传递)约定——反正,不作死就不会死。

C++

从 C++ 最原始的名称“C with classes”就可以看出来,C++ 最初的设计目的就是为了给 C 带上类。换用本文的角度来描述,也就是将 C 的语言边界扩大,以满足 OOP 的部分需求。
由于脱胎于效率至上的 C,因此 C++ 无论如何发展,都不能离开——或者说无法摆脱——C 的效率原则。于是乎,同是类的成员函数,却有了虚函数与非虚函数之分。虚函数以函数指针的动态方式提供了多态性的支持,而非虚函数则保证了静态调用的效率。站在 Bjarne Stroustrup 的角度上来看,他认为每个使用 C++ 的程序员都应该清楚地知道自己在做什么,所以怹老人家就放心地把这两种函数都交给了我们。当然,这也成了 C++ 饱受诟病的原因之一。我在去年底的时候曾经在微博上进行了 一次讨论,所针对的正是这个问题,这也成了我写作本篇 blog 的最直接原因。
言归正传。C++ 的确带来了 OO 的特性支持,但是它对消息驱动的支持却仍是有着很大的局限性。考虑下面的代码:

1
2
3
4
5
6
7
8
9
10
class A { /* ... */ };
 
class B : public A
{
public:
    virtual void f(void) { /* ... */ }
};
 
B b;
A *a = &b;

在 C++ 的语法限制下,指针 a 是不能直接调用 f() 的。虽然我们可以通过一个强制类型转换来获得一个 B 的指针,但这样做不免过于丑陋,另外安全性也不能保证——因为在绝大部分情况下你并不知道 a 是不是真的指向了一个 B 的实例。
还是之前那句话:当特性需求超越了语言边界之外,那么只有由框架来进行补充。考虑下面的代码,它借助了 COM 框架解决了这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A : public IUnknown { /* ... */ };
 
class B : public A
{
public:
    STDMETHOD(f)(void) PURE;
};
 
A *a = NULL;
// CoCreateInstance...
 
B *b = NULL;
if (SUCCEEDED(a->QueryInterface(IID_IB, (void **)&b))) {
    b->f();
}

Objective-C

走笔至此似乎没有理由不谈谈 Objective-C 了。几年前随着 iPhone 的横空出世,Objective-C 可以说是以迅雷不及掩耳盗铃之势红透了大江南北。较之 C/C++ 而言,它的标准完全握在水果公司自己的手中,因此无需看着委员会的眼色行事,语言边界的推进也就可以在不扯到蛋的前提下迈出更大的步子。
以消息驱动为例,Objective-C 的选择子(selector)是一个相当优秀的设计。考虑前文中由基类调用派生类方法的例子,在 Objective-C 之中可以这样完成:

1
2
3
4
5
6
7
8
9
10
11
12
@interface A : NSObject
@end
 
@interface B : A
 
- (void)f;
 
@end
 
B *b = [[B alloc] init];
A *a = b;
[a performSelector: @selector(f)];

这里凸显了 Objective-C 的优势:水果独占了它的标准,所以它可以名正言顺地和框架捆绑在一起。这样一来,任何框架级的约束便可以顺理成章地扩张到语言的本身。考虑上面这个例子,我们不需要像 COM 一样强调每个组件实现都要继承自 IUnknown 或其子类;正相反,程序员们会觉得为 NSObject 写子类是一件很自然的事情。——很显然,只有统一的、设计完善的顶层抽象,继承和多态才会发挥出更为鸡冻人心的力量。
从语言边界的角度来讲,Objective-C 的编译器将每一个消息的驱动行为都转换成了 objc_msgSend 的统一调用,较之 Windows 存在于框架中的 SendMessage 而言,Objective-C 的语言边界无疑是更为深远的。

订阅本站

没有评论

(Required)
(Required, will not be published)