说在前面的

从面相对象说起

面向对象的程序设计(Object-Oriented Programming,简记为OOP)这个概念大家都有所耳闻,目前(2017.12),在Tiobe世界语言排行榜上排前十的语言中,C语言和Assembly language(汇编)外的八种语言均原生支持面向对象的程序设计

怎么判断一种编程语言是否支持OOP呢?看看这门语言是否支持类(class)、对象(object)、封装(encapsulation)、继承(inheritance)等功能和特性,支持这些就可以进行面向对象编程。拿Objective-C(OC)来说,类就是Class,对象就是instance,万物的基类是NSObject,这些东西在C语言里并不存在,是OC使用C语言的结构体(struct)抽象出来的产物。

我们从Objective-C的名字上也能看出一些端倪,直译过来是对象化的C语言,当然不仅是OC,排行榜前十中的C++同样是C语言的一个超集;C#和Java同样属于类C语言,把面向对象做的更加彻底;PHP虽然是脚本语言,其解释器是使用C语言写的;而我们常说的Python,其全称则是CPython,也是用C语言实现的解释器,当然Python解释器也有Java和C#实现的版本。

为什么C语言,比其他语言显得更底层呢?接触过的朋友相信都有很深的体会,C语言的程序,是在和图灵机硬件打交道,变量、数组、结构体,声明在堆内存就要为其分配内存空间大小,分配了内存,就要手动回收;数组还要区分静态和动态,每块数据占几个字节,躺在内存的什么位置,一切都按编程人员的安排。所以有人说C语言就是一个高级汇编,想起来确实有一分道理(笑)。但在智能手机、移动计算机计算能力大大提升的今天,计算资源早已不是通用编程首先考虑的问题,相比于C语言强迫编程人员从机器的角度设计程序,抽象程度更高的OOP才更接近人脑的思维方式,才更适合提高软件工程师的编程效率。

即使如此,仍有一部分人至今站在OOP的对立面,从代码复杂度、建模能力要求等方面提出异议,坚持写C++、Python、PHP的时候不构造类,写纯过程的程序。但其实,这些自称为原C党的朋友,并不能说自己没有使用OOP,因为这些语言中变量,跟C语言中的变量,有本质的不同。

就用字符串数组来举例子,C语言是没有string类型的,只有字符数组,用\0来标记字符串结束;而其他语言中的string则是早已封装好的字符串类(Class),用起来跟整型无异。

C语言中字符串和数字变量声明

1
2
char name[] = "Tom\0";
int age = 12;

Python中字符串和数字变量声明

1
2
name = "Tom"
age = 12

C++中字符串和数字变量声明

1
2
string name = "Tom";
int age = 12;

我们在Python和C++中使用字符串,早已不是在直接与设备内存打交道,而C语言中的“字符串”还停留在只是内存中的一段连续空间的阶段。

再来看一看数组,C++虽然也支持C的数组,但我想对比的其实是C++标准库中的向量(Vector),以及Python中的链表(List),这些高级容器同样是基于OOP理念设计的类,仍只有C语言的数组内容直接映射在内存上。

所以即使你不构造Class,在C++、Python、PHP中仍在使用对象和实例的OOP特性,即使开发的是线性程序。

彻底的OOP

经常会看到有人抱怨Java把面向对象的理念做的太过头,C#作为Java的仿制品,也同样逃脱不了被诟病的现实,但其稳定性也是有口皆碑。然而真正把OOP理念实现的彻头彻尾彻彻底底的,反而是最早的OOP语言之一的Smarttalk,让我先看一段Samrttalk的代码

1
Transcript show: 'Hello world'

这是Smarttalk版本的Hello world程序,Transcript是Squeak(这是Smalltalk语言的一种版本实现)环境里,把信息显示到屏幕上的一个对象。这段代码是用冒号给这个对象发送了一个消息(Message),如果给这段代码加上一对中括号,是不是像极了Ojective-C,没错,因为OC就是参考Smarttalk设计的Runtime。

同样,Samrttalk也支持中括号的写法,我们可以把上面的一段代码段落,赋值给一个变量:

1
t := [ Transcript show: 'Hello world']

这个t变量,其实是一个闭包(BlockClosure)对象,相同的概念在C++ 11标准里才出现,相比之下Smarttalk的设计理念真的很前卫。而OC作为Smarttalk的追随者,更是拥有NSOperation类来实现闭包,相比之下,block并不是基于OOP的设计。

C++的blcok和ObjC的NSOperation,这里block写法OC同样支持

1
2
3
4
5
6
7
8
9
10
void hello = ^ {
NSLog(@"hello world");
};
hello();
NSBlockOperation* block = [NSBlockOperation blockOperationWithBlock:^{
// 做一些操作
}];
[[NSOperationQueue mainQueue] addOperation:block];

要注意的是NSBlockOperation是在OC支持block以后才出现的类,在此之前要使用NSOpertaion,我们需要继承NSOpertaion类,并重写这个类的-(void)main方法,这无疑是一件十分繁琐的事。

