本文从OC的可变数据和不可变数据作为引子,开始聊聊我眼中的OC和SmallTalk,想到哪儿就聊到哪儿了。本文中的观点都是个人观点,如果大家理解不一样,纯属正常,欢迎讨论。关于可变数据和不可变数据就不多聊了,有很多文章已经聊过了

OC中的可变/不可变数据

写过OC都知道,OC的基础类型分为Mutable和IMMutable两种类型,分别对应可变数据和不可变数据。OC在具体实现的时候,使用了类簇这样的设计模式。

类簇

不可变数据和可变数据内部实际上是一个家族类,多个类各司其职。而暴露给使用者的只有不可变数据和可变数据这两类。并且可变数据继承自不可变数据。这样强调了OC的简洁性,使调用者不需了解多个内部的家族类。

NSArrayNSMutableArray就是这样的典型实现,具体点我。但是这样也就限制住了拓展性。比如说,我现在想要实现一个数字排序的数组类,我要怎么做?很简单,创建一个继承自NSArryNSOrderArray,写一些排序功能的代码,初始化后自动给数组排序,查找的时候用二分查找就好了。迄今为止没有问题,现在我想再实现一个可变类型的NSMutableOrderArray,我思考了一会,懵逼了。NSMutableOrderArray是该继承自NSMutableArray还是NSOrderArray?如果继承自NSMutableArray,那我就要重新写排序代码,如果继承自NSOrderArray,那我就要重新写可变代码。这两条路,虽然最后可以实现出来NSOrderArrayNSMutableOrderArray,但是继承关系肯定是乱的。

如果我知道NSArryNSMutableArray内部的家族类的细节,_NSArrayI_NSArrayM,就可以自由组合出我想要的数字排序数组类。很可惜,作为调用者这些细节被屏蔽掉了。

那我该如何实现出来这样的类?像NSOrderSet一样,将NSOrderArray继承自NSObject,从头再实现一个数组,然后再将NSMutableOrderArray继承自NSOrderArray。这个工作量就太大了,还不如每次使用NSArray的时候再排序,牺牲点性能没什么关系。在这个过程中说明了,类簇是OC简洁性和拓展性的权衡了。

如果不同类簇设计,有没有别的设计方法解决这个问题,多继承?C++将多继承加入又拿掉,加入又拿掉。实际上多继承带来的麻烦和解决的问题,还不一定谁多,多继承Pass。AOP?实现出面向切面编程的协议,比如”NSIMMutableSequence“、”NSMutableSequence“、”NSOrderSequence“,这三个协议分别对应不可变数据集合、可变数据集合、排序集合协议。实现的类组合想要的协议即可。这样使简洁性下降了,自己在实现NSMutableOrderArray的时候,conforms了”NSMutableSequence“和 “NSOrderSequence“后必定要写几个关于可变集合的方法。不过,还可以接受,降低了点简洁性换来了很强的拓展性。我个人觉得是个不错的设计方式,Swift就更加向这种思想靠拢。

我们还可以从Apple的角度想想OC。你能想象Apple挑了一门复杂的语言,然后你要做一个APP,需要先学1年这个复杂的语言?Excuse me?这样,Apple早就倒了。

桥接

既然同种数据类型有可变数据和不可变数据之分,这两者就要提供互相转换的方法。而OC中的拷贝就是这两者的桥接。从不可变数据mutableCopy就可以得到可变数据,可变数据copy就可以得到不可变数据。

更多关于拷贝的问题,这篇文章讲的很好,推荐一下。copy实际上是对可变数据才有用的操作,对于不可变数据再复制出来一份一模一样的数据是没有意义的。对于可变数据copy就是,将状态消除,制作出一份不可变的“快照”,而mutableCopy是制作出一份一模一样的可变数据,就像是这个数据开启了另一条时间线,另一个宇宙。OC中的设计就是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
@interface NSArray<__covariant ObjectType> : NSObject <NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration>

- (id)copy; //浅复制
- (id)mutableCopy; //深复制出NSMutableArray

@end

@interface NSMutableArray<ObjectType> : NSArray<ObjectType>

- (id)copy; //深复制出NSArry(快照)
- (id)mutableCopy; //深复制出NSMutableArray(另一条时间线)

@end

如果光看这两个接口,我可能会这样设计:

1
2
3
4
5
6
7
8
9
10
11
12
@interface NSArray<__covariant ObjectType> : NSObject <NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration>

- (id)bridgeToMutable; //深复制出NSMutableArray

@end

@interface NSMutableArray<ObjectType> : NSArray<ObjectType>

- (id)bridgeToIMMutable; //深复制出NSArry(快照)
- (id)copy; //深复制出NSMutableArray(另一条时间线)

@end

Apple将copymutableCopy放在了NSObject里面,让所有子类都可以不再多添加方法也没有大问题,其中也可能有些内存管理上的考虑。但是我个人觉得后者这么设计,接口会更加明确,思想体现的更加直接。

架构设计

2016年,国外iOS圈开始不断尝试IMMutable Model Layer,这其中有FacebookPinterest。Facebook还开源了Remodel加强这一实践,以后有时间会写篇文章好好深聊这个东西,此处先留坑。这实际上,也是响应式编程思想的体现。国内就不知何时会搞起IMMutable Model Layer这样的架构设计,也许就在这2017年吧。

不同语言中的可变/不可变数据

Python中的可变数据和不可变数据是根据数据类型定的,list和dict都是可变的,str和tuple都是不可变的。可变数据没有不可变的版本,不可变数据也没有可变的版本。

JS中的list、 map都是可变的,原本是没有不可变版本的。Facebook开源了个immutable.js,提供这些数据的不可变版本。这样JS中就和OC一样数据可以分为可变数据和不可变数据了。

