CS 科普 —— 万物皆对象

题图来自 Cyandev 随便画的。

从 C 语言诞生起,面向对象这个概念就开始被提出。结构体也许是最简单的组织一类事物的事物,只不过 C++ 出现后,继承、多态的概念才产生。面向对象的重要思想是一切皆是对象。C++ 对于这句话践行得并不是特别好,所以我们就拿 Objective-C 来说事。

Side note: 事实上,虽然 Objective-C 诞生比 C++ 早,但是我认为其面向对象思想践行得却比 C++ 要好,尤其是消息派发机制以及其他动态特性。而 C++ 很多需求的实现需要依赖模板这种编译时技术,实现了 ZCA(零开销抽象)的同时却降低了程序的可读性和灵活性。

我们知道,Objective-C(以下简称 OC)很棒的地方就是它的 Runtime,就同指针对于 C,这也是 OC 的精华。它一定程度上实现了动态语言的部分特性(对于动态语言的界定我这里可能说得比较模糊,简单理解吧),而又具有编译型语言应有的优良特点。

OC 实现类的方法也是 Runtime 的功劳,简单来讲,OC Runtime 通过一定数据结构来描述一个类,类的实例对象又通过一定的引用方式可以访问到类的基本信息。

所以说,类到底是什么?我们举一个简单的例子:

@interface Person : NSObject
@property (copy) NSString *name;
+ (instancetype)personWithName:(NSString *)name;
- (void)talk;
- (void)play;
@end

这里我们也无需具体实现它,如果要用结构体来描述这个类的一个实例,我可能会这样写:

struct Person {
    NSString *name;
};

哦,看出问题了吗?我们似乎缺失了一些信息,首先是实例方法不见了。于是我这样:

struct Person {
    NSString *name;
    void (*talk)(struct Person *self);
    void (*play)(struct Person *self);
};

加一个函数指针不就行了么。然而,如果每个对象都需要存储一大堆重复的函数指针,是不是有点太浪费了呢。看起来我们需要另外一个对象来存储一些函数指针。

struct PersonClass {
    void (*talk)(struct Person *self);
    void (*play)(struct Person *self);
};

struct Person {
    PersonClass *cls;
    NSString *name;
};

OK,这样一来,我们只需要将每个 Person 实例的 cls 指针设置为全局统一的 PersonClass 对象的地址就行了,我们就可以通过每个实例的 cls 拿到这个类的信息,进而就可以获取到所需的方法了。

到目前为止,我们有了两种结构,一种是每个 Person 实例都有的 Person 结构体,它存储了不同 Person 实例各自的数据,实现了个体的差异化;另一种是每个 Person 实例都需要引用的一个『字典』一样的 PersonClass 对象,这个对象全局只有一个,它涵盖了所有 Person 实例共性的部分,比如类的名字、实例的方法、一些其他额外属性等等。这套体系看起来很完美嘛。

等等…继承呢?

到现在为止我们都一直在讨论 Person 类自己,假设我们现在有了一个新的类 Teacher,是 Person 的子类,那怎么办呢?不管怎么样我们先把它的结构写出来:

struct TeacherClass {
    void (*talk)(struct Teacher *self);  // Kind of different.
    void (*punishStudent)(struct Teacher *self, struct Person *stu);
};

struct Teacher {
    TeacherClass *cls;
    NSString *name;
    NSArray *students;
};

相较于 Person 类,Teacher 多了一个 students 的实例变量和一个 punishStudent 的实例方法,与此同时他的 talk 方法跟父类还不一样。然后我们基于这个实现看看下面这些问题:

  1. 我现在想调用一个 Teacher 对象的 play 方法;
  2. 我想在 talk 方法中调用父类实现。

好像目前这两个需求都实现不了,因为我们的 Teacher 类跟 Person 类没有建立起任何联系,play 方法的函数地址我根本找不到啊 ╮(╯_╰)╭

有了之前的经验我迅速做了修改:

struct TeacherClass {
    PersonClass *super;
    void (*talk)(struct Teacher *self);  // Kind of different.
    void (*punishStudent)(struct Teacher *self, struct Person *stu);
};

很简答,在 TeacherClass 里加一个 PersonClass 类型的 super 字段(暂时不用考虑类型的问题,这些指针编译时统统可以转换成 void *,这里为了大家好理解就写出类型了),由于所有的类对象都有个 super 字段,所以我们可以一级一级搜索,如果自身这个类没有所需的方法,就通过 super 去找它的父类,以此类推找到根类。

到现在为止还需要解决一个问题,记得我们的 Person 类有一个类方法吗?怎么调用它呢?如果你用过 OC,应该知道调用一个对象的方法怎么写:

[obj foo];

调用一个类方法也很简单:

[obj.class bar];

// or

[ObjClass bar];

这就是万物皆对象,只要是个对象,它调用方法的写法都是一致的,中括号左侧为消息接收者,右侧为方法选择子和参数等,消息接收者就是一个个的对象。

所以,类本身其实也是个对象

啊!什么?类也是对象,我明白了!

struct PersonClass {
    void *cls;
    void *super;
    void (*talk)(struct Person *self);
    void (*play)(struct Person *self);
};

这不嘛, 对象肯定有个 cls 字段指向它的类对象啊,所以我们的 TeacherClass 对象也有 cls 可以指向它们共有的类对象,先别管它是什么类型。然后由于我们需要调用类方法,那么自然这个方法的地址要从 TeacherClass 的 cls 字段指向的类对象中找,那么这个类对象的定义应该就是这样:

struct PersonMetaClass {
    void *cls;
    void *super;
    Person *(*personWithName)(...);
};

嗯,非常好,里面有我们需要的类方法,顺便,我还给它起了个好听的名字,就叫 PersonMetaClass 吧!

其实到这里,我们就基本实现了 OC 中的类型系统了,它就是这么简单。

可以看看这个经典的图:

《CS 科普 —— 万物皆对象》每个对象要找实例方法,就通过 isa(也就是我们写的 cls 字段)找到它的类对象,如果这个类对象中没有要找的方法,就通过 superclass 找它的父类。如果要找一个类的类方法,就通过类的 isa 找到它的 MetaClass,然后继续使用上面的规则。由于 MetaClass 也是 Class,Class 又是对象,所以 MetaClass 也有它自己的类对象,这里 OC 统一规定是 NSObject 这个基类了,其实这里置空或者指向自己我认为都是没问题的,看设计者怎么设计了,总之要有这个指针,因为他们都是对象。

其实面向对象并没有严格的界限什么是类,什么是类的实例,它们只不过是角色不同的对象而已,这其实才是最纯正的面向对象。这似乎是先鸡先蛋问题,但很多地方的设计又都是这样,很奇妙。

所以本文就借着 OC 随便扯了扯面向对象的最基本的一个特点,一切皆对象,剖析了一下 OC 的实现方式,没讲具体的技术细节,只是讲了大致的框架,细节需要各位自己通过分析源码来了解。相信大家至少能明白 Meta Class 了吧,嘿嘿。

    原文作者:算法小白
    原文地址: https://juejin.im/entry/58c80e4444d90400699faa77
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