通过定义构造器(Initializers
)来实现构造过程,这些构造器可以看做是用来创建特定类型新实例的特殊方法。与 Objective-C 中的构造器不同,Swift 的构造器无需返回值,它们的主要任务是保证新实例在第一次使用前完成正确的初始化。
存储属性的初始赋值
ps: 当你为存储型属性设置默认值或者在构造器中为其赋值时,它们的值是被直接设置的,不会触发任何属性观察者(
property observers
)。
默认属性值
ps: 如果一个属性总是使用相同的初始值,那么为其设置一个默认值比每次都在构造器中赋值要好。两种方法的效果是一样的,只不过使用默认值让属性的初始化和声明结合得更紧密。使用默认值能让你的构造器更简洁、更清晰,且能通过默认值自动推导出属性的类型;同时,它也能让你充分利用默认构造器、构造器继承等特性
参数的内部名称和外部名称
然而,构造器并不像函数和方法那样在括号前有一个可辨别的名字。因此在调用构造器时,主要通过构造器中的参数名和类型来确定应该被调用的构造器。正因为参数如此重要,如果你在定义构造器时没有提供参数的外部名字,Swift 会为每个构造器的参数自动生成一个跟内部名字相同的外部名。
可选属性类型
如果你定制的类型包含一个逻辑上允许取值为空的存储型属性——无论是因为它无法在初始化时赋值,还是因为它在之后某个时间点可以赋值为空——你都需要将它定义为可选类型optional type
。可选类型的属性将自动初始化为nil
,表示这个属性是有意在初始化时设置为空的。
默认构造器
如果结构体或类的所有属性都有默认值,同时没有自定义的构造器,那么 Swift 会给这些结构体或类提供一个默认构造器。这个默认构造器将简单地创建一个所有属性值都设置为默认值的实例。
结构体的逐一成员构造器
除了上面提到的默认构造器,如果结构体没有提供自定义的构造器,它们将自动获得一个逐一成员构造器,即使结构体的存储型属性没有默认值。
值类型的构造器代理
如果你为某个值类型定义了一个自定义的构造器,你将无法访问到默认构造器(如果是结构体,还将无法访问逐一成员构造器)。这个限制可以防止你为值类型定义了一个进行额外必要设置的复杂构造器之后,别人还是错误地使用了一个自动生成的构造器。
ps: 假如你希望默认构造器、逐一成员构造器以及你自己的自定义构造器都能用来创建实例,可以将自定义的构造器写到扩展(extension)中,而不是写在值类型的原始定义中。
下面例子将定义一个结构体Rect,用来代表几何矩形。这个例子需要两个辅助的结构体Size和Point,它们各自为其所有的属性提供了初始值0.0。你可以通过以下三种方式为Rect创建实例——使用被初始化为默认值的origin和size属性来初始化;提供指定的origin和size实例来初始化;提供指定的center和size来初始化。在下面Rect结构体定义中,我们为这三种方式提供了三个自定义的构造器:
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)
}
}
类的构造器代理规则
为了简化指定构造器和便利构造器之间的调用关系,Swift 采用以下三条规则来限制构造器之间的代理调用:
规则 1
指定构造器必须调用其直接父类的的指定构造器。
规则 2
便利构造器必须调用同一类中定义的其它构造器。
规则 3
便利构造器必须最终导致一个指定构造器被调用。
一个更方便记忆的方法是:
- 指定构造器必须总是向上代理
- 便利构造器必须总是横向代理
如图所示,父类中包含一个指定构造器和两个便利构造器。其中一个便利构造器调用了另外一个便利构造器,而后者又调用了唯一的指定构造器。这满足了上面提到的规则 2 和 3。这个父类没有自己的父类,所以规则 1 没有用到。
子类中包含两个指定构造器和一个便利构造器。便利构造器必须调用两个指定构造器中的任意一个,因为它只能调用同一个类里的其他构造器。这满足了上面提到的规则 2 和 3。而两个指定构造器必须调用父类中唯一的指定构造器,这满足了规则 1。
ps:这些规则不会影响类的实例如何创建。任何上图中展示的构造器都可以用来创建完全初始化的实例。这些规则只影响类定义如何实现。
下面图例中展示了一种涉及四个类的更复杂的类层级结构。它演示了指定构造器是如何在类层级中充当“管道”的作用,在类的构造器链上简化了类之间的相互关系。
两段式构造过程
Swift 中类的构造过程包含两个阶段。第一个阶段,每个存储型属性通过引入它们的类的构造器来设置初始值。当每一个存储型属性值被确定后,第二阶段开始,它给每个类一次机会在新实例准备使用之前进一步定制它们的存储型属性。
两段式构造过程的使用让构造过程更安全,同时在整个类层级结构中给予了每个类完全的灵活性。两段式构造过程可以防止属性值在初始化之前被访问,也可以防止属性被另外一个构造器意外地赋予不同的值。
ps: Swift 的两段式构造过程跟 Objective-C 中的构造过程类似。最主要的区别在于阶段 1,Objective-C 给每一个属性赋值
0
或空值(比如说0
或nil
)。Swift 的构造流程则更加灵活,它允许你设置定制的初始值,并自如应对某些属性不能以0
或nil
作为合法默认值的情况。
Swift 编译器将执行 4 种有效的安全检查,以确保两段式构造过程能顺利完成:
安全检查 1
指定构造器必须保证它所在类引入的所有属性都必须先初始化完成,之后才能将其它构造任务向上代理给父类中的构造器。
如上所述,一个对象的内存只有在其所有存储型属性确定之后才能完全初始化。为了满足这一规则,指定构造器必须保证它所在类引入的属性在它往上代理之前先完成初始化。
安全检查 2
指定构造器必须先向上代理调用父类构造器,然后再为继承的属性设置新值。如果没这么做,指定构造器赋予的新值将被父类中的构造器所覆盖。
安全检查 3
便利构造器必须先代理调用同一类中的其它构造器,然后再为任意属性赋新值。如果没这么做,便利构造器赋予的新值将被同一类中其它指定构造器所覆盖。
安全检查 4
构造器在第一阶段构造完成之前,不能调用任何实例方法,不能读取任何实例属性的值,不能引用self作为一个值。
类实例在第一阶段结束以前并不是完全有效的。只有第一阶段完成后,该实例才会成为有效实例,才能访问属性和调用方法。
以下是两段式构造过程中基于上述安全检查的构造流程展示:
阶段 1
- 某个指定构造器或便利构造器被调用。
- 完成新实例内存的分配,但此时内存还没有被初始化。
- 指定构造器确保其所在类引入的所有存储型属性都已赋初值。存储型属性所属的内存完成初始化。
- 指定构造器将调用父类的构造器,完成父类属性的初始化。
- 这个调用父类构造器的过程沿着构造器链一直往上执行,直到到达构造器链的最顶部。
- 当到达了构造器链最顶部,且已确保所有实例包含的存储型属性都已经赋值,这个实例的内存被认为已经完全初始化。此时阶段 1 完成。
阶段 2
- 从顶部构造器链一直往下,每个构造器链中类的指定构造器都有机会进一步定制实例。构造器此时可以访问self、修改它的属性并调用实例方法等等。
- 最终,任意构造器链中的便利构造器可以有机会定制实例和使用self。
构造器的继承和重写
跟 Objective-C 中的子类不同,Swift 中的子类默认情况下不会继承父类的构造器。Swift 的这种机制可以防止一个父类的简单构造器被一个更专业的子类继承,并被错误地用来创建子类的实例。
ps: 父类的构造器仅会在安全和适当的情况下被继承。
当你在编写一个和父类中指定构造器相匹配的子类构造器时,你实际上是在重写父类的这个指定构造器。因此,你必须在定义子类构造器时带上override
修饰符。即使你重写的是系统自动提供的默认构造器,也需要带上override
修饰符
ps: 当你重写一个父类的指定构造器时,你总是需要写
override
修饰符,即使你的子类将父类的指定构造器重写为了便利构造器。
相反,如果你编写了一个和父类便利构造器相匹配的子类构造器,由于子类不能直接调用父类的便利构造器,因此,严格意义上来讲,你的子类并未对一个父类构造器提供重写。最后的结果就是,你在子类中“重写”一个父类便利构造器时,不需要加override
前缀。
构造器的自动继承
如上所述,子类在默认情况下不会继承父类的构造器。但是如果满足特定条件,父类构造器是可以被自动继承的。在实践中,这意味着对于许多常见场景你不必重写父类的构造器,并且可以在安全的情况下以最小的代价继承父类的构造器。
假设你为子类中引入的所有新属性都提供了默认值,以下 2 个规则适用:
规则 1
如果子类没有定义任何指定构造器,它将自动继承所有父类的指定构造器。
规则 2
如果子类提供了所有父类指定构造器的实现——无论是通过规则 1 继承过来的,还是提供了自定义实现——它将自动继承所有父类的便利构造器。(即使属性没有默认值,只要实现了父类的所有指定构造器,就会自动继承父类的所有便利构造器)
即使你在子类中添加了更多的便利构造器,这两条规则仍然适用。
ps: 对于规则 2,子类可以将父类的指定构造器实现为便利构造器。
可失败构造器
如果一个类、结构体或枚举类型的对象,在构造过程中有可能失败,则为其定义一个可失败构造器。这里所指的“失败”是指,如给构造器传入无效的参数值,或缺少某种所需的外部资源,又或是不满足某种必要的条件等。
ps: 可失败构造器的参数名和参数类型,不能与其它非可失败构造器的参数名,及其参数类型相同。
严格来说,构造器都不支持返回值。因为构造器本身的作用,只是为了确保对象能被正确构造。因此你只是用return nil
表明可失败构造器构造失败,而不要用关键字return
来表明构造成功。
带原始值的枚举类型的可失败构造器
带原始值的枚举类型会自带一个可失败构造器init?(rawValue:)
,该可失败构造器有一个名为rawValue
的参数,其类型和枚举类型的原始值类型一致,如果该参数的值能够和某个枚举成员的原始值匹配,则该构造器会构造相应的枚举成员,否则构造失败
类的可失败构造器
值类型(也就是结构体或枚举)的可失败构造器,可以在构造过程中的任意时间点触发构造失败。甚至在属性被初始化前。
而对类而言,可失败构造器只能在类引入的所有存储型属性被初始化后,以及构造器代理调用完成后,才能触发构造失败。
class Product {
let name: String!
init?(name: String) {
self.name = name
if name.isEmpty { return nil }
}
}
构造失败的传递
类,结构体,枚举的可失败构造器可以横向代理到类型中的其他可失败构造器。类似的,子类的可失败构造器也能向上代理到父类的可失败构造器。
class CartItem: Product {
let quantity: Int!
init?(name: String, quantity: Int) {
self.quantity = quantity
super.init(name: name)
if quantity < 1 { return nil }
}
}
重写一个可失败构造器
如同其它的构造器,你可以在子类中重写父类的可失败构造器。或者你也可以用子类的非可失败构造器重写一个父类的可失败构造器。这使你可以定义一个不会构造失败的子类,即使父类的构造器允许构造失败。
ps: 你可以用非可失败构造器重写可失败构造器,但反过来却不行。
当你用子类的非可失败构造器重写父类的可失败构造器时,向上代理到父类的可失败构造器的唯一方式是对父类的可失败构造器的返回值进行强制解包。
可失败构造器 init!
你可以在init?
中代理到init!
,反之亦然。你也可以用init?
重写init!
,反之亦然。你还可以用init
代理到init!
,不过,一旦init!
构造失败,则会触发一个断言。
必要构造器
在类的构造器前添加required
修饰符表明所有该类的子类都必须实现该构造器:
class SomeClass {
required init() {
// 构造器的实现代码
}
}
在子类重写父类的必要构造器时,必须在子类的构造器前也添加required修饰符,表明该构造器要求也应用于继承链后面的子类。在重写父类中必要的指定构造器时,不需要添加override修饰符
class SomeSubclass: SomeClass {
required init() {
// 构造器的实现代码
}
}
ps: 如果子类继承的构造器能满足必要构造器的要求,则无须在子类中显式提供必要构造器的实现。
通过闭包或函数设置属性的默认值
如果某个存储型属性的默认值需要一些定制或设置,你可以使用闭包或全局函数为其提供定制的默认值。每当某个属性所在类型的新实例被创建时,对应的闭包或函数会被调用,而它们的返回值会当做默认值赋值给这个属性。
这种类型的闭包或函数通常会创建一个跟属性类型相同的临时变量,然后修改它的值以满足预期的初始状态,最后返回这个临时变量,作为属性的默认值。
class SomeClass {
let someProperty: SomeType = {
// 在这个闭包中给 someProperty 创建一个默认值
// someValue 必须和 SomeType 类型相同
return someValue
}()
}
ps: 如果你使用闭包来初始化属性,请记住在闭包执行时,实例的其它部分都还没有初始化。这意味着你不能在闭包里访问其它属性,即使这些属性有默认值。同样,你也不能使用隐式的
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]
}
}
let board = Checkerboard()
print(board.squareIsBlackAtRow(0, column: 1))
// 打印 "true"
print(board.squareIsBlackAtRow(9, column: 9))
// 打印 "false"
要点总结
有两点疑惑:
class Product {
let name: String!
init?(name: String) {
self.name = name
if name.isEmpty { return nil }
}
}
The
Product
class defined above is very similar to theAnimal
structure seen eariler.TheProduct
class has a constantname
property that must not be allowed to take an empty string value.To enforce this requirement,theProduct
class uses a failable initializer to ensure that the propety's value is nonempty before allowing initialization to succeed.
However,Product
is a class,not a structure.This means that unlikeAnimal
,any failable initializer for theProduct
class must provide an initial value for thename
property before triggering an intialization failure.
In the example above,thename
property of theProduct
class is defined as having an implicitly unwrapped optional string type(String!).Because it is of an optional type,this means that thename
property has a default value ofnil
before it is assigned a specific value during initialization.This default value ofnil
in turn means that all of the properties introduced by the Product class have a valid initial value.As a result,the failable intializer forProduct
can trigger an initialization failure at the start of the intializer if it is passed an empty string,before assigning a specific value to thename
property within the initializer.
以上的代码片段和描述都是从书上摘录的。令我疑惑的是,根据描述来看,再init?的方法体里,self.name = name应该放到 return nil下面去。意思是说optional在构造过程的初期是默认为nil,因此在构造过程的开始时期就会触发failure intializer。但实际情况是必须先给name一个初始值才能完成构造过程,因为根据安全检查1,首先要对本类新引进的存储属性初始化,然后安全检查2,才会调用父类构造器完成初始化,这样看来,可失败构造器都是在完成所有构造后才对。在xcode里也跑了一下,虽然上面的代码是可以执行的。但从运行效果上来看,应该是先完成了initialization,才进行的可失败判断。
Conversely,if you write a subclass initializer that matches a superclass convenience initializer,that superclass convenience initializer can never be called directly by your subclass,as per the rules described above in Initializer Delegation For Class Types.Therefore, your subclass is not (strictly speaking)providing an override of the superclass initializer.As a result,you do not write the
override
modifier when providing a matching implementation of a superclass convenience initializer.
对于这段描述,我个人的理解:
第一遍的理解:(后面有第二遍看到这里时的新理解)
子类的convenience initializer为什么在严格意义上没有提供一个对父类convenience initializer的override?因为父类的convenience initializer并不一定会被子类继承(只有在某种情况下,也就是子类实现了所有父类的designated initializer才会触发自动继承),所以当子类没有继承该方法,而你要给该方法加一个override是不对的。在继承了之后加override才是对的。这样的话就是说有时候是override了,有时候不是override了。那么swift就干脆说这个地方不用写override啦,就给大家省事了,你就不用去分析什么时候该写override,什么时候不该写override。编译器也省得去校验啦。故此而有了这句话。
第二次看到这里的时候又有的新的理解:如前面一句所说,superclass的convenience initializer并不是直接的被subclass掉用。仔细推敲了一下这句话。普通的方法继承可能是父类和子类里的那个方法实际上是同一个方法(即,地址空间是同一个)而自动继承过来的convenience initializer并不和父类是同一个地址空间,而是单独给子类开了一个地址空间,虽然函数体是一样的。这个猜想等到实际理解了编译器的override和方法继承原理之后再来确定。强烈感觉第二个解释是对的,第一个解释略勉强.