注:本文为自己学习The Swift Programming Language的笔记,其中的例子为引用原书和其他博文或自己原创的。每个例子都会批注一些实践过程中的经验或思考总结。
1.基础
类、结构体和枚举通过初始化(initializtion)让它们的实例做好使用准备。初始化的过程包括为实例每个存储属性赋初始值,或者完成新实例能被使用之前所必要的一些其他的设置。
通过定义构造函数(initializer)来实现初始化过程,构造函数是一种特殊的方法,它在创建新实例的时候被调用。与Objective-C不同的是,Swift的构造器并不返回值,它的主要任务是确保新实例在第一次使用之前完成了所有必要的初始工作。类的实例还可以实现析构函数(deinitializer),用来完成类实例回收之前的一些清理工作。
2.为存储属性设置初始值
类和结构体的实例在被创建时,必须保证所有的存储属性有自己的初始值,存储属性不能处在一个不定(indeterminate)状态。
除了可以在构造函数中初始化存储属性的值,也可以在存储属性定义时赋给它默认值,但这两种方式无论哪一种都不会触发属性观察器。
2.1构造函数
最简单形式的构造函数像一个没有参数的实例方法,定义构造函数使用init关键字。
定义一个Fahrenheit(华氏温度)结构体,他有一个temperature(温度)存储变量,通过构造函数给它赋初始值:
struct Fahrenheit {
var temperature : Double
init() {
temperature = 32.0
}
}
华氏温度的冰点是32度,相当于摄氏温度的0度。
2.2默认属性值
如果一个属性总是有相同的初始值,那么不要在构造函数中赋初始值,而是选择在属性定义时提供默认值。虽然最后的效果是一样的,但是定义默认值比构造函数中声明初始值离存储属性的定义更近,这样使构造函数更加简洁而且使用了类型推断,并且默认值让你更容易利用默认构造函数和构造函数的继承。
上面的例子可以简化为:
struct AnotherFahrenheit {
var temperature = 32.0
}
没有显式定义构造函数而使用默认的构造函数让类型声明更加简洁。
3.定制构造函数
构造函数可以通过使用传入参数、可空属性类型或者在初始化过程中修改常量属性的值完成满足需求的定制。
3.1初始化参数
构造函数可以像其他函数和方法一样有自己的传入参数,每个参数有自己的参数名和参数类型。下面的例子定义一个Celsius(摄氏温度)结构体,它有两个重载的构造函数init(fromFahrenheit : )和init(fromKelvin : )分别通过华氏温度和开尔文温度来初始化摄氏温度:
struct Celsius {
var temperature = 0.0
init(fromFahrenheit fahrenheit : Double) {
temperature = (fahrenheit - 32.0) / 1.8
}
init(fromKelvin kelvin : Double) {
temperature = kelvin - 273.15
}
let boilingPointOfWater = Celsius(fromFahrenheit : 212.0)
let freezingPointOfWater = Celsius(fromKelvin : 273.15)
}
两个构造函数相当于完成了不同温度记法向摄氏温度的转换,两个Celsius类型的常量存储属性通过两个构造函数实现赋初始值。
3.2本地参数名和外部参数名
和其他函数和方法一样,构造函数的传入参数可以有用来外部传入的外部参数名和用来内部调用的内部参数名。
由于构造函数没有显式的函数名,外部参数名的使用就更为重要。因此Swift为每一个没有显式声明外部参数名的参数提供了自动的外部参数名,自动外部参数名和本地参数名相同。如果不想自动提供外部参数名,可以在写外部参数名的位置用下划线表示,以重载默认外部参数名。
下面定义一个Color颜色类,它有红绿蓝三个介于0.0到1.0之间的值的存储变量,可以通过构造函数赋初始值,Swift提供默认的外部参数名:
struct Color {
let red = 0.0, green = 0.0, blue = 0.0
init(red : Double, green : Double, blue : Double) {
self.red = red
self.green = green
self.blue = blue
}
}
let magenta = Color(red : 1.0, green : 0.0, blue : 1.0)
定义一个magenta(洋红色)实例,构造函数中必须提供三个参数的外部参数名,默认的构造函数不再起作用。
3.3可空属性类型
如果一个属性被声明为可空类型(optional type),那么它可以允许初始值为空(nil)。可空类型的属性自动初始化为nil,表示在初始化时这个属性暂时还没有值(no value yet)。
下面定义一个继承自Color类的子类ColorWithOpacity,它有一个可空Double(optional double)存储属性:
class ColorWithOpacity : Color {
var alpha : Double?
init(red: Double, green: Double, blue: Double) {
super.init(red : red, green : green, blue : blue)
}
init(red: Double, green: Double, blue: Double, alpha : Double) {
super.init(red : red, green : green, blue : blue)
self.alpha = alpha
}
}
使用第一个构造函数构造一个普通veryGreen实例,初始化时并没有给alpha赋值,它的初始值是nil:
let veryGreen = ColorWithOpacity(red : 0.0, green : 1.0, blue : 0.0)
let transparentVeryGreen = ColorWithOpacity(red : 0.0, green : 1.0, blue : 0.0, alpha : 0.5)
3.4初始化时修改常量属性值
常量属性的值可以在构造函数的任何位置进行修改,只要在初始化完成时有一个确定的值。修改上面的变量存储属性alpha为常量,构造函数中仍然可以修改它的值:
class ColorWithOpacity : Color {
let alpha : Double?
init(red: Double, green: Double, blue: Double) {
super.init(red : red, green : green, blue : blue)
}
init(red: Double, green: Double, blue: Double, alpha : Double) {
super.init(red : red, green : green, blue : blue)
self.alpha = 0.0
self.alpha = alpha
}
}
常量属性alpha可以被多次赋值,虽然第一句没有什么意义。
4.默认构造函数
Swift给[所有存储属性都有默认值]且[没有显式提供构造函数]的任何结构体和基类[base class]提供默认的构造函数,默认构造函数没有传入参数,所有的存储属性被赋予默认值(没有显式提供默认值的可空类型的默认值是nil)。
除了这个最基本的默认构造函数,Swift还提供一种完成所有存储属性初始化的构造函数,参数是所有存储属性,默认所有的参数外部参数名和局部参数名同属性名。
下面定义一个Point结构体,它满足默认构造函数条件:是结构体、每个属性有默认值、没有显式构造函数,那么Swift为它提供两个默认构造函数:
struct Point {
var x = 0.0, y = 0.0
}
let original = Point()
let somePointAtXAxis = Point(x : 1.0, y : 0.0)
不带参数的构造函数相当于创建一个原点实例,带参数的构造函数就要传入所有存储属性的初始值。
5.值类型的构造函数委托
构造函数委托[initializer delegation]允许在一个构造函数中调用其他的构造函数,这有效地避免的多个构造函数之间的代码重复。
值类型[value type]和引用类型[reference type]关于构造函数委托有不同的规则和形式。值类型(结构体和枚举)由于没有继承,构造函数委托相对简单,因为他们只能委托自己定义的其他构造函数。而对于类由于可以继承,它必须在构造函数中保证继承的存储属性都被赋了一个合适的值。
对于值类型,用self.init调用其他的构造函数,self.init只能在构造函数中调用。
注意,一旦值类型定义了自定义的构造函数,默认的构造函数均不可使用。这样的约束避免了他人在使用你的添加了十分重要的初始化工作的复杂的构造函数时误用简单的默认构造函数。如果又想保留值类型的两种默认的构造函数,又想实现自己的构造函数,就要用到扩展[extension],这在以后的章节中会详细阐述。
下面的例子中定义了一个Size结构体表示矩形的长和宽,Rect矩形类有三重构造函数重载,第一个和第二个构造函数实现了两种默认构造函数,而第三种构造函数通过构造函数的委托,调用第二种构造函数:
struct Size {
var width = 0.0, height = 0.0
}
struct Rect {
var origin = Point()
var size = Size()
init() {}
init(origin: Point, size: Size) {
self.origin = origin
self.size = size
}
init(center: Point, size: Size) {
let originX = center.x - (size.width / 2)
let originY = center.y - (size.height / 2)
self.init(origin: Point(x: originX, y: originY), size: size)
}
}
第三个构造函数通过将中点转换为原点,委托第二个构造函数实现初始化。
6.类的继承和初始化
类的所有存储属性,包括继承自超类的所有属性,必须在初始化的过程中赋初始值。
Swift为类类型提供了两种构造函数来保证每一个存储属性都有初始值,他们分别叫做指定构造函数[designated initializer]和快捷构造函数[convenience initializer]。
6.1指定构造函数和快捷构造函数
(1)指定构造函数
指定构造函数[designated initializer]是类的基础构造函数,它完整的初始化了类中定义的所有存储属性并调用了适当的超类的构造函数来完成超类链[superclass chain]中所有的属性初始化任务。
类的指定构造函数一般很少,通常来说只有一个。指定构造函数是初始化过程的汇集点[funnel point],它会沿着超类链逐级进行初始化。每一个类都必须要有至少一个指定构造函数,在某些类中这项要求以继承一个或多个指定构造函数满足。
(2)快捷构造函数
快捷构造函数是类的次级构造函数,有两种方式构建一个快捷构造函数。一是使用部分默认值调用指定构造函数,二是为了特殊用途或特定输入值创建一个类的实例。
快捷构造函数并不是必要的,当普通构造函数的简短形式可以让类的初始化更加简洁明了时使用快捷构造函数。
6.2构造函数链
Swift采用了三种构造函数之间的委托规则来简化指定构造函数和快捷构造函数之间的关系:
(1)规则一:指定构造函数必须调用直接上级超类的指定构造函数
(2)规则二:快捷构造函数必须调用当前类中其他可用构造函数(可以是指定或快捷)
(3)规则三:快捷构造函数必须从根本上终止调用指定构造函数
简化表达就是[指定构造函数必须向上委托,快捷构造函数只能同级交叉委托]。
结合以上规则分析原书中的构造函数链例子来进一步理解指定构造函数和快捷构造函数:
这是一个四级继承构造函数链,最上层的类是基类。
第一层类的指定构造函数没有超类的指定构造函数可以调用,另外两个快捷构造函数满足规则2和3:一个调用同层的指定构造函数,另一个调用同层的快捷构造函数。
第二、三、四层的指定构造函数必须调用上一层类的指定构造函数,符合规则一。所谓[快捷构造函数必须从根本上终止调用指定构造函数]即规则三指的是快捷构造函数不会被任何指定构造函数调用。
注意,我们仅仅在类的定义时区别两种构造函数,在创建类的实例时他们从外面看起来是没有任何区别的。
6.3初始化的两个阶段
Swift类的初始化分两个阶段过程进行,在第一阶段每一个存储变量被赋初始值,一旦存储属性的初始值确定了,第二阶段开始。第二阶段中每个类都被赋予在新实例准备好被使用前的更改存储属性的机会。两阶段的初始化过程让初始化保证安全的同时赋予灵活性,它防止了属性值在初始化完成之前被访问,并且防止了属性被其他构造函数无意赋予其他值。
Swift编译器完成了4种有效的安全性检查来保证两阶段初始化无误完成:
(1)Safety check 1:[指定构造函数在委托超类构造函数之前必须保证每个该类的存储属性均被初始化]
(2)Safety check 2:[指定构造函数只有在向上委托了超类构造函数之后才能给继承的属性赋值,否则给继承属性赋的新值会在委托时被超类覆盖]
(3)Safety check 3:[快捷构造函数在给任何属性(该类定义&继承的属性)赋值之前必须委托另外的构造函数,否则这些赋值会被委托的构造函数覆盖]
(4)Safety check 4:[在初始化第一阶段完成之前,构造函数不能调用任何实例方法,不能读任何实例属性的值,也不能引用self的值]
基于以上四条安全检查,Swift初始化两阶段具体过程如下:
[阶段1]
(1)类的指定构造函数或者快捷构造函数被调用
(2)分配该类的这个实例占用的内存,但内存并没有初始化
(3)指定构造函数确保该类中自己定义的存储属性都有初始值,这些存储属性占用的内存被初始化
(4)指定构造函数调用超类的构造函数完成超类存储属性的初始化
(5)重复(3)(4)沿继承链逐级向上,直到基类完成其存储属性初始化
(6)当到达继承链的顶部之后,实例占用的所有内存被初始化,阶段1结束
[阶段2]
(2)最终,所有的快捷构造函数可以通过self定制这个实例
6.4构造函数继承和重写
不想Objective-C的子类,Swift的子类并不默认继承超类构造函数。Swift这样做防止了超类的一个简单的构造函数被实行特别功能的子类构造函数自动继承后子类实例不能被正确创建。
如果希望定义的子类能够呈现一个或多个超类的构造函数,可以在子类中用重写(override)实现。
重写指定构造函数时,可以在子类中重写它的实现并在当前重写中调用超类版本的该构造函数;重写快捷构造函数时,重写中必须调用该类中其他的构造函数。
重写构造函数时不用加override关键字前缀,和方法、属性、下标不同。
6.5构造函数自动继承
如上所述,子类并不默认继承超类构造函数,但是在满足特定条件情况下子类能够自动继承超类构造函数。这意味着大多数情况我们不用重写超类构造函数,并能够花费最小代价安全的完成继承工作。
假设子类中定义的所有新属性都有默认值,那么一下两条规则有:
(1)规则一:[如果子类没有定义任何指定构造函数,那么它自动继承超类的指定构造函数]
(2)规则二:[如果子类实现超类所有的指定构造函数,要么通过规则一要么以其他方式,它自动继承超类所有的快捷构造函数]
6.6构造函数的语法
指定构造函数和最简单的构造函数格式一样:
init(parameters) {
statements
}
快捷构造函数定义要用到convenience关键字:
convenience init(parameters) {
statements
}
6.7两种构造函数的应用举例
通过原书中的例子来熟悉指定构造函数、快捷构造函数以及构造函数自动继承。定义RecipeIngredient类继承Food类,ShoppingList类继承RecipeIngredient类。
(1)Food类是基类,它有两个构造函数来完成初始化:
class Food {
var name: String
init(name: String) {
self.name = name
}
convenience init() {
self.init(name: "[Unnamed]")
}
}
类并没有默认的成员构造函数(memberwise initializer),Food类中定义的指定构造函数完成这个功能。可以调用两种不同的构造函数完成类实例的创建:
let namedMeat = Food(name: "Bacon")
let mysteryFood = Food()
(2)RecipeIngredient类继承Food类,它自己定义了存储属性quantity(当然也继承Food的属性name),也定义两种构造函数:
class RecipeIngredient: Food {
var quantity: Int
init(name: String, quantity: Int) {
self.quantity = quantity
super.init(name: name)
}
convenience init(name: String) {
self.init(name: name, quantity: 1)
}
}
指定构造函数
init
(name:
String
, quantity:
Int
)调用超类的指定构造函数,满足规则1;在委托超类构造函数之前初始化自己定义的属性quantity,满足安全检查1。
快捷构造函数
init
(name:
String
)调用该类的指定构造函数实现,满足规则2&3和安全检查3。
由于快捷构造函数init(name: String)和超类中的指定构造函数同名,满足自动继承构造函数的规则二,RecipeIngredient类继承Food类的快捷构造函数init(),因此RecipeIngredient类有三个构造函数:
let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)
(3)最后一级的类是ShoppingList类,它增加了一个购买与否的标识purchased:
class ShoppingListItem: RecipeIngredient {
var purchased = false
var description: String {
var output = "\(quantity) x \(name.lowercaseString)"
output += purchased ? " ✔" : " ✘"
return output
}
}
由于自动继承构造函数的规则一&二,ShoppingList类自动继承RecipeIngredient类的三种构造函数:
var breakfastList = [
ShoppingListItem(),
ShoppingListItem(name: "Bacon"),
ShoppingListItem(name: "Eggs", quantity: 6),
]
breakfastList[0].name = "Orange juice"
breakfastList[0].purchased = true
for item in breakfastList {
println(item.description)
}
// 1 x orange juice ✔
// 1 x bacon ✘
// 6 x eggs ✘
这三个类的构造函数链示意图如下所示:
7.用闭包或函数设定默认值
class SomeClass {
let someProperty: SomeType = {
// create a default value for someProperty inside this closure
// someValue must be of the same type as SomeType
return someValue
}()
}
值得注意的是,这类闭包没有传入参数,返回值类型someValue必须是SomeType类型的。并且这些闭包不能访问任何其他的属性值(即使它有默认值),不能调用任何实例方法,不能访问self属性。
以国际象棋的棋盘颜色初始化为例子,熟悉闭包设定属性默认值的方法。国际象棋棋盘是黑白相间的,用一个一维数组表示二维棋盘的颜色,由于初始化复杂,要以默认值的形式赋值给属性的话就要用到闭包:
struct Checkerboard {
let boardColors: Bool[] = {
var temporaryBoard = Bool[]()
var isBlack = false
for i in 1...10 {
for j in 1...10 {
temporaryBoard.append(isBlack)
isBlack = !isBlack
}
isBlack = !isBlack
}
return temporaryBoard
}()
func squareIsBlackAtRow(row: Int, column: Int) -> Bool {
return boardColors[(row * 10) + column]
}
}
当类的实例被创建时,这个闭包就被调用,boardColors数组被赋予默认的黑白相间的颜色。