讲述面向对象中的一个重要概念——继承,使用继承 能够方便地在已有类的基础上进行扩展,定义一个具备父 类所有功能的新类。css
咱们在定义一个新类的时候,常常会遇到要定义的新类是某个类的扩展或者是对某个类的修正 这种状况。若是能够在已有类的基础上追加内容来定义新类,那么新类的定义将会变得更简单。面试
像这种经过扩展或者修改既有类来定义新类的方法叫做 继承 (inheritance)。在继承关系中,被继
承的类称为 父类 (superclass),经过继承关系新建的类称为 子类 (subclass)。编程
继承意味着子类继承了父类的全部特性,父类的数据成员和成员函数自动成为子类的数据成员
和成员函数。除此以外,子类还能够ide
● 追加新的方法函数
● 追加新的实例变量学习
● 从新定义父类中的方法测试
固然,若是子类中只追加新的实例变量而不变动方法则没有任何意义。子类中从新定义父类的方法 叫做 重写 (override)。spa
让咱们来看几个例子。在图 3-1 中,类 B 是类 A 的子类,类 B 继承了类 A 的实例变量和方法, 但重写了 method2。类 C 也是类 A 的子类,类 C 中增长了新的实例变量 z 和新的方法 method3。类 B 和类 C 都是类 A 的子类,不管类 A、类 B 和类 C 的任何一个实例变量都可以执行方法 method1 和 method2。设计
父类和子类是一种相对的称呼。例如,在上例中,若是以类 B 为父类又派生出一个子类 D,那 么类 B 相对于类 A 是子类,相对于类 D 却为父类。3d
另外,在集合用语中,子集指的是比较小的集合(相对于父集),但在类的状况下子类通常是父 类的扩展。为了不这种命名上的混乱,C++ 中把父类称为 基类 (base class),把子类称为 派生 类 或 导出类 (derived class)。考虑到面向对象的程序设计中通常都使用父类、子类的叫法,本书也 使用这种叫法。
一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个个人iOS交流群:1012951431, 分享BAT,阿里面试题、面试经验,讨论技术, 你们一块儿交流学习成长!但愿帮助开发者少走弯路。
类的层次结构
假如以某个类为父类生成若干子类,而后再继承这些子类并生成更多的子类,如此循环下去就 可能会生成一颗倒立的树,它由经过继承而彼此关联的类组成,这样的树称为 类层次结构 (class hierarchy)。
位于类层次最顶端的类称为 根类 (root class),如图 3-2 所示。
NSObject 是 Cocoa 环境下的根类,Cocoa 中全部的类都直接或间接地继承了 NSObjectA。新建的 任何类都必须是 NSObject 或它的继承类的子类。NSObject 中定义了全部 Objective-C 对象的基本 方法。
因为这种类的层次关系,Objective-C 的全部对象都继承了 NSObject 类中定义的各类属性。 Objective-C 的对象可以做为对象来使用,就是由于类 NSObject 中定义了对象的基本功能。 在面向对象的语言中,有的和 Objective-C 同样有惟一根类,例如 Java 和 Smalltalk 等;有的则不 存在惟一根类,如 C++。
若是想经过继承为某个类定义一个子类,该怎么办呢?
Objective-C 在子类的接口部分声明继承关系。在 2.2 节中咱们已经说明了如何定义类的接口,这 里再介绍一遍。
定义父类 A 的子类 B 的时候,“类名”是新类 B,冒号后面的“父类名”是须要继承的类 A。
至此为止本书中的父类都使用了 NSObject,这是由于 Objective-C 中全部的类都要继承根类, 而 NSObject 是 Objective-C 中全部类的根类 。若是子类有想继承的类,就要直接指明该类为父类,否 则就须要指定 NSObject 为父类。前文中定义 Volume 类的时候,由于 Volume 类并无特别想继承的 类,因此直接使用了 NSObject 做为父类。
实例变量的声明中只须要声明新增的变量。若是没有新增的变量,则只须要加上 {} 便可,有时 甚至连 {} 均可以省略。 方法的声明中只须要追加新增的方法。若是要覆盖父类中已声明的方法(重写),则须要在接口 中对方法从新声明。一般咱们会给重写的方法加上注释,以便理解。
下面展现了定义类 A 的子类 B 时接口部分的状况。变量 x 和方法 method1 继承于类 A,因此不 须要从新声明,方法 method2 的声明也能够被省略。
假设有一个已经定义好了的类 Alpha,那么头文件 Alpha.h 就应该已经存在。要定义类 Alpha 的 子类 Beta 的时候,头文件 Beta.h 中必须包含 Alpha.h。不知道父类定义的话是没法定义子类的。因此 包含父类接口的头文件是必须的。
类的实现部分必须引入包含类的接口部分的头文件。实现部分须要包含新增和重写的方法的实 现。固然实现部分也能够定义各类局部函数和变量。
图 3-3 的文件 Gamma.m 的方法中调用了方法doSomething ,这个方法是从类 Alpha 继承而来 的。 文 件 Gamma.m 引 入 的 头 文 件 Gamma.h 中 引 入 了 Beta.h,Beta.h 中 又 引 入 了 Alpha.h, 所 以 Gamma.m 能够调用方法doSomething。
类的定义能够不断地使用继承向下扩展,但不管怎么扩展,只要保证了这种头文件的引入方式, 任何一个派生类中就都能使用父类中定义的变量和方法。
子类中定义的方法,除了可以访问新追加的实例变量外,也可以访问父类中定义的实例变量。
另外,由于继承的缘由,子类也能够响应父类中定义的消息。但若是子类中重写了父类的方法, 就须要注意实际运行中到底哪一个方法(父类的仍是子类的)被执行了。
如图 3-4 所示,类 A 包含方法 method一、method二、method3。类 B 是类 A 的子类,类 B 中从新 定义了 method2。类 C 是类 B 的子类,类 C 中从新定义了 method1。
咱们来看看给类 B 的实例变量发送消息时的状况。首先,假设向类 B 的实例对象发送了对应 method1 的消息,即进行了方法调用。虽然类 B 中没有 method1 的定义,但由于类 B 的父类类 A 中 定义了 method1,因此会找到类 A 的 method1,调用成功。消息 method3 的状况下也是一样的道理, 类 A 中定义的 method3 会被执行。method2 同前两个消息不一样,类 B 中定义了 method2,因此会使 用自身定义的 method2 来响应这个消息。
而给类 C 的实例发送消息的话会怎么样呢?类 C 中有 method1 的定义,因此会直接使用类 C 中 定义的 method1 来响应这个消息。类 C 中没有 method2 的定义,因此调用的时候会使用类 B 中定义 的 method2 来响应。类 C 和类 B 中都没有定义 method3,因此类 A 中的定义 method3 会被调用。
子类继承了父类以后,有时就可能会但愿调用父类的方法来执行子类中定义的其余处理,或者 根据状况进行和父类同样的处理或子类中单独定义的处理。让咱们来看看图 3-4 中的例子,若是要在 类 B 的 method2 的定义中调用类 A 的 method2,那么该怎么办呢?经过 self 调用 method2 的话,就 会变成递归调用自身定义的 method2。
若是子类中想调用父类的方法,能够经过 super 关键字来发送消息。使用 super 发送消息后,就 会调用父类或父类的父类中定义的方法。如图 3-5 所示,类 C 中定义了 method1 和 method3。类 C 的 method1 中经过 super 调用了 method3,这时被调用的 method3 是类 A 中定义的 method3。
super 和 self 不一样,并不肯定指向某个对象。因此 super 只能被用于调用父类的方法,不能经过 super 完成赋值,也不能把方法的返回值指定为 super。
新追加的实例变量有时须要被初始化。另外,子类也可能须要同父类不一样的初始化方法。这些 状况下就须要为子类定义本身的初始化方法。
子类中重写 init 初始化方法的时候,一般按照如下逻辑。其余以 init 开头的初始化方法也是同理。
请注意第一行调用了父类的init 方法,父类的init 方法会初始化父类中定义的实例变量。下 面是子类专有的初始化操做。
若是全部的类的初始化方法都这样写,那么根类 NSObject 的init 方法就必定会被执行。不然 生成的对象就没法使用。与此同时,这样作也能够防止漏掉父类中定义的实例变量的初始化。
执行的时候父类的初始化方法可能会出错。出错时则会返回 nil,这种状况下子类也不须要再进
行初始化,直接返回 nil 就能够了。
若是父类是 NSObject,则基本上不可能初始化出错,所以不判断这个返回值也是能够的。使用 传入的参数或经过从文件读入变量进行初始化时,由于值的类型错误或读取文件失败等缘由,初始 化有可能会失败。这种状况下,须要确认父类的初始化方法的返回值。另外,上例中对 self 进行了 赋值,关于这个赋值的含义咱们会在第 8 章中详细说明,这里只须要记住这是初始化方法的一种固 定写法便可。
生成实例对象的方法alloc 会把实例对象的变量都初始化为 0(后面会提到的实例变量 isa 除 外)。因此,若是子类中新追加的实例变量的初值能够为 0,则能够跳过子类的初始化。可是为了明确是否能够省略,最好为初值可为 0 的变量加上注释。
从程序的书写角度来讲,设定初始值的方法有两种,便可以在初始化方法中一次性完成实例变量 的初始化,也能够在初始化方法中先设置实例变量为默认值,而后再调用别的方法来设置实例变量 的值。例如,类 Volume 也能够经过先调用初始化方法init ,而后再调用setMax: 等方法来设定音 量的最大值、最小值和变化幅度。原则上来讲,初始赋值以后值再也不发生变化的变量和须要显示设 定初值的变量,都须要经过带参数的初始化方法来进行初始化。
咱们来定义一个带有静音功能的类 MuteVolume。该类只有一个功能,即当收到mute 消息时, 设置音量为最小。
类 MuteVolume 的定义很是简单,父类是已经定义好的类 Volume。子类 MuteVolume 除了可使 用父类 Volume 中定义的全部实例变量和方法以外,还新增长了一个 mute 方法。
这里使用了 Volume 做为父类,并引入了头文件 Volume.h。Volume 的父类是 NSObject,因此 , 因此就不须要再进行指定了。
没有定义新的实例变量,意味着子类中没有要追加的实例变量。
该测试程序的功能是从终端读入输入的字符串,并根据字符串的第一个字符来决定如何设置音 量。具体来讲,第一个字符为 u 时表示提升音量,d 表示下降音量,m 表示静音,q 表示退出程序。
编译子类的时候,须要连同父类一块儿编译和连接,不然就没法使用父类中定义的方法。本例中
编译所须要的文件一共有 5 个,即 Volume.h、Volume.m、MuteVolume.h、MuteVolume.m、main.m。
上面经过继承实现静音功能类的例子很是简单,让咱们来看一个更实用的例子。
假设该例子要实现两个功能。第一个功能是,当再次收到mute 消息时,音量会恢复原值;第二 个功能是,在静音状态下收到up 或down 消息时,会返回最小音量值,同时改变音量值。
实现这些功能的方法有不少,这里咱们增长一个 BOOL 类型的变量 muting,同时修改方
法initWithMin:max:step:和 方法value的 实现。
初始化方法initWithMin:max:step:首 先调用了父类的初始化方法,而后对新增的实例变量 muting 进行了初始化。如前所述,子类的初始化必定要在父类的初始化以后进行。
value方 法根据当前是否为静音状态返回不一样的值。静音状态下,返回最小值 min。mute 方法
中只须要改变实例变量 muting 的状态来标识是否静音,不须要更改音量值 val。
编译的状况和上一节同样。main.m 直接使用上一节的便可。
若是想在一个方法中调用当前类中定义的方法,能够利用 self。但若是存在继承关系,经过 self 调用方法时要格外注意。
在图 3-6 的例子中,有三个类 A、B、C。类 A 中定义了 method一、method2 和 method3 三个方法。 类 B 继承了类 A,重写了 method1 和 method3。类 C 继承了类 B,重写了 method2。
假设类 B 的方法 method3 想调用 method1 和 method2,经过 self 调用了 method1 和 method2。我 们来分析一下这个过程当中到底哪一个函数被调用了。对类 B 的实例对象调用 method3 方法时,首先会 经过 self 调用 method1,这个 method1 就是类 B 自身定义的 method1。接着,method3 经过 self 调用 method2,由于类 B 中并无 method2 的定义,因此就会调用从类 A 中继承而来的 method2。
而若是是类 C 的实例对象调用方法 method3 的话会怎么样呢?咱们首先来看看 method3,由于 类 C 中并无定义 method3,因此调用的是类 B 中定义的 method3。要注意这个时候 self 指的是类 C 的实例对象,当 [self method1] 执行时,由于类 C 中没有定义 method1,因此调用的是类 B 中 定义的 method1。而后,当 [self method2] 执行时,由于类 C 中定义了 method2,因此执行的是 类 C 中定义的 method2,而不是上例中类 A 中定义的 method2。另外还有一点须要注意,就算类 B 中定义了 method2,调用的也是类 C 中定义的 method2。
也就是说,self 指的是收到当前消息的实例变量 ,所以,就算是同一个程序,根据实例的类的不 同,实际调用的方法也可能不相同。
使用 self 的时候要必定当心,要仔细分辨到底调用了哪一个类的方法。即使如此,利用 self 的特 性来编程也是很常见的,更多详细内容请参考 11.1 节的内容。
而若是不使用 self 而使用 super,程序执行的结果会怎样呢?
图 3-7 是用 super 替代图 3-6 中的 self 的状况。使用 super 调用方法时,最后被调用的方法是类 B 的父类中定义的方法。因此不管是类 B 仍是类 C 的实例变量调用了 method3,最后调用到的都是类 A 中定义的 method1 和 method2。
咱们用一个简单的程序来验证一下上面所描述的内容。这个程序自己并无太大的意义,仅仅 是用来测试方法调用的。
测试程序中有三个类 A、B、C。类 A 中定义了方法 method1 和 method2。类 B 中对 method1 进 行了重写,经过 self 调用了 method1,经过 super 调用了 method2。类 C 重写了 method1。
程序执行以后输出以下。能够看出,类 B 和类 C 的实例分别调用了不一样的方法。
实现接口声明中的方法时,可把具有独立功能的部分独立出来定义成子方法。通常状况下,这 些子方法都只供内部调用,不须要包含在类的接口中对外公开。
这种状况下,局部方法能够只在实现部分(一般是 .m 文件)中实现,而不须要在接口部分中进 行声明。这样一来,就算其余模块引用了接口文件,也没法得到这个方法的定义,没法调用这个方 法,从而就实现了局部方法。但这里只是说没法从接口中得到这个方法的定义,这个方法自己仍是存在的,只要发送了消息,就可以执行。
让咱们来看一个简单的例子,类 ClickVolume 是类 Volume 的一个子类,它的主要功能是当音量 发 生 变 化(提 高 或 降 低 )时 发 出 提 示 音。 提 高 或 降 低 音 量 时 发 出 提 示 音 使 用 一 个 共 同 的 方 法playClick, 定义以下所述。由于这个功能不会在其余地方使用到,因此咱们把它定义成一个局 部方法,不在接口文件中声明。
未在接口中声明的局部方法和没有进行属性声明的 C 语言函数同样,只能被定义在局部方法之 后的方法调用。在上面的例子中,playClick 就必须定义在up 和down 的前面。定义顺序方面出现的 问题,可使用第 10 章介绍的“范畴”(category)来解决。
编程的时候使用局部方法能够加强程序的可维护性,但在继承的时候可能会出现问题。例如, 子类新追加的方法可能并不知道父类已经实现了局部方法而去从新实现一个父类的局部方法。
为了不这一问题,苹果公司建议为局部方法名添加固定的前缀(详情请参考附录 C)。
前面已经介绍过了如何定义初始化方法,但还有一些要注意的地方。
根据需求有时可能须要为一个类定义多个不一样的初始化方法。例如,既须要提供一个可指定每 个参数初始值的初始化方法,又须要提供一个每一个参数都直接使用默认值的初始化方法;既须要提供 一个用内存变量进行初始化的初始化方法,又须要提供一个能从文件读入变量完成初始化的初始化 方法等。 指定初始化方法 (designated initializer)就是指能确保全部实例变量都能被初始化的方法, 这种方法是初始化的核心,类的非初始化方法会调用指定初始化方法完成初始化。一般,接收参数 最多的初始化方法就是指定初始化方法。
子类的指定初始化方法一般都是经过向 super 发送消息来调用超类的指定初始化方法。除此以外, 还有一些经过封装来调用指定初始化方法的方法叫做 非指定初始化方法 (secondary initializer)。图 3-8展现了指定初始化方法的概念,箭头指明了调用关系。图中每一个类都只有一个指定初始化方法,实 际上也能够存在多个。
子类的指定初始化方法,必须调用超类的指定初始化方法。如图 3-8 中所示,按照类层次从底向 上,各个类的指定初始化方法会被连锁调用,一直到最上层的 NSObject 的指定初始化方法——init 为止。
若是子类中想重写父类中的指定初始化方法,就必定要调用父类的指定初始化方法,而不能调 用父类的非指定初始化方法。缘由是非指定初始化方法内部会调用指定初始化方法,形成递归循环 调用,没法终止。
请看图 3-9 中的例子,类 A 的指定初始化方法是initWithMax :。init 是类 A 的非指定初始 化方法。类 B 是类 A 的子类,在 B 中重写了指定初始化方法initWithMax :。initWithMax :中 调用了父类类 A 的 init。如图所示,若是类 A 的 init 中经过 self 调用了initWithMax :,那么,当 初始化对象是类 B 的实例时,就又会调用到类 B 的initWithMax :,这样就变成了一个递归循环, 调用永远没法结束。
再让咱们回头看一下图 3-8,图 3-8 中类的非指定初始化方法都调用了指定初始化方法来进行初 始化,同时父类的非指定初始化方法也能够被继承,但定义的时候必定要注意,不然也会出现循环 调用的问题。
Objective-C 没有特殊的语法或关键字来代表哪一个方法是指定初始化方法,因此一般须要经过 文档或注释来标明指定初始化方法。Cocoa API 文档中的绝大多数类都标明了哪一个方法是指定初始 化方法。
另外,若是你想一块儿进阶,不妨添加一下交流群1012951431,选择加入一块儿交流,一块儿学习。期待你的加入!