一切皆对象

OC作为Smarttalk的追随者,在OOP的理念上是要强于C++、Python和PHP的,interfaceimplementationgettersetter的接口设计,和Java、C#相互参考,水平相近,但仍比Smarttalk和Ruby略逊一筹。

熟悉Cocoa框架的朋友都知道,UI绘制框架CoreGraphic中仍然要使用大量的CG开头的C语言函数,点、线、面的容器,依旧是CGPoint,CGSize,CGRect这些C语言结构体;数字变量依然是int、NSInteger、NSNumber(数字类)混着用,相互转换忙的不亦乐乎。当然这一切在OC支持字面量特性(Literals)以后有了好转:

1
2
3
4
//通过@符号直接把普通变量转换为数字对象
NSNumber *myIntegerNumber = @8;
//转回来
NSInteger customNumber = [myIntegerNumber integerValue];

相比之下,Smarttalk和Ruby做的更彻底,更好用,下面是用Smarttalk重复输出十次Hello world的代码,给数字10发timesRepeat消息,重复消息参数中的闭包:

1
10 timesRepeat: [Transcript show: 'Hello world']

为什么整数类要设计这么方法呢?因为Smarttalk中并没有循环语法,甚至其他语言常见的条件语句if/else在Smarttalk中都是不存在的,而都是使用OOP的理念实现,有兴趣了解更多关于Smarttalk的内容,请来这里

给对象发消息是更符合人类思维模式的设计

这里我们从继承Smarttalk理念的Ruby说起,虽然其使用点语法替代了冒号,但仍能看出Ruby中的数字类型,就是数字对象。

1
2
//将数字对象102转换成字符串对象
102.to_s

用Smarttalk实现则是

1
102 printString

相比之下Python则像是一个作者对OOP还处于感性认知阶段设计出来的语言,所以会设计出len()、map()、fliter()这种C语言函数风格的接口,例如我在OC中我们获取数组的长度使用count属性,使用点语法或者中括号消息都可以获取(关于OC中的点语法和中括号语法我们后面再聊)

1
2
3
NSArray* a = @[@(1),@(2),@(3)];
a.count;
[a count];

这很面向对象,因为我们要获取数量的主体数组实例a,发消息让他返回长度很符合人类的思维逻辑。同样的我们看看Ruby,也是一样的操作

1
2
3
a = [1,2,3]
a.length
a.size

然而当我使用第一次写Python代码的时候,我经历了很多人都遇到过的情况,不知道字符串或者数组如何获取长度。因为Python中string和list都没有length、size、count、len等属性和方法,然后我们发现Python提供了一个len()方法获取序列长度,这个方法接受一切的对象作为参数。

1
2
3
4
5
a = [1,2,3]
len(a)
s = "123"
len(s)

针对这个问题,有一部分人认为不是问题,他们说做OOP不要太教条主义,len在前在后能有很大差别么?我想说真的是有的,这个看似简单的前后问题,其实影响了实际的编程体验,就是是否基于对象思考问题的体验。

一方面,len()方法像一个凭空存在的方法,不依赖于任何类和对象,也不是依附于某个模块,知道它存在,才会去使用它,同样的还有Python中的type()、map()方法等。另一方面,这一类方法到底可以用于什么类型的对象,开发者心里也没底,必须对照接口标明的参数类型使用。

这一切无疑不利于程序开发的思维连贯性,有朋友可能觉得我说的言过其实,我这里举一个例子大家体会一下何为思维连贯性。

需求是将一段英文字符串的单词逆序,How are you处理成you are How

我们用OC实现如下:

1
2
3
4
5
6
#import <Foundation/Foundation.h>
NSString* reverse(NSString* text) {
NSArray *words = [text componentsSeparatedByString:@" "];
NSArray *reversed = [[words reverseObjectEnumerator] allObjects];
return [reversed componentsJoinedByString:@" "];
}

Python实现为

1
2
3
4
def reverse(text):
a = text.split(' ')
a.reverse()
return ' '.join(a)

Ruby的实现为

1
2
3
def reverse(string)
return string.split.reverse.join(' ')
end

观察出来区别了了吧,重要的不是Ruby只用了一行代码,而是Ruby相比于OC和Python,省去了很多中间变量,别看只是一点点节省,其实省去我们实际开发中很大一部分无用工作。当然,OC可以通过括号多层嵌套连贯起来写,也能达到同样的效果,但我们并不推荐这样做,因为OC的方法名偏长,如果缩进不当,会让代码更难理解。

相比之下,Python的接口设计更滑稽一些,首先在Ruby中

1
2
array.reverse!
array.reverse

是两个不同的方法,前者只逆转array,没有返回值。后者则返回一个新的逆转数组对象,Python没有类似设计。

其次Ruby和OC都将join方法设计在array类里,唯独Python将其设为字符串类型的方法,导致了Python没法连贯地将中间参数略去。