类是所有面向对象编程语言中最重要的类型,它往往具有非常丰富的语法特征,比如继承、多态等等。在Swift编程语言中,类类型属于引用类型(reference type),并且也是除了协议外,唯一具有继承(inheritance)语法特性的类型。类类型与结构体类型在许多方面具有相似之处,比如在之前所提到的12大特性:
- 可定义存储式实例与类型属性;
- 可定义计算式实例与类型属性;
- 可使用属性观察者;
- 可定义实例与类型方法;
- 可定义初始化器;
- 可定义数组下标;
- 可对结构体进行扩展;
- 可遵循协议。
- 继承:允许一个类继承另一个类的特征。
- 类型投射:允许在运行时检查并解释一个类的对象实例的类型。
- 析构器(deinitializer):允许一个类的对象实例释放它所分配的任一资源。
- 引用计数:允许对一个类的对象实例有多个引用。
定义一个类时,我们使用关键字 class。
类的属性
类的属性与结构体的属性都一样,所以这里简单例举了一些例子来描述类中属性的定义以及使用。
import Foundation
/// 定义了一个类Test
class Test {
/// 定义了存储式实例属性a,
/// 其类型为Int
let a = 1
/// 定义了惰性存储式实例属性str,
/// 其类型为String,
/// 并用一个闭包调用对它初始化
lazy var str: String = {
// 这里的self指向的是访问此实例属性的对象实例
print("a = \(self.a)")
return "\(self)"
}() // 注意这里后面必须用(),否则就是对闭包的引用而不是调用了
/// 定义了计算式实例属性property
var property: String {
get {
guard let index = str.range(of: ".")?.lowerBound else {
return str
}
let startIndex = str.index(after: index)
// 这里返回字符串中出现 . 符号的后面的子串
return String(str[startIndex...])
}
set {
str = newValue
}
}
/// 定义了存储式实例属性observer,
/// 并对它使用了属性观察者
var observer = 1.0 {
willSet {
print("The new value is: \(newValue)")
}
didSet {
print("The original value is: \(oldValue)")
observer += oldValue
}
}
/// 定义了一个存储式类型属性s,
/// 并为它添加属性观察者
static var s = 100 {
willSet {
print("new s is: \(newValue)")
}
didSet {
print("original s = \(oldValue)")
}
}
/// 定义了一个计算式类型属性compute
static var compute: Int {
get {
return s
}
set {
s = newValue
}
}
}
// 用Test类创建一个对象实例test
var test = Test()
// 第一次访问惰性存储式实例属性str,
// 用闭包调用对它进行初始化,这里输出:
// a = 1
// str = SwiftFoundation.Test
print("str = \(test.str)")
// 输出:property = Test
print("property = \(test.property)")
test.property = "Hello"
// 输出:str = Hello
print("str = \(test.str)")
// 输出:property = Hello
print("property = \(test.property)")
上述代码又简单且详细地介绍了各类属性的使用以及效果。而上述属性的定义方式用在结构体中都没有问题。
类的方法
类的方法在大部分情况下也与结构体和枚举类型的一样。不过由于类具有继承特质,所以这会导致在某些方面,类的方法与结构体的方法会有些差异。
这里先列出与结构体和枚举类型相类似的类中的方法,后面有差异化的方法将在继承部分做详细描述。
/// 定义一个类Test
class Test {
/// 定义一个存储式实例属性
var a = 10
/// 定义一个实例方法method(a:)
func method(a: Int) {
// 在类的方法中可随意修改存储式实例变量属性,
// 而不需要使用mutating关键字来修饰该方法
self.a += a
}
/// 定义一个类型方法
static func typeMethod() {
print("This is \(self)")
}
/// 定义了一个下标
subscript(index: Int) -> Int {
return a + index
}
}
// 用类Test创建了一个实例对象test
var test = Test()
// 调用test的实例方法method(a:)
test.method(a: 10)
// 访问test的下标
// 输出:value is: 25
print("value is: \(test[5])")
// 调用Test的类型方法
// 输出:This is Test
Test.typeMethod()
从上述代码片段,我们可以看到类中的方法的定义方式以及使用方式与结构体和枚举的一样。不过类中的初始化器方法要比结构体和枚举的丰富得多,后面会做专门介绍。
类作为引用类型
结构体与枚举类型都是值类型,如果我们在一个函数中创建它们的对象实例,那么它们默认会被分配在当前函数的栈空间上,并且在概念上作为不同的对象实体。而类是引用类型,用类所声明的每一个对象其实都是引用形式,指向同一个对象实体。而用类所创建的对象实体则是被动态分配在堆存储空间上。因此类的对象具有引用计数,当指向某一对象实例的最后一个引用不指向它时,那么该对象实例则被自动释放。而结构体与枚举类型的对象实例由于默认被分配在栈上,所以它们本身就是被自动回收的。
下面举了一个简单的例子,说明了类的引用性质以及引用计数的工作特性。
/// 定义一个类Test
class Test {
/// 定义一个存储式实例属性
var a: Int
/// 这里实现了Test的初始化器方法
init(a: Int) {
print("a = \(a)")
self.a = a
}
/// 这里实现了Test类的析构器方法
deinit {
print("\(self) is destroyed!")
}
}
// 这里用Test类创建了一个对象实例,
// 但由于没有任何引用指向它。
// 所以创建了该对象实例之后,
// 它也就立马被销毁了。
// 这里输出:
// a = 10
// SwiftFoundation.Test is destroyed!
_ = Test(a: 10)
// 这里创建另一个Test的对象实例,
// 此时声明了test对象对它进行了一次引用,
// 所以这里只输出:
// a = 20
var test: Test! = Test(a: 20)
// 这里声明了 Test! 类型的对象tmp,
// 它指向了test对象实例
var tmp: Test! = test
// 我们通过tmp对象修改了实例属性a的值
tmp.a += 5
// 这里输出:a = 25
// 说明tmp确实与test指向同一个对象实例
print("a = \(test.a)")
// 我们先将test置空
// 这里什么也没发生
test = nil
// 我们这里输出一句话表示下列语句将会产生副作用
print("Test instance will be destroyed!")
// 执行了这条语句之后将会输出:
// SwiftFoundation.Test is destroyed!
// 由于tmp是当前对原本test所指向的Test对象实例的最后一个引用,
// 所以当它也被置空了之后,
// 该Test对象引用计数减为零,
// 调用其析构器方法
tmp = nil
上述代码简单而又详细地介绍了类类型的对象实例的引用特性。这里还涉及了类对象的引用计数机制,当原本指向某一个对象实例的引用指向了其他对象实例,或指向空,那么该对象的引用计数就会减1,减到零的时候就调用了它的析构器方法。
正由于类是引用类型,所以Swift新增了一对操作符 === 与 !== 用于判定同一个类的两个对象引用是否指向同一对象实例。尽管函数类型也是引用类型,但这对操作符却不能用于函数类型的对象引用。我们下面来看一个简单例子。
/// 定义一个类Test
class Test {
/// 定义一个存储式实例属性
let a: Int
/// 这里实现了Test的初始化器方法
init(a: Int) {
self.a = a
}
}
let t1 = Test(a: 1)
let t2 = Test(a: 2)
let test = t1
if t1 === test {
print("Identical!")
}
if t1 !== t2 {
print("Not identical!")
}
上述代码简单地介绍了两个同一类类型的对象引用是否指向同一对象。而Swift之所以不提供对函数引用做出是否相等的比较,是由于函数会被重度优化,使得一些具有相同代码的函数块会被整合在一起。具体可以参见此帖:http://stackoverflow.com/questions/24111984/how-do-you-test-functions-and-closures-for-equality
类作为引用类型对于用 let 所声明的对象引用与值类型的对象实例相比有很大不同之处。我们之前已经提到,对于值类型的常量而言,其成员都将作为常量而不允许被修改;而对于引用类型的常量而言,其成员可以被修改,而该常量引用不能指向其他对象实例。
下面我们再回过头来看看元组这一复合类型。在前面我们把元组单独拎出来没有将它归为值类型或引用类型中,因为元组类型本质上是定义了一组匿名对象实例,这些对象实例本身是值类型就是值类型,本身为引用类型就是引用类型。所以它是对一组命名对象进行简单封装的语法糖而已。下面我们来看一个简单实例就能明白。
class Test {
var member = 0
deinit {
print("Test destroyed!")
}
}
func foo() {
print("Over!")
}
// 分别定义四种类型的对象
var i = 10
let f = foo
var s = "Hello"
var t: Test! = Test()
// 下面我们通过一个元组将上面四个对象进行封装,
// 该元组的类型为:
// (Int, () -> Void, String, Test!)
// 这里要注意的是,
// 该元组的某个元素如果是值类型,
// 那么存放进去的时候用的就是值拷贝,
// 如果是引用类型,
// 那么存放的时候则是采用引用机制
var tuple = (i, f, s, t)
// 修改i的值,查看元组中i的值
i += 10
// 输出:tuple.0 = 10
print("tuple.0 = \(tuple.0)")
// 修改s的值
s = "Bye"
// 输出:tuple.2 = Hello
print("tuple.2 = \(tuple.2)")
// 修改对象t的存储式实例属性member的值
t.member = 10
// 输出:tuple.3 = 10
print("tuple.3 = \(tuple.3.member)")
// 将t置空,
// 此时还没触发Test对象的析构器方法
t = nil
print("Will destroy...")
// 对元组中的Test对象实例的引用置空,
// 此时触发析构器方法
// 这里就会输出:Test destroyed!
tuple.3 = nil
tuple.1()
通过上述代码示例我们就可以看到元组的本质了。它本身不具备“对象”特质,而是对一组对象在语法上的封装。换句话说,也就是我们本来要用不同对象名进行访问各自对象实例的形式转变为通过一个元组的标识符以索引访问或标签访问的形式进行访问了。
继承
在Swift编程语言中,一个类可以继承其他类。如果类B继承了类A,那么我们将类A称为类B的父类(superclass),把类B则称为类A的子类(subclass)。当类B继承了类A之后,类B则相当于继承了类A的所有属性及方法,这意味着子类具有父类所有的属性与方法,并且只要父类的属性与方法不是私有的,则可以直接访问。此外,同大部分面向对象的编程语言一样,Swift对于类类型的继承只能是单继承,也就是说,如果类B继承了类A,那么它就不能再继承其他类了,但它仍然可以再遵循其他多个协议。所以Swift所采用的也是单继承多遵循的机制。
对于传统的面向对象的编程语言来说,继承语法特性中还含有一个非常重要的概念——多态(polymorphism)。什么是多态呢?当一个子类B继承了其父类A之后,它可以重写(override)父类A中的计算属性以及方法,然后我们可以声明父类A的对象引用指向其子类B的对象实例。此时,当该对象引用调用了被子类B所重写的方法时,实际调用的是类B中被重写的方法,而不是父类A中原本的方法。当父类的某一计算属性或方法被重写之后,我们可以使用 super 关键字来显式调用父类所实现的计算属性或方法。下面我们举一个简单的例子来描述多态这一概念。
/// 定义一个类Father
class Father {
/// 定义了实例方法method
func method() {
print("This is Father!")
}
}
/// 定义一个类Son,
/// 它继承了类Father
class Son: Father {
/// 重写了父类Father的实例方法
override func method() {
print("This is Son!")
}
/// 定义了Son自己的实例方法foo
func foo() {
// 这里先调用父类的method实例方法
super.method()
//这里再调用Son自己的method实例方法
self.method()
}
}
// 各位注意,我们这里使用Father类型声明了对象引用ref,
// 并且将它指向Son的对象实例
let ref: Father = Son()
// 这里调用method实例方法
// 输出:This is Son!
ref.method()
// 这里直接创建一个临时的Son对象实例,
// 然后调用其foo实例方法,输出:
// This is Father!
// This is Son!
Son().foo()
上述代码片段简单明了地讲解了面向对象的多态特性,并且也介绍了如何在子类实例方法中调用父类所实现的实例方法。我们在稍后对方法继承的介绍中将会描述方法重写语法的详细细节。
如果我们想实现一个类,使得它不允许被其他类继承,那么我们可以在该类的定义前添加 final 关键字,如果对一个用 final 所修饰的类进行继承,那么编译器就会直接报错。当然,我们也可以用 final 关键字去修饰一个类中的方法,使得它不允许被其子类重写。下面我们来举个例子进行说明。
/// 定义一个类LastLeaf,
/// 它被final修饰,
/// 其他类不能继承该类
final class LastLeaf {
/// 定义了实例方法method
func method() {
print("This is Father!")
}
}
/// 定义了一个类Father
class Father {
/// 定义了实例方法method,
/// 它被final修饰,
/// 意味着其子类将不能重写该方法
final func method() {
print("This is a final method!")
}
}
// 这里出现编译错误:
// 继承自一个final类LastLeaf
class Root: LastLeaf {
}
/// 定义了一个类Son,
/// 继承自类Father
class Son: Father {
// 试图对父类的method方法进行重写,
// 出现编译报错:
// 实例方法重写了一个'final'实例方法
override func method() {
}
}
后续我们将详细描述类继承机制中分别对属性、方法、下标继承的语法特性。随后,再详细介绍类中的初始化器及析构器。
对属性的继承
当一个子类继承了某一父类之后,该子类将会继承该父类的所有属性,包括存储式属性、计算式属性以及针对存储式属性的观察者。这意味着子类可以直接访问父类的所有属性,只要它们具有足够的访问权限。此外,在Swift编程语言中,当子类继承了父类的存储式属性之后,在子类中就不能再定义与父类中同名的存储式属性了;但子类可以重写父类的计算式属性以及针对存储式属性的属性观察者。如果要重写计算式属性以及属性观察者,那么在子类中需要在定义这些属性的时候使用 override 关键字。如果我们在父类中不想让子类重写自己的某一计算式属性或属性观察者,则可以使用 final 关键字来修饰。
而对于计算式类型属性,父类可以使用 class 关键字进行声明,表示允许子类重写该计算式类型属性。如果使用了 static 关键字进行声明,那么其子类将不能重写该计算式类型属性。此外,Swift不支持针对父类存储式类型属性的属性观察者进行重写,由于存储式类型属性只能用 static 进行声明,而不能用 class。如果子类B重写了其父类A的某个计算式类型属性,但子类B又不想让它的子类对该属性进行重写,那么我们也可以在子类B中将该重写的类型属性声明为 final。
这里要详细说明的是对属性观察者的重写。当子类B重写了父类A的某一存储式实例属性的属性观察者之后,我们用类B的对象实例修改该属性时,先调用子类B的 willSet 方法,再调用父类A的 willSet 方法,随后调用父类A的 didSet 方法,最后调用子类B的 didSet 方法。即便在类B中使用 super 来访问该属性,效果也是如此。而对于计算式属性的重写,如果用子类B的对象实例去访问它,那么只会调用子类B的getter或setter方法。当然,如果用 super 来访问此计算式属性,那么也会调用父类A的getter或setter方法。
下面我们举一些例子进行详细说明。
/// 定义一个类Father,
/// 它将作为父类
class Father {
/// 定义一个存储式实例属性a,
/// 它是Int类型,并初始化为0
var a = 0
/// 定义一个计算式实例属性b
var b: Int {
get {
return a
}
set {
a = newValue
}
}
/// 定义了一个存储式实例属性c,
/// 并为它提供了属性观察者
var c = 0 {
willSet {
print("c new value = \(newValue)")
}
didSet {
print("c original value = \(oldValue)")
}
}
/// 定义了一个计算式实例属性f,
/// 并且不允许子类对它进行重写
final var f: String {
return "final"
}
/// 定义了一个存储式类型属性ss
static var ss = 10
/// 定义了一个计算式类型属性sc
class var sc: Int {
get {
return ss
}
set {
ss = newValue
}
}
}
/// 定义了类Child,
/// 并继承了Father类
class Child: Father {
/// 定义了存储式实例属性aa,
/// 注意,这里不能再用a标识符定义存储式实例属性,
/// 否则会引发命名冲突
var aa: Int = 1
/// 重写了父类Father的计算式实例属性b
override var b: Int {
get {
// 这里的 self. 与 super. 均可省,
// 此外后面的 super. 也可用 self. 代替
return self.aa + super.a
}
set {
aa = newValue
}
}
/// 重写了父类Father的
/// 针对存储式实例属性c的属性观察者。
/// 注意,这里不允许对c再做初始化
override var c: Int {
willSet {
print("Child c set: \(newValue)")
}
didSet {
print("Child c org: \(oldValue)")
}
}
/// 重写了父类Father的计算式类型属性sc
/// 并且其子类将不能重写此计算式类型属性
override final class var sc: Int {
get {
// 这里的 super. 可省,
// 并且我们也可以用 self. 来访问ss类型属性
return super.ss + 10
}
set {
ss -= newValue
}
}
/// 定义了Child类的method实例方法,
/// 用于测试计算式实例属性的重写特征
func method() {
// 这里先访问父类的计算式实例属性
super.b += 10
// 输出a的结果
print("a = \(a)")
// 这里访问Child类重写的计算式实例属性
self.b += 5
// 再看看a的值
print("a = \(a)")
// 然后观察aa的值
print("aa = \(aa)")
}
}
// 这里用Father类型声明ref,
// 并且将它指向Child类的对象实例
var ref: Father = Child()
// 这里访问的是Child类中的计算式实例属性b
ref.b += 5
// 这里将触发的是Child类中实现的属性观察者
// 这里输出:
// Child c set: 10(先调用Child类的willSet)
// c new value = 10(再调用Father类的willSet)
// c original value = 0(随后调用Father类的didSet)
// Child c org: 0(最后调用Child类的didSet)
ref.c = 10
// 直接通过Child类的对象实例调用其method方法
// 这里输出:
// a = 10
// a = 10
// aa = 16
// 说明访问子类Child自身的计算式实例属性时
// 没有访问父类相关的计算式实例属性
Child().method()
// 这里用Father类的类型引用指向了Child类的类型
var typeRef: Father.Type = Child.self
// 这里访问的是Child类中的计算式类型属性sc
typeRef.sc += 5
// 我们发现,以下两条打印语句输出结果相同,
// 说明Father类与Child类共享存储式类型属性ss
print("ss = \(Father.ss)")
print("ss = \(Child.ss)")
// 输出:a = 0
// 由于上述语句中没有对ref所引用的
// Child对象实例的存储式实例属性a进行修改
print("a = \(ref.a)")
上述代码详细描述了类的属性继承的特点,包括了对计算式属性以及属性观察者的重写。 这里,我们已经可以看到计算式属性的多态特性。而属性观察者的机制则比较特别,它采用的是直接继承的方式做触发的,以形成一条响应链。上述代码涉及到的类的元类型将在下面做详细描述。我们这里所要理解的是,子类继承了父类之后就会继承它所有的属性,包括所有的类型属性。不过由于实例属性是属于对象本身的,而类型属性是属于类型的,所以这两者之间存在一定的差异性。我们在上述代码中可以看到,父类Father与其子类Child共享了存储式类型属性ss。
我们用以下两个图来分别描述上述代码中Father类与Child类的对象模型以及类型模式。
image.png
上图中,对象模型的两个图用于分别表示Father类的对象实例与Child类的对象实例的内部结构。其中,对于Child对象实例而言,它含有父类Father域的三个存储式实例属性,然后还有自己的存储式实例属性aa。而对于类型模型而言,我们可以看到Father类与其子类Child是共享同一存储空间的,它们共享了存储式类型属性ss。
对方法的继承
当子类B继承了其父类A之后,子类B将可访问所有父类A的方法。如果子类B要重写父类A的方法,那么需要使用对此方法用 override 关键字进行声明,并且该方法的返回类型、实参标签以及每个形参的类型都必须与父类的一样,否则编译将会报错。由于Swift是一个注重代码编写安全性的编程语言,所以当我们要做方法重写的时候不会因为某些疏忽而导致变成了方法重载,因为我们在重写父类的方法时必须使用 override 关键字,而如果要使用方法重载则不能使用 override。所以这就显式地将方法重写与方法重载给区分开了,以避免我们有些笔误。下面我们来看一些具体的例子。
/// 定义类Father
class Father {
/// 定义了一个实例方法method
func method() {
print("This is a method.")
}
/// 定义了实例方法method(a:)
func method(a: Int) {
print("Int a = \(a)")
}
/// 定义了类型方法foo
class func foo() {
print("This is foo.")
}
/// 定义了类型方法foo(a:),
/// 由于它用的是static,
/// 所以子类不能重写此方法
static func foo(a: Int) {
print("foo a = \(a)")
}
}
/// 定义了类Child,继承了Father类
class Child: Father {
/// 重写了Father的method实例方法
override func method() {
print("This is a Child method!")
}
/// 重写了Father的method(a:)实例方法
override func method(a: Int) {
print("Child Int a = \(a)")
}
/// 这里对method(a:)进行方法重载,
/// 注意,这里不能添加override
func method(a: Double) {
// 这里先调用父类的method(a:)方法
super.method(a: Int(a))
print("Double a = \(a)")
}
/// 这里重写了父类的foo类型方法,
/// 并且使得Child的foo类型方法不允许被它的子类重写
override final class func foo() {
print("Child foo")
}
}
// 这里声明了Father类型的对象引用ref,
// 并且用Child对象实例为它初始化
let ref: Father = Child()
// 这里调用的是Child的实例方法method
ref.method()
// 这里调用的是Child的实例方法method
ref.method(a: 100)
// 这里直接通过Child的对象实例来调用
// 它自己所重载的method(a:)实例方法
Child().method(a: 1.5)
// 这里声明了Father的类型引用,
// 指向了Child的元类型
let typeRef: Father.Type = Child.self
// 这里调用的是Child的类型方法foo
typeRef.foo()
// 这里调用的是Father的类型方法foo(a:)
typeRef.foo(a: 5)
上述代码简单明了地呈现了方法重写与方法重载的区别以及不同的效果。这里对方法重写还有一点需要各位特别注意的,当父类A的一个实例方法M调用了另一个实例方法N,而N被其子类B重写,那么当我们用子类对象实例调用方法M的时候,M中所调用的N不是父类A的N,而是子类B所实现的N。下面我们举个例子来阐明这种多态特性。
/// 定义类Father
class Father {
/// 定义了一个实例方法main
func main() {
// 在这里调用实例方法method(a:)
method(a: 50)
}
/// 定义了实例方法method(a:)
func method(a: Int) {
print("Int a = \(a)")
}
}
/// 定义了类Child,继承了Father类
class Child: Father {
/// 重写了Father的method(a:)实例方法
override func method(a: Int) {
print("Child Int a = \(a)")
}
}
// 这里声明了Father类型的对象引用ref,
// 并且用Child对象实例为它初始化
let ref: Father = Child()
// 这里调用了main实例方法,
// 输出:Child Int a = 50
// 说明这里的main方法中所调用的method(a:)
// 是Child所实现的实例方法
ref.main()
// 这里直接通过Father的对象实例去调用main方法,
// 输出:Int a = 50
// 说明这里的main方法中所调用的method(a:)
// 是Father所实现的实例方法
Father().main()
上述代码进一步描述了面向对象编程的多态特性。其实这也非常容易理解:由于main函数中在调用method(a:)方法时,我们缺省了 self. 。我们没用 self. 并不是说它就没有了,而是编译器会自动加上。因为我们在一个实例方法中调用任何实例属性或实例方法时,其实都是用的 self 去访问、调用的,而 self 则是指向当前调用该实例方法的对象实例。所以我们看的时候注意这里所访问实例属性、实例方法是否被子类重写过,如果重写过,那么当用该子类对象实例去调用当前方法时,那么这些被重写的属性与方法用的就是该子类所实现的版本。当然,对于类型方法也同样如此,这里不再赘述。
对下标的继承
之前已经比较详细地讨论了下标的语法特性。在类类型中,下标也能继承并且可重写,由于下标语法本身就类似于实例方法,而其特质则更像计算式实例属性。下面我们举一个简单的例子来看看下标的继承及重写。
/// 定义类Father
class Father {
/// 定义存储式实例属性member
var member = 1
/// 定义下标,参数为Int类型
subscript(index: Int) -> Int {
return index + member
}
/// 定义下标,参数为String类型
subscript(str: String) -> Int {
return str.count + member
}
}
/// 定义了类Child,继承了Father类
class Child: Father {
/// 重写参数类型为Int的下标
override subscript(index: Int) -> Int {
return index - member
}
/// 重写参数类型为String的下标,
/// 并且使得Child的子类将不能重写此下标
override final subscript(str: String) -> Int {
return str.count - member
}
}
// 这里声明了Father类型的对象引用ref,
// 并且用Child对象实例为它初始化
let ref: Father = Child()
// 这里访问的是Child类的下标实现,
// value的值为9
var value = ref[10]
print("value = \(value)")
// 这里访问的是Child类的下标实现,
// value的值为2
value = ref["abc"]
print("value = \(value)")
上述代码介绍了下标的继承及重写,其语法特征还是比较简单直接的。
类的初始化器方法
类的初始化器与结构体的初始化器方法在语法上几乎一样,不过类的初始化器没有默认的逐成员的形式。这就意味着,如果在类中存在声明了一个未被直接初始化的存储式实例属性,那么我们必须显式添加上初始化器方法为它初始化。当然,对于类类型来说,Swift也为它提供了缺省的初始化器方法,如果类中的所有存储式实例属性在定义的同时也直接被初始化了,那么我们就可以不显式提供初始化器方法,直接在创建对象实例时用默认的不带任何参数的初始化器方法。之前几小节的例子用的都是缺省的初始化器方法的方式创建对象实例的。
由于类是一个引用类型,并且具有继承特性,所以关于类的初始化器方法比结构体和枚举类型的复杂一些。就初始化代理的语法形式上来看也是不太一样的。本小节我们将先详细介绍指定的初始化器(designated initializer)以及便利初始化器(convenience initializer),下一小节我们将重点介绍对初始化器方法的继承与重写。
在类类型中,指定的初始化器方法是对于一个类而言是最基本的初始化器形式,它应该对当前类中的所有存储式实例属性全都完成初始化,随后如果当前类有父类的话,还需要调用父类的初始化器方法,使得将此初始化做向上传递,形成一条不断向父类传递的初始化链。比如,如果类C继承了类B,类B继承了类A,那么在类C中的初始化器方法要调用类B的初始化器方法,而类B的初始化器方法中又要调用类A的初始化器方法,以此形成了一条C -> B -> A的初始化链。
在类类型中,必须至少有一个指定的初始化器。另外与结构体不同的是,我们不能在一个指定的初始化器中直接调用另一个指定的初始化器,如果我们想在某个初始化器中调用当前类的某一指定的初始化器,那么我们必须将此初始化器声明为便利的,通过 convenience 关键字。类的便利初始化器方法中既可以直接调用一个指定的初始化器方法,也可以选择调用其他便利初始化器方法。不过这里需要注意的是,一个便利初始化器方法只能调用本类的其他初始化器方法,而不能调用父类的。此外,即便一个便利初始化器方法调用了另一个便利初始化器方法,但最终必须有一个便利初始化器方法调用了指定的初始化器方法。当然,在某一便利初始化器方法中只能选择调用一个其他的初始化器方法,不能调用多个。
如果当前类定义了一个存储式实例属性常量(即用 let 声明),那么该属性常量只能在指定的初始化器中进行初始化,而不能在便利初始化器中进行。因为从性质上来说,真正的初始化器其实是指定的初始化器方法,而便利初始化器可作为二次初始化使用,通常用于对已初始化好的属性做进一步处理,或是用于参数值的过滤等。所以在便利初始化器方法中,如果没有调用过指定的初始化器方法,那么不能对尚未被初始化的存储式实例属性进行访问。下面举一个简单的例子来说明类的指定的初始化器以及便利初始化器。
/// 定义类A
class A {
/// 定义了一个存储式实例属性a,
/// 并且不对它直接初始化
var a: Int
/// 定义了不带任何参数的指定的初始化器方法
init() {
// 这里将属性a初始化为0
a = 0
}
/// 定义了带有一个参数的指定的初始化器
init(a: Int) {
// 这里对属性a初始化为形参所指定的值
self.a = a
// 在此初始化器方法中不能调用其他初始化器方法
}
/// 定义了带有一个参数的便利初始化器
convenience init(b: Int) {
// 注意!在调用了指定的初始化器方法之前不能对未初始化的实例属性进行访问
// 所以,这里如果先用 self.a = b 就会出现编译报错
// 这里直接调用带有一个参数的指定的初始化器
// init(a:)
self.init(a: b)
// 输出b的值,
// 这里访问实例属性a没有问题
print("b = \(self.a)")
}
/// 定义了带有一个参数的便利初始化器
convenience init(c: Int) {
// 先输出c的值
print("c = \(c)")
// 然后调用便利初始化器init(b:)
self.init(b: c)
}
}
// 通过不带参数的指定的初始化器来创建类A的对象实例
var a = A()
print("a1 = \(a.a)")
// 通过init(a:)指定的初始化器方法来创建类A的对象实例
a = A(a: 10)
print("a2 = \(a.a)")
// 通过init(b:)便利初始化器方法来创建类A的对象实例
a = A(b: 20)
print("a3 = \(a.a)")
// 通过init(c:)便利初始化器方法来创建类A的对象实例
a = A(c: 30)
print("a4 = \(a.a)")
上述代码清晰简洁地描绘了指定的初始化器与便利初始化器的使用与区别。
初始化器方法的继承与重写
假定一个子类B继承了其父类A,如果子类B没有定义属于自己的存储式实例属性,或者子类B定义了属于自己的存储式实例属性,但同时在定义时直接做了初始化了,那么子类可直接使用继承自父类的初始化器方法。顺便提一下,Swift中我们不能用 final 去修饰一个初始化器方法。我们下面先来看一个简单的例子。
/// 定义了类Father
class Father {
/// 定义了存储式实例属性a
let a: Int
/// 定义了不带参数的指定的初始化器
init() {
// 这里直接将a初始化为0
a = 0
}
/// 定义了带有一个参数的指定的初始化器
/// init(a:)
init(a: Int) {
// 这里将形参a的值赋值给属性a
self.a = a
}
}
/// 定义了类Child
class Child: Father {
/// 定义了Child自己的存储式实例属性ch,
/// 同时为它进行了初始化
let ch = 100
}
// 这里通过继承自Father的不带参数的指定的初始化器来创建Child的对象实例
var ref: Father = Child()
// 这里通过继承自Father的指定的初始化器init(a:)来创建Child的对象实例
ref = Child(a: 10)
如果子类含有自己的存储式实例属性并且需要使用初始化器方法进行初始化,或者有其他需要自己做特殊处理的初始化,那么需要自己定义初始化器方法。子类可以自己定义与父类不一样的指定的初始化器方法,也可以重写父类已有的指定的初始化器方法。当然,与一般方法重写一样,重写初始化器方法时也需要在前面添加 override 关键字,并且必须在此指定的初始化器方法中调用某个父类的指定的初始化器方法,但不能调用父类的便利初始化器方法。我们这里还必须注意的是,一旦我们在子类中实现了自己的初始化器,那么我们在创建子类的对象实例时就不能使用任何父类所提供的初始化器方法了,必须显式用子类所提供的初始化器方法进行创建。另外还要注意一点的就是上一节所提到的,子类的便利初始化器方法中只能调用自己的其他初始化器方法,而不能直接调用父类的初始化器方法。此外,便利初始化器不允许被重写。
下面我们来举一些例子描述初始化器方法的重写。
/// 定义了类Father
class Father {
/// 定义了存储式实例属性a
let a: Int
/// 定义了不带参数的指定的初始化器
init() {
// 这里直接将a初始化为0
a = 0
}
/// 定义了带有一个参数的指定的初始化器
/// init(a:)
init(a: Int) {
// 这里将形参a的值赋值给属性a
self.a = a
}
/// 定义了一个便利初始化器方法init(str:)
convenience init(str: String) {
self.init(a: str.count)
print("str = \(str)")
}
}
/// 定义了类Child
class Child: Father {
/// 定义了Child自己的存储式实例属性ch,
/// 并且尚未对它进行初始化
let ch: Int
/// 重写了父类Father的init指定的初始化器方法
override init() {
// 我们这里先对Child类的ch存储式实例属性进行初始化
ch = 0
// 然后再调用父类Father的init方法
super.init()
}
/// 重写了父类Father的init(a:)指定的初始化器方法
override init(a: Int) {
// 这里先对Child自己的ch属性进行初始化
ch = a
// 然后调用父类的init(a:)指定的初始化器方法
// 注意,这里不能使用self.a
// 因为父类的初始化尚未完成
super.init(a: a + ch)
}
/// 这里定义了Child自己的指定的初始化器方法init(b:)
init(b: Int) {
// 先为Child自己的实例属性ch做初始化
ch = b
print("b = \(b)")
// 然后这里必须调用父类Father的某一指定的初始化器方法
super.init(a: b)
}
/// 这里重载了父类的init(a:)指定的初始化方法,
/// 由于是方法重载,
/// 所以这里不能添加override关键字
init(a: Double) {
print("double value = \(a)")
// 这里先对Child的实例属性ch做初始化
ch = Int(a)
// 然后这里调用了父类Father的init(a:)指定的初始化方法
super.init(a: ch + 1)
}
/// 定义Child自己的便利初始化器方法init(str:)
/// 由于便利初始化器不能被重写,
/// 所以这里不能添加override
convenience init(str: String) {
print("Child string = \(str)")
if let value = Double(str) {
// 如果str能转为Double类型的值,
// 则调用Child自己的init(a:)指定的初始化器方法
self.init(a: value)
}
else {
// 否则调用Child所重写的不带任何参数的指定的初始化器方法
self.init()
}
}
}
// 这里通过继承自Father的不带参数的指定的初始化器来创建Child的对象实例
var ref: Father = Child()
print("a = \(ref.a)")
// 这里通过重写了父类的init(a:)指定的初始化器方法来创建Child对象实例
ref = Child(a: 10)
print("a = \(ref.a)")
// 这里通过Child重载的init(a:)指定的初始化器方法来创建Child对象实例
ref = Child(a: 1.5)
print("a = \(ref.a)")
// 这里通过Child自己的init(b:)指定的初始化器方法来创建Child对象实例
ref = Child(b: 30)
print("a = \(ref.a)")
// 这里通过Child自己的便利初始化器方法init(str:)来创建Child对象实例
ref = Child(str: "12.3")
print("a = \(ref.a)")
上述代码详细介绍了初始化器方法继承与重写的使用方式,提到了若干需要注意的约束。同时也演示了子类重载父类的初始化器方法的用法。这里值得注意的是,子类的指定的初始化器方法的执行逻辑是:先初始化子类自己的存储式实例属性,再初始化其父类的。也就是说,在概念上,我们应该将对父类指定的初始化器方法的调用放到最后调用,即便我们将它提前,但效果上仍然如此。因为Swift是一门追求安全性的编程语言,为了简化编译器对父类指定的初始化器方法的追踪,Swift在语法上就禁止我们直接在子类的指定的初始化器方法中直接访问父类的尚未被初始化的存储式实例属性,即便我们先调用了父类的指定的初始化器方法。下面我们举个例子进行说明。
/// 定义了类Father
class Father {
/// 定义了存储式实例属性a
let a: Int
/// 定义了不带参数的指定的初始化器
init() {
// 这里直接将a初始化为0
a = 0
}
/// 定义了带有一个参数的指定的初始化器
/// init(a:)
init(a: Int) {
// 这里将形参a的值赋值给属性a
self.a = a
}
}
/// 定义了类Child
class Child: Father {
/// 定义了Child自己的存储式实例属性ch,
/// 并且尚未对它进行初始化
let ch: Int
/// 重写了父类Father的init指定的初始化器方法
override init() {
// 我们这里先对Child类的ch存储式实例属性进行初始化
ch = 0
// 然后再调用父类Father的init方法
super.init()
}
/// 重写了父类Father的init(a:)指定的初始化器方法
override init(a: Int) {
// 这里会先报一个编译错误:
// 属性'self.ch'在super.init的调用处
// 没有被初始化
super.init(a: a + 1)
// 这里还会报一个编译错误:
// 'self.ch'只能被初始化一次
ch = self.a + 1
}
/// 定义Child自己的便利初始化器方法init(str:)
convenience init(b: Int) {
// 这里先调用Child自己的指定的初始化器方法init(a:)
self.init(a: b)
// 然后在便利初始化器方法中
// 可直接访问父类的实例属性a和自己的实例属性ch
print("value = \(super.a + self.ch)")
}
}
所以,我们在实现子类的指定的初始化器方法时应当先对自己的存储式实例属性做初始化,最后调用父类的指定的初始化器方法。这样,我们就能清晰地观察到在当前子类的指定的初始化器方法中不能直接访问父类的尚未被初始化的存储式实例属性了。这对于之前熟悉C++或Java的朋友需要注意,在C++和Java中,我们一般都是先调用父类的构造方法,然后再调用子类自己的。但Swift则完全不同。
必须实现的初始化器方法
Swift编程语言引入了必须实现的初始化器方法(required initializers)。一旦父类的某个指定的初始化器方法前加上了 required 关键字,那么子类必须重写该指定的初始化器方法,在重写该初始化器方法时,无需使用 override 关键字,而直接使用 required 关键字,因为 required 已经包含了 override 的语义。下面举一个简单的例子进行说明。
/// 定义了类Father
class Father {
/// 定义了存储式实例属性a
let a: Int
/// 定义了不带参数的指定的初始化器
required init() {
// 这里直接将a初始化为0
a = 0
}
/// 定义了带有一个参数的指定的初始化器
/// init(a:)
required init(a: Int) {
// 这里将形参a的值赋值给属性a
self.a = a
}
/// 重载了init(a:)指定的初始化器方法
/// 并且不用required修饰
init(a: String) {
self.a = a.count
}
}
/// 定义了类Child
class Child: Father {
/// 定义了Child自己的存储式实例属性ch,
/// 并且尚未对它进行初始化
let ch: Int
/// 重写了父类Father的init指定的初始化器方法
required init() {
// 我们这里先对Child类的ch存储式实例属性进行初始化
ch = 0
// 然后再调用父类Father的init方法
super.init()
}
/// 重写了父类Father的必须的init(a:)指定的初始化器方法
required init(a: Int) {
ch = a * a
super.init(a: ch)
}
// 重写了父类father的一般的init(a:)指定的初始化器方法
override init(a: String) {
ch = a.count - 1
super.init(a: a)
}
}
// 这里调用Child重写的required的init初始化器方法创建对象实例
var ref: Father = Child()
// 这里调用的是Child重写的required的init(a:)初始化器方法创建对象实例
ref = Child(a: 10)
// 这里调用的是Child重写的一般的init(a:)指定的初始化器方法创建对象实例
ref = Child(a: "abc")
上述代码简单明了地解释了必须实现的初始化器方法的使用。
类的析构器方法
在Swift编程语言中,析构器方法是类类型独有的。析构器方法比初始化器方法要简单很多,它形式单一,不接受任何参数,而且也没有返回值。我们不能显式通过对象实例来调用析构器方法,而只能通过Swift运行时的引用计数管理自动去调用。
我们之前已经了解了,类类型属于引用类型。也就是说,用类类型声明的对象其实都是指向该类对象实例的一个引用,每当一个新的引用指向该对象实例时,其引用计数就会递增,而当指向该对象实例的引用指向其他对象或是被函数返回给销毁时,那么该对象实例的引用计数递减。一旦对象实例的引用计数减为零时,则会触发对其析构器方法的调用。
如果一个类不显式提供析构器方法,那么Swift会为他自动添加一个默认缺省的析构器方法,像之前的例子均为如此。而与初始化器方法不同的是,如果父类显式给出了自己的析构器方法,子类仍然可以不实现自己的析构器方法。同时,即便子类实现了自己的析构器方法,也不需要(也不能)直接调用父类的析构器方法,这一切都是Swift编译器和运行时自行处理调用的。
/// 定义了类Father
class Father {
/// 这里实现了Father的析构器方法
deinit {
print("Father is destroyed")
}
}
/// 定义了类Child
class Child: Father {
/// 这里实现了Child自己的析构器方法
deinit {
print("Child is destroyed")
}
}
_ = {
// 在闭包中声明了一个 Father? 类型的对象引用,
// 然后用Child对象实例为它初始化。
// 此时该对象实例的引用计数为1
var a: Father? = Child()
// 这里声明了 Father? 类型的对象引用b,
// 指向了对象引用a,
// 此时该Child对象实例的引用计数增加到2
var b = a
// 将a置空,它所引用的Child对象实例的引用计数减1
a = nil
print("Will destroy...")
// 将b置空,它所引用的Child对象实例的引用计数减1,
// 此时减到了0,触发Child对象的析构器方法
// 这里输出:
// Child is destroyed
// Father is destroyed
b = nil
print("Another test")
// 这里声明另一个对象引用ref,
// 指向了另一个Child的对象实例
var ref = Child()
print("Changed")
// 我们将此ref引用指向了另一个Child的对象实例,
// 此时它刚所引用的对象实例引用计数减1,减到了0,
// 触发其析构器方法
ref = Child()
print("Over!")
}()
// 在此闭包调用结束后由于最后的ref没有被其他对象所引用,
// 因此它被回收之后触发了它之前所引用对象的引用计数减1,
// 到0之后调用了第三个Child对象实例的析构器方法
上述代码非常简单,通过从引用计数以及函数回收临时对象的角度呈现了对象实例如何被销毁的实例。上述代码特意穿插了一些打印语句,使得我们能清晰看到对象实例被销毁的时序。此外,我们还能观察到析构器方法的调用次序,即先调用当前类的析构器方法,再调用其父类的析构器方法。
类类型与协议类型的组合
在前面我们讲到了协议的组合,通过协议的组合我们有时可以不必将多个协议进行继承而得到一个新增的协议类型,而是可以直接用组合的方式来指明当前对象类型遵循哪些协议。不过有这个语法特性还不够完美,在Swift 4.0开始起,Swift编程语言增加了能够将类类型与协议类型进行组合的类型。熟悉Objective-C的同学知道,Objective-C就能将一个类类型与多个协议类型进行组合,从而可用来声明一个代理对象,比如:UIViewController<UITableViewDataSource, UITableViewDelegate> *delegate。这对于代理对象类型而言非常有用,这可以使得原本在类型上就比较严格的Swift用起来更为灵活方便。下面我们举一个简单的例子:
protocol MyProtA {
func foo()
}
protocol MyProtB {
func method()
}
class Father {
func father() { }
}
class Son: Father, MyProtA, MyProtB {
func foo() { }
func method() { }
}
// 这里将对象obj声明为同时具备Father类类型,
// 并遵循MyProtA协议与MyProtB协议
let obj: Father & MyProtA & MyProtB = Son()
// obj对象能同时访问到这三种类型中的方法与属性
obj.father()
obj.foo()
obj.method()
各位可以看到,上述代码简单扼要地描述了类类型与协议进行组合的样例。我们不需要新增一种协议类型将MyProtA与MyProtB进行组合起来,而且也不要求Father类去遵循这些协议。我们仅仅对所要使用的对象obj声明其类型,将它要满足的类型全都声明上即可。不过由于Swift编程语言属于单继承、多实现的编程语言,因此对对象的声明后面只能有一个类类型,然后可以跟多个协议类型。此外,结构体与枚举类型不能与协议进行组合,因为它们本身不具备继承特性,而且它们都是非常具体的值类型。
类遵循协议时的更多特性
我们之前在已经详细描述了协议及其相关语法特性。当协议用于类类型的时候还会有其他一些特性。这里主要列出4个:
1、一个协议如果既可用于结构体与枚举类型,也能用于类类型,那么当在里面声明了一个 mutating 实例方法时,一个类遵循该协议之后,对此 mutating 方法的实现不需要加 mutating 关键字。因为类的实例方法没有 mutating 这个概念,它本身就是引用类型,所以也没有所谓的“写时拷贝”机制。
2、一个协议可以通过添加 class 特性将它作为仅对类类型有效的协议类型。如果一个协议用了 class 限定,并且需要继承其他协议,那么必须将 class 写在最前面,然后后面再跟其他协议名。此外,在被 class 所限定的协议中不可直接声明 mutating 的实例方法。如果一个被 class 所限定的协议继承了某个声明了 mutating 实例方法的协议,此时不会产生语法错误。但是,当一个类继承该 class 协议并实现了那个实例方法之后,我们去调用那个实例方法会引发运行时异常。
3、协议中如果要声明类型方法,则只能使用 static 关键字进行声明,而不能使用 class 关键字,无论它是否被 class 限定,不过类在遵循该协议并实现该类型方法时,仍然可以使用 class 关键字表示其子类可以重写该类型方法。
4、协议中如果声明了初始化器方法,那么对于遵循该协议的类类型而言则必须显式实现该初始化器方法,并且同时要用 required 进行修饰,除非该类已被final 修饰从而无法再被继承。而对于结构体和枚举类型而言,像不带任何参数的初始化器方法,在这些值类型中则可以不给出显式实现。
下面我们就举些例子进行说明。
/// 这里定义了一个协议Prot
protocol Prot {
/// 声明了一个mutating实例方法method
mutating func method()
/// 声明了一个初始化器方法init(a:)
init(a: Int)
}
/// 这里定义了一个协议MyProt,
/// 它作为一个只作用于类类型的协议,
/// 并且继承了Prot协议
protocol MyProt: class, Prot {
/// 这里声明了一个类型方法
static func typeMethod()
}
/// 定义了类Test
class Test: MyProt {
/// 这里实现了Prot协议的初始化器方法,
/// 并且将它作为必须实现的初始化器方法。
/// 这里需要注意的是,
/// 当类实现了协议中的初始化器方法时,
/// 该初始化器方法必须用required修饰
required init(a: Int) {
print("a = \(a)")
}
/// 这里实现了Prot协议的mutating实例方法method,
/// 这里的mutating不能添加
func method() {
print("This is a method!")
}
/// 这里实现了MyProt协议的类型方法typeMethod,
/// 这里可以使用class修饰,
/// 表示其子类可以重写此类型方法
class func typeMethod() {
print("This is a type method!")
}
}
// 这里声明了MyProt协议类型的对象prot,
// 并用Test对象实例对它进行初始化
var prot: MyProt = Test(a: 10)
// 调用Test的method实例方法,
// 这里会引发运行时异常
prot.method()
// 调用Test的typeMethod类型方法
type(of: prot).typeMethod()
上述代码演示了类遵循协议时的一些需要注意的地方。我们这里可以看到,当执行 prot.method() 这条语句之后,就引发了程序异常崩溃,而当我们将所定义的 MyProt 后面的“class,”去掉就能正常运行了。所以一般来说我们需要慎用仅针对类类型的协议。而上述代码中所出现的 type(of:) 函数,我们在后面详细描述。
网友评论