类与结构体相似,但存在如下一些主要差别:
引用类型
类是引用类型。这意味着类实例有两个结构体或枚举实例所不具备的关键特性。
可变性
类实例是可变的。虽然对类实例的引用是个常量(let),不过你可以通过该引用修改实例属性的值。类的实例方法绝不能标记为mutating。
多引用
如果给定的类实例被赋予多个变量或作为参数传递给函数,那么你就拥有了对相同对象的多个引用。
继承
类可以拥有子类。如果一个类有父类,那么它就是这个父类的子类。这样,类就可以构成一种树形结构了。
在Objective-C中,类是唯一一种对象类型。一些内建的Swift结构体类型会桥接到Objective-C的类类型,不过自定义的结构体类型却做不到这一点。因此,在使用Swift进行iOS编程时,使用类而非结构体的一个主要原因就是它能够与Objective-C和Cocoa互换。
4.4.1 值类型与引用类型
枚举与结构体是一类,类是另一类,这两类之间的主要差别在于前者是值类型,而后者是引用类型。
值类型是不可变的。实际上,这意味着你无法修改值类型实例属性的值。看起来可以修改,但实际上是不行的。比如,我们考虑一个结构体。结构体是值类型:
struct Digit { var number : Int init(_ n:Int) { self.number = n }}
看起来好像可以修改Digit的number属性。毕竟,这是将该属性声明为var的唯一目的;Swift的赋值语法使我们相信修改Digit的number是可行的:
var d = Digit(123)d.number = 42
但实际上,在上述代码中,我们并未修改这个Digit实例的number属性;我们实际上创建了一个不同的Digit实例并替换掉了之前那个。要想证明这一点,我们添加一个Setter观察者:
var d : Digit = Digit(123) { didSet { print("d was set") }}d.number = 42 // "d was set"
一般来说,当修改一个实例值类型时,你实际上会通过另一个实例替换掉当前这个实例。这说明了如果对该实例的引用是通过let声明的,那么这就是无法修改值类型实例的原因。如你所知,使用let声明的初始化变量是不能被赋值的。如果该变量指向了值类型实例,并且该值类型实例有一个属性,即便这个属性是通过var声明的,如果我们对该属性赋值,那么编译器就会报错:
struct Digit { var number : Int init(_ n:Int) { self.number = n }}let d = Digit(123)d.number = 42 // compile error
原因在于这种修改需要替换掉d盒子中的Digit实例。不过,我们无法通过另一个Digit实例替换掉d所指向的Digit实例,因为这意味着要对d赋值,而let声明是不允许这么做的。
反过来,这正是设置实例属性的结构体或枚举的实例方法要被显式标记为mutating关键字的原因所在。比如:
struct Digit { var number : Int init(_ n:Int) { self.number = n } mutating func changeNumberTo(n:Int) { self.number = n }}
如果不使用mutating关键字,那么上述代码将无法编译通过。mutating关键字会让编译器相信你知道这里会产生什么样的结果:如果方法被调用了,那么它会替换掉这个实例,这样该方法只能在使用var声明的引用上进行调用,let则不行:
let d = Digit(123)d.changeNumberTo(42) // compile error
不过,我所说的这一切都不适用于类实例!类实例是引用类型,而非值类型。如果一个类的实例属性可以被修改,那么显然要用var声明;不过,若想通过类实例的引用来设置属性,那么引用是无须声明为var的:
class Dog { var name : String = "Fido"}let rover = Dogrover.name = "Rover" // fine
在上面最后一行代码中,rover所指向的类实例会在原地被修改。这里不会对rover进行隐式赋值,因此let声明是无法阻止修改的。在设置属性时,Dog变量上的Setter观察者是不会被调用的:
var rover : Dog = Dog { didSet { print("did set rover") }}rover.name = "Rover" // nothing in console
如果显式设置rover(设为另一个Dog实例),那么Setter观察者会被调用;不过,这里仅仅是修改了rover所指向的Dog实例的一个属性,因此Setter观察者不会被调用。
这些示例都涉及声明的变量引用。对于函数调用的参数来说,值类型与引用类型之间的差别依然存在,并且与之前所述一致。如果尝试对枚举参数的实例属性或结构体参数的实例属性赋值,那么编译器就会报错。如下代码无法编译通过:
func digitChanger(d:Digit) { d.number = 42 // compile error}
要想让上述代码编译通过,请使用var来声明参数:
func digitChanger(var d:Digit) { d.number = 42}
但如下函数声明没有使用var依然也能编译通过:
func dogChanger(d:Dog) { d.name = "Rover"}
值类型与引用类型存在这些差别的深层次原因在于:对于引用类型来说,在对实例的引用与实例本身之间实际上存在一个隐藏的间接层次;引用实际上引用的是对实例的指针。这又引申出了另一个重要的隐喻:在将类实例赋给变量或作为参数传递给函数时,你可以使用针对同一个对象的多个引用。但结构体与枚举却不是这样。在将枚举实例或结构体实例赋给变量、传递给函数,或从函数返回时,真正赋值或传递的本质上是该实例的一个新副本。不过,在将类实例赋给变量、传递给函数,或从函数返回时,赋值或传递的是对相同实例的引用。
为了证明这一点,我将一个引用赋给另一个引用,然后修改第2个引用,接下来看看第1个引用会发生什么。先来看看结构体:
var d = Digit(123)print(d.number) // 123var d2 = d // assignment!d2.number = 42print(d.number) // 123
上述代码修改了结构体实例d2的number属性;这并不会影响d的number属性。下面再来看看类:
var fido = Dogprint(fido.name) // Fidovar rover = fido // assignment!rover.name = "Rover"print(fido.name) // Rover
上述代码修改了类实例rover的name属性,fido的name属性也随之发生了变化!这是因为第3行的赋值语句执行后,fido与rover都指向了相同的实例。在对枚举或结构体实例赋值时,实际上会执行复制,创建出全新的实例。不过在对类实例进行赋值时,得到的是对相同实例的新引用。
参数传递亦如此。先来看看结构体:
func digitChanger(var d:Digit) { d.number = 42}var d = Digit(123)print(d.number) // 123digitChanger(d)print(d.number) // 123
我们将Digit结构体实例d传递给了函数digitChanger,它会将局部参数d的number属性设为42。不过,Digit实例d的number属性依然为123。这是因为,传递给digitChanger的Digit是个完全不同的Digit。作为函数实参传递Digit的动作会创建一个全新的副本。不过对于类实例来说,传递的是对相同实例的引用:
func dogChanger(d:Dog) { // no "var" needed d.name = "Rover"}var fido = Dogprint(fido.name) // "Fido"dogChanger(fido)print(fido.name) // "Rover"
函数dogChanger中对d的修改会影响Dog实例fido!将类实例传递给函数并不会复制该实例,而更像是将该实例借给函数一样。
可以生成相同实例的多个引用的能力在基于对象编程的世界中是非常重要的,其中对象可以持久化,并且其中的属性也会随之持久化。如果对象A与对象B都是长久存在的对象,并且它们都拥有一个Dog属性(Dog是个类),将对相同Dog实例的引用分别传递给这两个对象,对象A与对象B都可以修改其Dog属性,那么一个对象对Dog属性的修改就会影响另一个对象。你持有着一个对象,然后发现它已经被其他人修改了。这个问题在多线程应用中变得更为严重,相同的对象可能会被两个不同的线程修改;值类型就不存在这些问题;实际上,正是由于这个差别的存在,在设计对象类型时,你会更倾向于使用结构体而非类。
引用类型有缺点,但同样也有优点!优点在于传递类实例变得非常简单,你所传递的只是一个指针而已。无论对象实例有多大,多复杂;无论包含了多少属性,拥有多少数据量,传递实例都是非常快速且高效的,因为整个过程中不会产生新数据。此外,在传递时,类实例更为长久的生命周期对于其功能性和完整性是至关重要的;UIViewController需要是类而不能是结构体,因为无论如何传递,每个UIViewController实例都会表示运行着的应用的视图控制器体系中同一个真实存在且持久的视图控制器。
递归引用
值类型与引用类型的另一个主要差别在于值类型从结构上来说是不能递归的:值类型的实例属性不能是相同类型的实例。如下代码无法编译通过:
struct Dog { // compile error var puppy : Dog?}
如Dog包含了Puppy属性,同时Puppy又包含了Dog属性等更为复杂的循环链也是不合法的。不过,如果Dog是类而不是结构体,那就没问题了。这是值类型与引用类型在内存管理上的不同导致的(第5章将会详细介绍引用类型内存管理,第12章会介绍这个话题)。
在Swift 2.0中,枚举case的关联值可以是该枚举的实例,前提是该case(或整个枚举)被标记为indirect:
enum Node { case None(Int) indirect case Left(Int, Node) indirect case Right(Int, Node) indirect case Both(Int, Node, Node)}
4.4.2 子类与父类
两个类彼此间可以形成子类与父类的关系。比如,我们有个名为Quadruped的类和名为Dog的类,并让Quadruped成为Dog的父类。一个类可以有多个子类,但一个类只能有一个直接父类。这里的“直接”指的是父类本身也可能有父类,这样会形成一个链条,直到到达最终的父类,我们称为基类或根类。由于一个类可以有多个子类,并且只有一个父类,因此会形成一个子类层次树,每个子类都从其父类分支出来,同时顶部只有一个基类。
对于Swift语言本身来说,并不要求一个类必须要有父类;如果有父类,那么最终也是从某个特定的基类延伸出来的。因此,Swift程序中可能会有很多类没有父类,会有很多独立的层次化子类树,每棵树都从不同的基类延伸出来。
不过,Cocoa却不是这样的。在Cocoa中只有一个基类:NSObject,它提供了一个类需要的所有必备功能,其他所有类都是该基类不同层次上的子类。因此,Cocoa包含了一个巨大的类层次树,甚至在你编写代码或创建自定义类之前就是这样的。我们可以将这棵树画出来作为一个大纲。事实上,Xcode可以呈现出这个大纲(如图4-1所示):在iOS项目窗口中,选择View→Navigators→Show Symbol Navigator并单击Hierarchical,选中过滤栏上的第1个与第3个图标(标记为蓝色)。Cocoa类是NSObject下面的树形结构的一部分。
图4-1:Xcode中呈现的Cocoa类层次关系的一部分
起初,设定父类与子类关系的目的在于可以让相关类共享一些功能。比如,我们有一个Dog类和一个Cat类,考虑为这两个类声明一个walk方法。因为狗与猫都是四肢动物,因此可以想象它们走路的方式大体上是相似的。这样,将walk作为Quadruped类的方法会更合理一些,并且让Dog与Cat成为Quadruped的子类。结果就是虽然Dog与Cat没有定义walk方法,但却可以向它们发送walk消息,这是因为它们都有一个拥有walk方法的父类。我们可以说子类继承了父类的方法。
要想将某个类声明为另一个类的子类,请在类声明的类名后面加上一个冒号和父类的名字,比如:
class Quadruped { func walk { print("walk walk walk") }}class Dog : Quadruped {}class Cat : Quadruped {}
现在来证明Dog实际上继承了Quadruped的walk方法:
let fido = Dogfido.walk // walk walk walk
注意,在上述代码中,可以向Dog实例发送walk消息,就好像walk实例方法是在Dog类中声明的一样,虽然实际上是在Dog的父类中声明的,这正是继承所起的作用。
子类化的目的不仅在于让一个类可以继承另一个类的方法;子类还可以声明自己的方法。通常,子类会包含继承自父类的方法,但远非这些。如果Dog没有定义自己的方法,那么我们就很难看到它存在于Quadruped之外的原因。不过,如果Dog知道一些Quadruped所不知道的事情(如bark),那么将其作为单独一个类才有意义。如果在Dog类中声明了bark方法,在Quadruped类中声明了walk方法,并且让Dog成为Quadruped的子类,那么Dog就继承了Quadruped类的行走能力,而且还可以bark:
class Quadruped { func walk { println("walk walk walk") }}class Dog : Quadruped { func bark { println("woof") }}
下面证明一下:
let fido = Dogfido.walk // walk walk walkfido.bark // woof
一个类是否有一个实例方法并不是什么重要的事情,因为方法可以声明在该类中,也可以声明在父类中并继承下来。发送给self的消息在这两种情况下都可以正常运作。如下代码声明了一个barkAndWalk实例方法,它向self发送了两条消息,并没有考虑相应的方法是在哪里声明的(一个在当前类中声明的,另一个则继承自父类):
class Quadruped { func walk { print("walk walk walk") }}class Dog : Quadruped { func bark { print("woof") } func barkAndWalk { self.bark self.walk }}
下面证明一下:
let fido = Dogfido.barkAndWalk // woof walk walk walk
子类还可以重新定义从父类继承下来的方法。比如,也许一些狗的bark不同于别的狗。我们可以定义一个类NoisyDog,它是Dog的子类。Dog声明了bark方法,不过NoisyDog也声明了bark方法,并且其定义不同于Dog对其的定义,这叫作重写。本质原则在于,如果子类重写了从父类继承下来的方法,那么在向该子类实例发送消息时,被调用的方法是子类所声明的那一个。
在Swift中,当重写从父类继承下来的东西时,你需要在声明前显式使用关键字override。比如:
class Quadruped { func walk { print("walk walk walk") }}class Dog : Quadruped { func bark { print("woof") }}class NoisyDog : Dog { override func bark { print("woof woof woof") }}
下面试一下:
let fido = Dogfido.bark // wooflet rover = NoisyDogrover.bark // woof woof woof
值得注意的是,子类函数与父类函数同名并不一定就是重写。回忆一下,只要签名不同,Swift就可以区分开同名的两个函数,它们是不同的函数,因此子类中的实现并不是对父类中实现的重写。只有当子类重新定义了继承自父类的相同函数才是重写,所谓相同函数指的是名字相同(包括外部参数名相同)和签名相同。
很多时候,我们想要在子类中重写某个东西,同时又想访问父类中被重写的对应物。这可以通过向关键字super发送消息来达成所愿。NoisyDog中的bark实现就是个很好的示例。NoisyDog的吠叫与Dog基本上是一样的,只不过次数不同而已。我们想要在NoisyDog的bark实现中表示出这种关系。为了做到这一点,我们让NoisyDog的bark实现发送bark消息,但不是发送给self(这会导致循环),而是发送给super;这样就会在父类而不是自己的类中搜索bark实例方法实现:
class Dog : Quadruped { func bark { print("woof") }}class NoisyDog : Dog { override func bark { for _ in 1...3 { super.bark } }}
下面是调用:
let fido = Dogfido.bark // wooflet rover = NoisyDogrover.bark // woof woof woof
下标函数是个方法。如果父类声明了下标,那么子类可以通过相同的签名声明下标,只要使用关键字override指定即可。为了调用父类的下标实现,子类可以在关键字super后使用方括号(如super[3])。
除了方法,子类还可以继承父类的属性。当然,子类还可以声明自己的附加属性,可以重写继承下来的属性(稍后将会介绍一些限制)。
可以在类声明前加上关键字final防止类被继承,也可以在类成员声明前加上关键字final防止它被子类重写。
4.4.3 类初始化器
类实例的初始化要比结构体或枚举实例的初始化复杂得多,这是因为类存在继承。初始化器的主要工作是确保所有属性都有初值,这样当实例创建出来后其格式就是良好的;初始化器还可以做一些对于实例的初始状态与完整性来说是必不可少的工作。不过,类可能会有父类,也有可能拥有自己的属性与初始化器。这样,除了初始化子类自身的属性并执行初始化器任务,我们必须要确保在初始化子类时,父类的属性也被初始化了,并且初始化器会按照良好的顺序执行。
Swift以一种一致、可靠且巧妙的方式解决了这个问题,它强制施加了一些清晰且定义良好的规则,用于指导类初始化器要做的事情。
1.类初始化器分类
这些规则首先对类可以拥有的初始化器种类进行了区分:
隐式初始化器
类没有存储属性,或是存储属性都作为声明的一部分进行初始化,没有显式初始化器,有一个隐式初始化器init()。
指定初始化器
在默认情况下,类初始化器是个指定初始化器。如果类中有存储属性没有在声明中完成初始化,那么这个类至少要有一个指定初始化器,当类被实例化时,一定会有一个指定初始化器被调用,并且要确保所有存储属性都被初始化。指定初始化器不可以委托给相同类的其他初始化器;指定初始化器不能使用self.init(...)。
便捷初始化器
便捷初始化器使用关键字convenience标记。它是个委托初始化器,必须调用self.init(...)。此外,便捷初始化器必须要调用相同类的一个指定初始化器,否则就必须调用相同类的另一个便捷初始化器,这就构成了一个便捷初始化器链,并且最后要调用相同类的一个指定初始化器。
如下是一些示例。类没有存储属性,因此它具有一个隐式init()初始化器:
class Dog {}let d = Dog
下面这个类的存储属性有默认值,因此它也有一个隐式init()初始化器:
class Dog { var name = "Fido"}let d = Dog
下面这个类的存储属性没有默认值,它有一个指定初始化器,所有这些属性都是在该指定初始化器中初始化的:
class Dog { var name : String var license : Int init(name:String, license:Int) { self.name = name self.license = license }}let d = Dog(name:"Rover", license:42)
下面这个类与上面的类似,不过它还有两个便捷初始化器。调用者无须提供任何参数,因为不带参数的便捷初始化器会沿着便捷初始化器链进行调用,直到遇到一个指定初始化器:
class Dog { var name : String var license : Int init(name:String, license:Int) { self.name = name self.license = license } convenience init(license:Int) { self.init(name:"Fido", license:license) } convenience init { self.init(license:1) }}let d = Dog
值得注意的是,本章之前介绍的初始化器可以做什么,什么时候做等原则依然有效。除了初始化属性,只有当类的所有属性都初始化完毕后,指定初始化器才能使用self。便捷初始化器是个委托初始化器,因此只有在直接或间接地调用了指定初始化器后,它才可以使用self(也不能设置不可变属性)。
2.子类初始化器
介绍完指定初始化器与便捷初始化器并了解了它们之间的差别后,我们来看看当一个类本身是另一个类的子类时,关于初始化器的这些原则会发生什么变化:
无声明的初始化器
如果子类没有声明自己的初始化器,那么其初始化器就会包含从父类中继承下来的初始化器。
只有便捷初始化器
如果子类没有自己的初始化器,那么它就可以声明便捷初始化器,并且与一般的便捷初始化器工作方式别无二致,因为继承向self提供了便捷初始化器一定会调用的指定初始化器。
指定初始化器
如果子类声明了自己的指定初始化器,那么整个规则就会发生变化。现在,初始化器都不会被继承下来!显式的指定初始化器的存在阻止了初始化器的继承。子类现在只拥有你显式编写的初始化器(不过有个例外,稍后将会介绍)。
现在,子类中的每个指定初始化器都有一个额外的要求:它必须要调用父类的一个指定初始化器,通过super.init(...)调用。此外,调用self的规则依然适用。子类的指定初始化器必须要按照如下顺序调用执行:
1.必须确保该类(子类)的所有属性都被初始化。
2.必须调用super.init(...),它所调用的初始化器必须是个指定初始化器。
3.满足上面两条之后,该初始化器才可以使用self,调用实例方法或访问继承的属性。
子类中的便捷初始化器依然适用于上面列出的各种规则。它们必须调用self.init(...),直接或间接(通过便捷初始化器链)调用指定初始化器。如果没有继承下来的初始化器,那么便捷初始化器所调用的初始化器必须显式声明在子类中。
如果指定初始化器没有调用super.init(...),那么在可能的情况下super.init()就会被隐式调用。如下代码是合法的:
class Cat {}class NamedCat : Cat { let name : String init(name:String) { self.name = name }}
在我看来,Swift的这个特性是错误的:Swift不应该使用这种秘密行为,即便这个行为看起来是“有益的”。我认为上述代码不应该编译通过;指定初始化器应该总是显式调用super.init(...)。
重写初始化器
子类可以重写父类初始化器,但要遵循如下限定:
·签名与父类便捷初始化器匹配的初始化器必须也是个便捷初始化器,无须标记为override。
·签名与父类指定初始化器匹配的初始化器可以是指定初始化器,也可以是便捷初始化器,但必须要标记为override。在重写的指定初始化器中可以通过super.init(...)调用被重写的父类指定初始化器。
一般来说,如果子类有指定初始化器,那就不会继承任何初始化器。不过,如果子类重写了父类所有的指定初始化器,那么子类就会继承父类的便捷初始化器。
可失败初始化器
只有在完成了自己的全部初始化任务后,可失败指定初始化器才能够调用return nil。比如,可失败子类指定初始化器必须要完成所有子类属性的初始化,在调用return nil前必须要调用super.init(...)(其实就是在实例销毁前,必须要先构建出实例。不过,这是必要的,目的是确保父类能够完成自己的初始化)。
如果可失败初始化器所调用的初始化器是可失败的,那么调用语法并不会发生变化,也不需要额外的测试。如果被调用的可失败初始化器失败了,那么整个初始化过程就会立刻失败(而且会终止)。
针对重写与委托的目的,返回隐式展开Optional的可失败初始化器(init!)就像是个常规的初始化器(init)一样。对于返回常规Optional(init?)的可失败初始化器,有一些额外的限制:
·init可以重写init?,反之则不行。
·init?可以调用init。
·init可以调用init?,方式是调用init并将结果展开(要使用感叹号,因为如果init?失败了,程序将会崩溃)。
如下示例展示了合法的语法:
class A:NSObject { init?(ok:Bool) { super.init // init? can call init }}class B:A { override init(ok:Bool) { // init can override init? super.init(ok:ok)! // init can call init? using "!" }}
无论何时,子类初始化器都不能设置父类的常量属性(let)。这是因为,当子类可以做除了初始化自己的属性以及调用其他初始化器之外的事情时,父类已经完成了自己的初始化,子类已经没有机会再初始化父类的常量属性了。
下面是一些示例。首先来看这样一个类,它的子类没有声明自己的显式初始化器:
class Dog { var name : String var license : Int init(name:String, license:Int) { self.name = name self.license = license } convenience init(license:Int) { self.init(name:"Fido", license:license) }}class NoisyDog : Dog {}
根据上述代码,我们可以像下面这样创建一个NoisyDog:
let nd1 = NoisyDog(name:"Fido", license:1)let nd2 = NoisyDog(license:2)
上述代码是合法的,因为NoisyDog继承了父类的初始化器。不过,你不能像下面这样创建NoisyDog:
let nd3 = NoisyDog // compile error
上述代码是不合法的。虽然NoisyDog没有声明自己的属性,它也没有隐式初始化器;但它的初始化器是继承下来的,其父类Dog也没有可供继承的隐式init()初始化器。
来看看下面这个类,其子类唯一的显式初始化器是便捷初始化器:
class Dog { var name : String var license : Int init(name:String, license:Int) { self.name = name self.license = license } convenience init(license:Int) { self.init(name:"Fido", license:license) }}class NoisyDog : Dog { convenience init(name:String) { self.init(name:name, license:1) }}
注意到NoisyDog的便捷初始化器是如何通过self.init(...)调用一个指定初始化器(正好是继承下来的)来满足其契约的。根据上述代码,有3种方式可以创建NoisyDog,如下所示:
let nd1 = NoisyDog(name:"Fido", license:1)let nd2 = NoisyDog(license:2)let nd3 = NoisyDog(name:"Rover")
下面这个类的子类声明了一个指定初始化器:
class Dog { var name : String var license : Int init(name:String, license:Int) { self.name = name self.license = license } convenience init(license:Int) { self.init(name:"Fido", license:license) }}class NoisyDog : Dog { init(name:String) { super.init(name:name, license:1) }}
现在,NoisyDog的显式初始化器是个指定初始化器。它通过在super调用指定初始化器满足了契约。现在的NoisyDog阻止了所有初始化器的继承;创建NoisyDog的唯一方式如下所示:
let nd1 = NoisyDog(name:"Rover")
最后,下面这个类的子类重写了其指定初始化器:
class Dog { var name : String var license : Int init(name:String, license:Int) { self.name = name self.license = license } convenience init(license:Int) { self.init(name:"Fido", license:license) }}class NoisyDog : Dog { override init(name:String, license:Int) { super.init(name:name, license:license) }}
NoisyDog重写了父类所有的指定初始化器,因此它继承了父类的便捷初始化器。有两种方式可以创建NoisyDog:
let nd1 = NoisyDog(name:"Rover", license:1)let nd2 = NoisyDog(license:2)
这些示例阐释了你应该牢牢记住的主要规则。你可能不需要记住其他规则,因为编译器会强制应用这些规则,并确保你所做的一切都是正确的。
1.必备初始化器
关于类初始化器还有一点值得注意:类初始化器前面可以加上关键字required,这意味着子类不可以省略它。反过来,这又表示如果子类实现了指定初始化器,从而阻止了继承,那么它必须要重写该初始化器,参见如下示例:
class Dog { var name : String required init(name:String) { self.name = name }}class NoisyDog : Dog { var obedient = false init(obedient:Bool) { self.obedient = obedient super.init(name:"Fido") }} // compile error
上述代码无法编译通过。init(name:)被标记为required,因此除非在NoisyDog中继承或重写init(name:),否则代码编译是通不过的。但我们不能继承,因为通过实现NoisyDog的指定初始化器init(obedient:),继承已经被阻止了。因此必须要重写它:
class Dog { var name : String required init(name:String) { self.name = name }}class NoisyDog : Dog { var obedient = false init(obedient:Bool) { self.obedient = obedient super.init(name:"Fido") } required init(name:String) { super.init(name:name) }}
注意,被重写的必备初始化器并没有标记override,但却被标记了required,这样就可以确保无论子类层次有多深都可以满足需求。
我已经介绍过了将初始化器声明为required的含义,但尚未介绍这么做的原因,本章后面将会通过一些示例进行说明。
2.Cocoa的特殊之处
在继承Cocoa类时,初始化器继承规则可能会产生一些奇怪的结果。比如,在编写iOS程序时,你肯定会声明UIViewController子类。假设该子类声明了一个指定初始化器。父类UIViewController中的指定初始化器是init(nibName:bundle:),因此为了满足规则,你需要像下面这样从指定初始化器中调用它:
class ViewController: UIViewController { init { super.init(nibName:"MyNib", bundle:nil) }}
现在看来一切正常;不过,你会发现创建ViewController实例的代码无法编译通过了:
let vc = ViewController(nibName:"MyNib", bundle:nil) // compile error
只有声明了自己的指定初始化器后,上面的代码才能编译通过;但现在并没有这么做。原因在于,通过在子类中实现指定初始化器,你阻止了初始化器的继承!ViewController类过去会继承UIViewController的init(nibName:bundle:)初始化器,但现在却不是这样。你还需要重写该初始化器,即便实现只是调用被重写的初始化器亦如此:
class ViewController: UIViewController { init { super.init(nibName:"MyNib", bundle:nil) } override init(nibName: String?, bundle: NSBundle?) { super.init(nibName:nibName, bundle:bundle) }}
现在,如下实例化ViewController的代码可以编译通过了:
let vc = ViewController(nibName:"MyNib", bundle:nil) // fine
不过,现在又有一个令人惊诧之处:ViewController本身无法编译通过了!原因在于还有一个施加于ViewController之上的必备初始化器,你还需要将其实现出来。之前你是不知道这一点的,因为当ViewController没有显式初始化器时,你会将必备初始化器继承下来;现在,你又阻止了继承。幸好,Xcode的Fix-It特性提供了一个桩实现;它什么都没做(事实上,如果调用,程序将会崩溃),不过却满足了编译器的要求:
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented")}
本章后面将会介绍该必备初始化器是如何应用的。
4.4.4 类析构器
只有类才会拥有析构器。它是个通过关键字deinit声明的函数,后跟一对花括号,里面是函数体。你永远不会自己调用这个函数;它是当类的实例消亡时由运行时调用的。如果一个类有父类,那么子类的析构器(如果有)会在父类的析构器(如果有)调用之前调用。
析构器的想法在于你可以在实例消亡前执行一些清理工作,或是向控制台打印一些日志,证明操作执行顺序是正确的。我将在第5章介绍内存管理主题时使用析构器。
4.4.5 类属性与方法
子类可以重写继承下来的属性。重写的属性必须要与继承下来的属性拥有相同的名字与类型,并且要标记为override(属性与继承下来的属性不能只名字相同而类型不同,因为这样就无法区分它们了)。需要遵循如下新规则:
·如果父类属性是可写的(存储属性或带有setter的计算属性),那么子类在重写时可以添加对该属性的setter观察者。
·此外,子类可以使用计算变量进行重写。在这种情况下:
·如果父类属性是存储属性,那么子类的计算变量重写就必须要有getter与setter。
·如果父类属性是计算属性,那么子类的计算变量重写就必须要重新实现父类实现的所有访问器。如果父类属性是只读的(只有getter),那么重写可以添加setter。
重写属性的函数可以通过super关键字引用(读或写)继承下来的属性。
类可以有静态成员,只需将其标记为static,就像结构体或枚举一样;还可以有类成员,标记为class。静态与类成员都可以由子类继承(分别作为静态与类成员)。
从程序员的视角来看,静态方法与类方法之间的主要差别在于静态方法无法重写;static就好像是class final的同义词一样。
比如,使用一个静态方法表示狗叫:
class Dog { static func whatDogsSay -> String { return "woof" } func bark { print(Dog.whatDogsSay) }}
子类现在继承了whatDogsSay,但却无法重写。Dog的子类不能包含签名相同的名为whatDogsSay的类方法或静态方法实现。
下面使用一个类方法表示狗叫:
class Dog { class func whatDogsSay -> String { return "woof" } func bark { print(Dog.whatDogsSay) }}
子类继承了whatDogsSay,并且可以重写,要么作为类函数,要么作为静态函数:
class NoisyDog : Dog { override class func whatDogsSay -> String { return "WOOF" }}
静态属性与类属性之间的差别是类似的,不过还要再增加一条重要差别:静态属性可以是存储属性,而类属性只能是计算属性。
下面通过一个静态类属性来表示狗叫:
class Dog { static var whatDogsSay = "woof" func bark { print(Dog.whatDogsSay) }}
子类继承了whatDogsSay,但却无法重写;Dog的子类无法声明类或静态属性whatDogsSay。
现在通过类属性来表示狗叫。它不能是存储属性,因此只能使用计算属性:
class Dog { class var whatDogsSay : String { return "woof" } func bark { print(Dog.whatDogsSay) }}
子类继承了whatDogsSay,并且可以通过类属性或静态属性重写它。不过,正如子类重写的静态属性不能是存储属性一样,这符合之前介绍的关于属性重写的原则:
class NoisyDog : Dog { override static var whatDogsSay : String { return "WOOF" }}