1. 继承
一个类可以继承另一个类的方法,属性和其它特性。当一个类继承其它类时,继承类叫子类,被继承类叫超类(或父类)。在 Swift 中,继承是区分「类」与其它类型的一个基本特征。
可以为类中继承来的属性添加属性观察器,这样一来,当属性值改变时,类就会被通知到。可以为任何属性添加属性观察器,无论它原本被定义为存储型属性还是计算型属性。
1.1.1 定义一个基类
不继承于其它类的类,称之为基类。
注意
Swift 中的类并不是从一个通用的基类继承而来。如果你不为你定义的类指定一个超类的话,这个类就自动成为基类。
1.1.2 子类生成
子类生成指的是在一个已有类的基础上创建一个新的类。子类继承超类的特性,并且可以进一步完善。你还可以为子类添加新的特性。
为了指明某个类的超类,将超类名写在子类名的后面,用冒号分隔:
class SomeClass: SomeSuperclass {
// 这里是子类的定义
}
1.2 重写
子类可以为继承来的实例方法,类方法,实例属性,或下标提供自己定制的实现。我们把这种行为叫重写。
如果要重写某个特性,你需要在重写定义的前面加上override 关键字。这么做,你就表明了你是想提供一个重写版本,而非错误地提供了一个相同的定义。
override 关键字会提醒 Swift 编译器去检查该类的超类(或其中一个父类)是否有匹配重写版本的声明。这个检查可以确保你的重写定义是正确的。
1.2.1 访问超类的方法,属性及下标
当你在子类中重写超类的方法,属性或下标时,有时在你的重写版本中使用已经存在的超类实现会大有裨益。比如,你可以完善已有实现的行为,或在一个继承来的变量中存储一个修改过的值。
在合适的地方,你可以通过使用super 前缀来访问超类版本的方法,属性或下标:
• 在方法someMethod() 的重写实现中,可以通过super.someMethod() 来调用超类版本的someMethod() 方法。
• 在属性someProperty 的 getter 或 setter 的重写实现中,可以通过super.someProperty 来访问超类版本的someProperty 属性。
• 在下标的重写实现中,可以通过super[someIndex] 来访问超类版本中的相同下标。
1.2.2 重写方法
在子类中,你可以重写继承来的实例方法或类方法,提供一个定制或替代的方法实现。
class Train: Vehicle {
override func makeNoise() {
print("Choo Choo")
}
}
1.2.3 重写属性
你可以重写继承来的实例属性或类型属性,提供自己定制的 getter 和 setter,或添加属性观察器使重写的属性可以观察属性值什么时候发生改变。
1.2.3.1 重写属性的 Getters 和 Setters
你可以提供定制的 getter(或 setter)来重写任意继承来的属性,无论继承来的属性是存储型的还是计算型的属性。子类并不知道继承来的属性是存储型的还是计算型的,它只知道继承来的属性会有一个名字和类型。你在重写一个属性时,必需将它的名字和类型都写出来。这样才能使编译器去检查你重写的属性是与超类中同名同类型的属性相匹配的。
你可以将一个继承来的只读属性重写为一个读写属性,只需要在重写版本的属性里提供 getter 和 setter 即可。但是,你不可以将一个继承来的读写属性重写为一个只读属性。
注意
如果你在重写属性中提供了 setter,那么你也一定要提供 getter。如果你不想在重写版本中的 getter 里修改继承来的属性值,你可以直接通过super.someProperty 来返回继承来的值,其中someProperty 是你要重写的属性的名字。
1.2.3.2 重写属性观察器
你可以通过重写属性为一个继承来的属性添加属性观察器。这样一来,当继承来的属性值发生改变时,你就会被通知到,无论那个属性原本是如何实现的
注意
<1>你不可以为继承来的常量存储型属性或继承来的只读计算型属性添加属性观察器。这些属性的值是不可以被设置的,所以,为它们提供willSet 或didSet 实现是不恰当。
<2>此外还要注意,你不可以同时提供重写的 setter 和重写的属性观察器。如果你想观察属性值的变化,并且你已经为那个属性提供了定制的 setter,那么你在 setter 中就可以观察到任何值变化了。
1.2.4 防止重写
你可以通过把方法,属性或下标标记为final 来防止它们被重写,只需要在声明关键字前加上final 修饰符即可
如果你重写了带有final 标记的方法,属性或下标,在编译时会报错。在类扩展中的方法,属性或下标也可以在扩展的定义里标记为 final 的。
你可以通过在关键字class 前添加final 修饰符( final class )来将整个类标记为 final 的。这样的类是不可被继承的,试图继承这样的类会导致编译报错。
2. 构造过程
构造过程是使用类、结构体或枚举类型的实例之前的准备过程。在新实例可用前必须执行这个过程,具体操作包括设置实例中每个存储型属性的初始值和执行其他必须的设置或初始化工作。
2.1 存储属性的初始赋值
类和结构体在创建实例时,必须为所有存储型属性设置合适的初始值。存储型属性的值不能处于一个未知的状态。你可以在构造器中为存储型属性赋初值,也可以在定义属性时为其设置默认值。
注意
当你为存储型属性设置默认值或者在构造器中为其赋值时,它们的值是被直接设置的,不会触发任何属性观察者。
2.1.1 构造器
构造器在创建某个特定类型的新实例时被调用。它的最简形式类似于一个不带任何参数的实例方法,以关键字init 命名:
init() {
// 在此处执行构造过程
}
2.1.2 默认属性值
可以在构造器中为存储型属性设置初始值。同样,你也可以在属性声明时为其设置默认值。
注意:如果一个属性总是使用相同的初始值,那么为其设置一个默认值比每次都在构造器中赋值要好。两种方法的效果是一样的,只不过使用默认值让属性的初始化和声明结合得更紧密。使用默认值能让你的构造器更简洁、更清晰,且能通过默认值自动推导出属性的类型;同时,它也能让你充分利用默认构造器、构造器继承等特性
2.2 自定义构造过程
可以通过输入参数和可选类型的属性来自定义构造过程,也可以在构造过程中修改常量属性。
2.2.1 构造参数
自定义构造过程时,可以在定义中提供构造参数,指定所需值的类型和名字。构造参数的功能和语法跟函数和方法的参数相同。
struct Celsius {
var temperatureInCelsius: Double
init(fromFahrenheit fahrenheit: Double) {
temperatureInCelsius = (fahrenheit - 32.0) / 1.8
}
init(fromKelvin kelvin: Double) {
temperatureInCelsius = kelvin - 273.15
}
}
let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)
// boilingPointOfWater.temperatureInCelsius 是 100.0
let freezingPointOfWater = Celsius(fromKelvin: 273.15)
// freezingPointOfWater.temperatureInCelsius 是 0.0
2.2.2 参数的内部名称和外部名称
跟函数和方法参数相同,构造参数也拥有一个在构造器内部使用的参数名字和一个在调用构造器时使用的外部参数名字。
然而,构造器并不像函数和方法那样在括号前有一个可辨别的名字。因此在调用构造器时,主要通过构造器中的参数名和类型来确定应该被调用的构造器。正因为参数如此重要,如果你在定义构造器时没有提供参数的外部名字,Swift 会为构造器的每个参数自动生成一个跟内部名字相同的外部名。
struct Color {
let red, green, blue: Double
init(red: Double, green: Double, blue: Double) {
self.red = red
self.green = green
self.blue = blue
}
init(white: Double) {
red = white
green = white
blue = white
}
}
//两种构造器都能用于创建一个新的Color 实例,你需要为构造器每个外部参数传值:
let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)
let halfGray = Color(white: 0.5)
2.2.3 不带外部名的构造器参数
如果你不希望为构造器的某个参数提供外部名字,你可以使用下划线( _ )来显式描述它的外部名,以此重写上面所说的默认行为。
struct Celsius {
var temperatureInCelsius: Double
init(fromFahrenheit fahrenheit: Double) {
temperatureInCelsius = (fahrenheit - 32.0) / 1.8
}
init(fromKelvin kelvin: Double) {
temperatureInCelsius = kelvin - 273.15
}
init(_ celsius: Double){
temperatureInCelsius = celsius
}
}
let bodyTemperature = Celsius(37.0)
// bodyTemperature.temperatureInCelsius 为 37.0
2.2.4 可选属性类型
如果你定制的类型包含一个逻辑上允许取值为空的存储型属性——无论是因为它无法在初始化时赋值,还是因为它在之后某个时间点可以赋值为空——你都需要将它定义为可选类型。可选类型的属性将自动初始化为nil ,表示这个属性是有意在初始化时设置为空的。
class SurveyQuestion {
var text: String
var response: String?
init(text: String) {
self.text = text
}
func ask() {
print(text)
}
}
2.2.5 构造过程中常量属性的修改
你可以在构造过程中的任意时间点给常量属性指定一个值,只要在构造过程结束时是一个确定的值。一旦常量属性被赋值,它将永远不可更改。
注意
对于类的实例来说,它的常量属性只能在定义它的类的构造过程中修改;不能在子类中修改。
//尽管text 属性现在是常量,我们仍然可以在类的构造器中设置它的值
class SurveyQuestion {
let text: String
var response: String?
init(text: String) {
self.text = text
}
func ask() {
print(text)
}
}
let beetsQuestion = SurveyQuestion(text: "How about beets?")
beetsQuestion.ask()
// 打印 "How about beets?"
beetsQuestion.response = "I also like beets. (But not with cheese.)"
2.3 默认构造器
如果结构体或类的所有属性都有默认值,同时没有自定义的构造器,那么 Swift 会给这些结构体或类提供一个默认构造器(default initializers)。这个默认构造器将简单地创建一个所有属性值都设置为默认值的实例。
class ShoppingListItem {
var name: String?
var quantity = 1
var purchased = false
}
var item = ShoppingListItem()
2.3.1 结构体的逐一成员构造器
如果结构体没有提供自定义的构造器,它们将自动获得一个逐一成员构造器,即使结构体的存储型属性没有默认值。
struct Size {
var width = 0.0, height = 0.0
}
let twoByTwo = Size(width: 2.0, height: 2.0)
2.3.2 值类型的构造器代理
构造器可以通过调用其它构造器来完成实例的部分构造过程。这一过程称为构造器代理,它能减少多个构造器间的代码重复。
对于值类型,你可以使用self.init 在自定义的构造器中引用相同类型中的其它构造器。并且你只能在构造器内部调用self.init 。
注意
假如你希望默认构造器、逐一成员构造器以及你自己的自定义构造器都能用来创建实例,可以将自定义的构造器写到扩展( extension )中,而不是写在值类型的原始定义中
2.4 类的继承和构造过程
类里面的所有存储型属性——包括所有继承自父类的属性——都必须在构造过程中设置初始值。Swift 为类类型提供了两种构造器来确保实例中所有存储型属性都能获得初始值,它们分别是指定构造器和便利构造器。
2.4.1.1 指定构造器和便利构造器
指定构造器是类中最主要的构造器。一个指定构造器将初始化类中提供的所有属性,并根据父类链往上调用父类的构造器来实现父类的初始化。
2.4.1.2 指定构造器和便利构造器的语法
类的指定构造器的写法跟值类型简单构造器一样:
init(parameters) {
statements
}
便利构造器也采用相同样式的写法,但需要在init 关键字之前放置convenience 关键字,并使用空格将它们俩分开:
convenience init(parameters) {
statements
}
2.4.1.3 类的构造器代理规则
规则 1 - 指定构造器必须调用其直接父类的的指定构造器。
规则 2 - 便利构造器必须调用同类中定义的其它构造器。
规则 3 - 便利构造器必须最终导致一个指定构造器被调用。
• 指定构造器必须总是向上代理
• 便利构造器必须总是横向代理
2.4.1.4 两段式构造过程
Swift 中类的构造过程包含两个阶段。第一个阶段,每个存储型属性被引入它们的类指定一个初始值。当每个存储型属性的初始值被确定后,第二阶段开始,它给每个类一次机会,在新实例准备使用之前进一步定制它们的存储型属性。
2.4.1.5 构造器的继承和重写
跟 Objective-C 中的子类不同,Swift 中的子类默认情况下不会继承父类的构造器。Swift 的这种机制可以防止一个父类的简单构造器被一个更精细的子类继承,并被错误地用来创建子类的实例。
注意:当你重写一个父类的指定构造器时,你总是需要写override 修饰符,即使你的子类将父类的指定构造器重写为了便利构造器。
class Vehicle {
var numberOfWheels = 0
var description: String {
return "\(numberOfWheels) wheel(s)"
}
}
class Bicycle: Vehicle {
override init() {
super.init()
numberOfWheels = 2
}
}
注意
子类可以在初始化时修改继承来的变量属性,但是不能修改继承来的常量属性。
2.4.1.6 构造器的自动继承
如上所述,子类在默认情况下不会继承父类的构造器。但是如果满足特定条件,父类构造器是可以被自动继承的。在实践中,这意味着对于许多常见场景你不必重写父类的构造器,并且可以在安全的情况下以最小的代价继承父类的构造器。
规则 1 - 如果子类没有定义任何指定构造器,它将自动继承所有父类的指定构造器。
规则 2 - 如果子类提供了所有父类指定构造器的实现——无论是通过规则 1 继承过来的,还是提供了自定义实现——它将自动继承所有父类的便利构造器。
2.5 可失败构造器
如果一个类、结构体或枚举类型的对象,在构造过程中有可能失败,则为其定义一个可失败构造器。这里所指的“失败”是指,如给构造器传入无效的参数值,或缺少某种所需的外部资源,又或是不满足某种必要的条件等。
为了妥善处理这种构造过程中可能会失败的情况。你可以在一个类,结构体或是枚举类型的定义中,添加一个或多个可失败构造器。其语法为在init 关键字后面添加问号( init? )。
注意
<1>可失败构造器的参数名和参数类型,不能与其它非可失败构造器的参数名,及其参数类型相同。
<2>严格来说,构造器都不支持返回值。因为构造器本身的作用,只是为了确保对象能被正确构造。因此你只是用return nil 表明可失败构造器构造失败,而不要用关键字return 来表明构造成功。
struct Animal {
let species: String
init?(species: String) {
if species.isEmpty { return nil }
self.species = species
}
}
通过该可失败构造器来构建一个Animal 的实例,并检查构造过程是否成功:
let someCreature = Animal(species: "Giraffe")
// someCreature 的类型是 Animal? 而不是 Animal
if let giraffe = someCreature {
print("An animal was initialized with a species of \(giraffe.species)")
}
// 打印 "An animal was initialized with a species of Giraffe"
如果你给该可失败构造器传入一个空字符串作为其参数,则会导致构造失败:
let anonymousCreature = Animal(species: "")
// anonymousCreature 的类型是 Animal?, 而不是 Animal
if anonymousCreature == nil {
print("The anonymous creature could not be initialized")
}
// 打印 "The anonymous creature could not be initialized"
2.5.1 枚举类型的可失败构造器
可以通过一个带一个或多个参数的可失败构造器来获取枚举类型中特定的枚举成员。如果提供的参数无法匹配任何枚举成员,则构造失败。
enum TemperatureUnit {
case Kelvin, Celsius, Fahrenheit
init?(symbol: Character) {
switch symbol {
case "K":
self = .Kelvin
case "C":
self = .Celsius
case "F":
self = .Fahrenheit
default:
return nil
}
}
}
利用该可失败构造器在三个枚举成员中获取一个相匹配的枚举成员,当参数的值不能与任何枚举成员相匹配时,则构造失败:
let fahrenheitUnit = TemperatureUnit(symbol: "F")
if fahrenheitUnit != nil {
print("This is a defined temperature unit, so initialization succeeded.")
}
// 打印 "This is a defined temperature unit, so initialization succeeded."
let unknownUnit = TemperatureUnit(symbol: "X")
if unknownUnit == nil {
print("This is not a defined temperature unit, so initialization failed.")
}
// 打印 "This is not a defined temperature unit, so initialization failed."
2.5.2 带原始值的枚举类型的可失败构造器
带原始值的枚举类型会自带一个可失败构造器init?(rawValue:) ,该可失败构造器有一个名为rawValue 的参数,其类型和枚举类型的原始值类型一致,如果该参数的值能够和某个枚举成员的原始值匹配,则该构造器会构造相应的枚举成员,否则构造失败。
enum TemperatureUnit: Character {
case Kelvin = "K", Celsius = "C", Fahrenheit = "F"
}
let fahrenheitUnit = TemperatureUnit(rawValue: "F")
if fahrenheitUnit != nil {
print("This is a defined temperature unit, so initialization succeeded.")
}
// 打印 "This is a defined temperature unit, so initialization succeeded."
let unknownUnit = TemperatureUnit(rawValue: "X")
if unknownUnit == nil {
print("This is not a defined temperature unit, so initialization failed.")
}
// 打印 "This is not a defined temperature unit, so initialization failed."
2.5.3 构造失败的传递
类,结构体,枚举的可失败构造器可以横向代理到类型中的其他可失败构造器。类似的,子类的可失败构造器也能向上代理到父类的可失败构造器。
无论是向上代理还是横向代理,如果你代理到的其他可失败构造器触发构造失败,整个构造过程将立即终止,接下来的任何构造代码不会再被执行。
注意:可失败构造器也可以代理到其它的非可失败构造器。通过这种方式,你可以增加一个可能的失败状态到现有的构造过程中。
2.5.4 重写一个可失败构造器
如同其它的构造器,你可以在子类中重写父类的可失败构造器。或者你也可以用子类的非可失败构造器重写一个父类的可失败构造器。这使你可以定义一个不会构造失败的子类,即使父类的构造器允许构造失败。
注意,当你用子类的非可失败构造器重写父类的可失败构造器时,向上代理到父类的可失败构造器的唯一方式是对父类的可失败构造器的返回值进行强制解包。
注意
你可以用非可失败构造器重写可失败构造器,但反过来却不行。
2.5.5 可失败构造器 init!
通常来说我们通过在init 关键字后添加问号的方式( init? )来定义一个可失败构造器,但你也可以通过在init 后面添加惊叹号的方式来定义一个可失败构造器( init! ),该可失败构造器将会构建一个对应类型的隐式解包可选类型的对象。
2.6 必要构造器
在类的构造器前添加required 修饰符表明所有该类的子类都必须实现该构造器:
class SomeClass {
required init() {
// 构造器的实现代码
}
}
在子类重写父类的必要构造器时,必须在子类的构造器前也添加required 修饰符,表明该构造器要求也应用于继承链后面的子类。在重写父类中必要的指定构造器时,不需要添加override 修饰符
class SomeSubclass: SomeClass {
required init() {
// 构造器的实现代码
}
}
注意
如果子类继承的构造器能满足必要构造器的要求,则无须在子类中显式提供必要构造器的实现。
2.7 通过闭包或函数设置属性的默认值
如果某个存储型属性的默认值需要一些定制或设置,你可以使用闭包或全局函数为其提供定制的默认值。每当某个属性所在类型的新实例被创建时,对应的闭包或函数会被调用,而它们的返回值会当做默认值赋值给这个属性。
class SomeClass {
let someProperty: SomeType = {
// 在这个闭包中给 someProperty 创建一个默认值
// someValue 必须和 SomeType 类型相同
return someValue
}()
}
注意闭包结尾的大括号后面接了一对空的小括号。这用来告诉 Swift 立即执行此闭包。如果你忽略了这对括号,相当于将闭包本身作为值赋值给了属性,而不是将闭包的返回值赋值给属性。
注意
如果你使用闭包来初始化属性,请记住在闭包执行时,实例的其它部分都还没有初始化。这意味着你不能在闭包里访问其它属性,即使这些属性有默认值。同样,你也不能使用隐式的self 属性,或者调用任何实例方法。
3. 析构过程
析构器只适用于类类型,当一个类的实例被释放之前,析构器会被立即调用。析构器用关键字deinit 来标示,类似于构造器要用init 来标示。
3.1 析构过程原理
Swift 会自动释放不再需要的实例以释放资源,Swift 通过自动引用计数(ARC) 处理实例的内存管理。通常当你的实例被释放时不需要手动地去清理。但是,当使用自己的资源时,你可能需要进行一些额外的清理。
在类的定义中,每个类最多只能有一个析构器,而且析构器不带任何参数
deinit {
// 执行析构过程
}
析构器是在实例释放发生前被自动调用。你不能主动调用析构器。子类继承了父类的析构器,并且在子类析构器实现的最后,父类的析构器会被自动调用。即使子类没有提供自己的析构器,父类的析构器也同样会被调用。