既然别的语言都不原生提供可变数据和不可变数据版本,那OC为什么要搞特殊提供呢?使用过OC的,都听说过OC的设计思想是基于SmallTalk的。要想了解为啥OC这么设计,那就得追溯到SmallTalk。

SmallTalk

smallTalk_immutable

原句在这里,这是1993年给出的一份SmallTalk演变历史。关于Local State在这一段:

This is probably a good place to comment on the difference between what we thought of as OOP-style and the superficial encapsulation called “abstract data types” that was just starting to be investigated in academic circles. Our early “LISP-pair” definition is an example of an abstract data type because it preserves the “field access” and “field rebinding” that is the hallmark of a data structure. Considerable work in the 60s was concerned with generalizing such structures [DSP ]. The “official” computer science world started to regard Simula as a possible vehicle for defining abstract data types (even by one of its inventors [Dahl 1970]), and it formed much of the later backbone of ADA. This led to the ubiquitous stack data-type example in hundreds of papers. To put it mildly, we were quite amazed at this, since to us, what Simula had whispered was something much stronger than simply reimplementing a weak and ad hoc idea. What I got from Simula was that you could now replace bindings and assignment with goals. The last thing you wanted any programmer to do is mess with internal state even if presented figuratively. Instead, the objects should be presented as sites of higher level behaviors more appropriate for use as dynamic components*.

文中主要说的是SmallTalk从LISP-Pair中主要学习的地方就是重新绑定和赋值(重新绑定和赋值实际上就是不可变数据,SICP中有提),并且不希望程序员乱用状态。使用面向对象编程应呈现为,将高层次的抽象行为动态的绑定到对象身上。

还有下面这一段:

Where does the special efficiency of object-oriented design come from? This is a good question given that it can be viewed as a slightly different way to apply procedures to data-structures. Part of the effect comes from a much clearer way to represent a complex system. Here, the constraints are as useful as the generalities. Four techniques used together—persistent state, polymorphism, instantiation, and methods-as-goals for the object—account for much of the power. None of these require an “object-oriented language” to be employed—ALGOL 68 can almost be turned to this style—an OOPL merely focuses the designer’s mind in a particular fruitful direction. However, doing encapsulation right is a commitment not just to abstraction of state, but to eliminate state oriented metaphors from programming.

尤其是最后一句话,SmallTalk提出的面向对象编程思想,不仅要抽象出状态,还要尽力干掉状态。这就明朗了,SmallTalk主张的是多使用不可变数据干掉状态。所以,OC要分为可变和不可变类型。能用不可变类型就用不可变类型,必须牵扯到状态时,再用可变数据。其实,这也就看出来了这两年提出的响应式编程、Facebook提出的immutable model layer都是SmallTalk早在几十年前主张的东西,只不过我们没有注重这种思想编程。然后在合适的时机,被人挖出来,重新强调一下。

既然聊到这里,就接着来聊一聊SmallTalk吧,SmallTalk只有两个核心思想:

  • 一切皆对象(Object),包括3、4这样的整形数字,包括bool类型。如果消息有参数,我想你已经猜到了,跟在:后面。
  • 过程抽象即发消息,其中包括3+4这样的简单算术,可被解释为给3发送一个4作为参数的+消息。

关于这点还有更醍醐灌顶的,甚至包括条件语句(if)和循环(for)都是向对象发消息。if语句的一般形式是这样的

1
bool表达式 ifTrue:[ ] ifFalse:[ ]

这解释为向bool表达式的结果true或者false对象,发送ifTrue:ifFalse:消息。如果是true就执行ifTrue的参数,如果false就执行ifFalse的参数。而[ ]表示的参数就是我们熟悉的块!SmallTalk中的块用[ ]表示,并且也是一个对象。

这一切都基于SmallTalk的思想:过程抽象即发消息。并且会呈现出在写SmallTalk语言的时候,都是这样的形式:<消息接受者><消息>。那SmallTalk如何解释?这些消息是有结合优先级的,以下为优先级从高到低:

  • 一元消息:没有参数的字母消息,比如无参数的方法。
  • 二元消息:非字母的消息,比如+。
  • 关键字消息:有参数的字母消息,比如有参数的方法。

比如,年底你拿到了年终奖,又遇到了个心仪的女孩,然后你早上从床上醒来,发现这只是一场梦:

1
yourDream: yourAccount annualBonus + girl

这句结合的优先级是这样:

1
yourDream: ((yourAccount annualBonus) + girl)

解释结合的时候,还有个有趣的一点。基本算术加减乘除,都是消息,而且都是二元消息,优先级是一样的。如果这样写,不会得到你想要的结果:

1
3 * 4 + 5 * 6 = 102

要想要得到你想要的结果,必须要手动加括号:

1
(3 * 4) + (5 * 6) = 42

如果将OC与SmallTalk做下对比,你会发现,OC可不是一切皆对象,也不是一切皆发消息。当然,OC也有与SmallTalk一致的地方:

  • self不管在什么上下文中,永远代表消息的接受者。super代表将覆盖的父类方法发送给消息的接受者。
  • 只有对象可以访问自己的变量,即变量私有。
  • 子类可以覆盖父类方法,但不能重新定义变量。

到这里,再重温一下这句话:使用面向对象编程应呈现为,将高层次的抽象行为动态的绑定到对象身上。SmallTalk整个语言呈现出<消息接受者><消息>,不就是这句话最好的证明吗?

是不是很惊讶SmallTalk的纯粹?不过,SmallTalk并不是工业级语言,这也代表着SmallTalk没用那么多的妥协和trade-off,是门理想语言。SmallTalk作为提出面向对象编程概念的语言,更多的是为后来面向对象编程工业级语言铺好了路。

引用

程序设计语言概念和结构(原书第2版)