可失败构造器
有时,定义一个构造器可失败的类,结构体或者枚举是很有用的。这里所指的“失败” 指的是,如给构造器传入无效的形参,或缺少某种所需的外部资源,又或是不满足某种必要的条件等。
为了妥善处理这种构造过程中可能会失败的情况。你可以在一个类,结构体或是枚举类型的定义中,添加一个或多个可失败构造器。其语法为在 init
关键字后面添加问号(init?
)。
注意
可失败构造器的参数名和参数类型,不能与其它非可失败构造器的参数名,及其参数类型相同。
可失败构造器会创建一个类型为自身类型的可选类型的对象。你通过 return nil
语句来表明可失败构造器在何种情况下应该 “失败”。
注意
严格来说,构造器都不支持返回值。因为构造器本身的作用,只是为了确保对象能被正确构造。因此你只是用
return nil
表明可失败构造器构造失败,而不要用关键字return
来表明构造成功。
例如,实现针对数字类型转换的可失败构造器。确保数字类型之间的转换能保持精确的值,使用这个 init(exactly:)
构造器。如果类型转换不能保持值不变,则这个构造器构造失败。
let wholeNumber: Double=12345.0 let pi =3.14159 iflet valueMaintained =Int(exactly: wholeNumber) { print("\(wholeNumber) conversion to Int maintains value of \(valueMaintained)") } // 打印“12345.0 conversion to Int maintains value of 12345” let valueChanged =Int(exactly: pi) // valueChanged 是 Int? 类型,不是 Int 类型 if valueChanged ==nil { print("\(pi) conversion to Int does not maintain value") } // 打印“3.14159 conversion to Int does not maintain value”
下例中,定义了一个名为 Animal
的结构体,其中有一个名为 species
的 String
类型的常量属性。同时该结构体还定义了一个接受一个名为 species
的 String
类型形参的可失败构造器。这个可失败构造器检查传入的species
值是否为一个空字符串。如果为空字符串,则构造失败。否则,species
属性被赋值,构造成功。
structAnimal { let species: String init?(species: String) { if species.isEmpty { returnnil } self.species = species } }
你可以通过该可失败构造器来尝试构建一个 Animal
的实例,并检查构造过程是否成功:
let someCreature =Animal(species:"Giraffe") // someCreature 的类型是 Animal? 而不是 Animal iflet giraffe = someCreature { print("An animal was initialized with a species of \(giraffe.species)") } // 打印“An animal was initialized with a species of Giraffe”
如果你给该可失败构造器传入一个空字符串到形参 species
,则会导致构造失败:
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”
注意
检查空字符串的值(如
""
,而不是"Giraffe"
)和检查值为nil
的可选类型的字符串是两个完全不同的概念。上例中的空字符串(""
)其实是一个有效的,非可选类型的字符串。这里我们之所以让Animal
的可失败构造器构造失败,只是因为对于Animal
这个类的species
属性来说,它更适合有一个具体的值,而不是空字符串。
枚举类型的可失败构造器
你可以通过一个带一个或多个形参的可失败构造器来获取枚举类型中特定的枚举成员。如果提供的形参无法匹配任何枚举成员,则构造失败。
下例中,定义了一个名为 TemperatureUnit
的枚举类型。其中包含了三个可能的枚举状态(Kelvin
、Celsius
和 Fahrenheit
),以及一个根据表示温度单位的 Character
值找出合适的枚举成员的可失败构造器:
enumTemperatureUnit { case Kelvin, Celsius, Fahrenheit init?(symbol: Character) { switch symbol { case"K": self = .Kelvin case"C": self = .Celsius case"F": self = .Fahrenheit default: returnnil } } }
你可以利用该可失败构造器在三个枚举成员中选择合适的枚举成员,当形参不能和任何枚举成员相匹配时,则构造失败:
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.”
带原始值的枚举类型的可失败构造器
带原始值的枚举类型会自带一个可失败构造器 init?(rawValue:)
,该可失败构造器有一个合适的原始值类型的 rawValue
形参,选择找到的相匹配的枚举成员,找不到则构造失败。
因此上面的 TemperatureUnit
的例子可以用原始值类型的 Character
和进阶的 init?(rawValue:)
构造器重写为:
enumTemperatureUnit: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.”
构造失败的传递
类、结构体、枚举的可失败构造器可以横向代理到它们自己其他的可失败构造器。类似的,子类的可失败构造器也能向上代理到父类的可失败构造器。
无论是向上代理还是横向代理,如果你代理到的其他可失败构造器触发构造失败,整个构造过程将立即终止,接下来的任何构造代码不会再被执行。
注意
可失败构造器也可以代理到其它的不可失败构造器。通过这种方式,你可以增加一个可能的失败状态到现有的构造过程中。
下面这个例子,定义了一个名为 CartItem
的 Product
类的子类。这个类建立了一个在线购物车中的物品的模型,它有一个名为 quantity
的常量存储型属性,并确保该属性的值至少为 1
:
classProduct { let name: String init?(name: String) { if name.isEmpty { returnnil } self.name = name } } classCartItem:Product { let quantity: Int init?(name: String, quantity: Int) { if quantity <1 { returnnil } self.quantity = quantity super.init(name: name) } }
CartItem
可失败构造器首先验证接收的 quantity
值是否大于等于 1 。倘若 quantity
值无效,则立即终止整个构造过程,返回失败结果,且不再执行余下代码。同样地,Product
的可失败构造器首先检查 name
值,假如 name
值为空字符串,则构造器立即执行失败。
如果你通过传入一个非空字符串 name
以及一个值大于等于 1 的 quantity
来创建一个 CartItem
实例,那么构造方法能够成功被执行:
iflet twoSocks =CartItem(name:"sock", quantity:2) { print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)") } // 打印“Item: sock, quantity: 2”
倘若你以一个值为 0 的 quantity
来创建一个 CartItem
实例,那么将导致 CartItem
构造器失败:
iflet zeroShirts =CartItem(name:"shirt", quantity:0) { print("Item: \(zeroShirts.name), quantity: \(zeroShirts.quantity)") } else { print("Unable to initialize zero shirts") } // 打印“Unable to initialize zero shirts”
同样地,如果你尝试传入一个值为空字符串的 name
来创建一个 CartItem
实例,那么将导致父类 Product
的构造过程失败:
iflet oneUnnamed =CartItem(name:"", quantity:1) { print("Item: \(oneUnnamed.name), quantity: \(oneUnnamed.quantity)") } else { print("Unable to initialize one unnamed product") } // 打印“Unable to initialize one unnamed product”
重写一个可失败构造器
如同其它的构造器,你可以在子类中重写父类的可失败构造器。或者你也可以用子类的非可失败构造器重写一个父类的可失败构造器。这使你可以定义一个不会构造失败的子类,即使父类的构造器允许构造失败。
注意,当你用子类的非可失败构造器重写父类的可失败构造器时,向上代理到父类的可失败构造器的唯一方式是对父类的可失败构造器的返回值进行强制解包。
注意
你可以用非可失败构造器重写可失败构造器,但反过来却不行。
下例定义了一个名为 Document
的类。这个类模拟一个文档并可以用 name
属性来构造,属性的值必须为一个非空字符串或 nil
,但不能是一个空字符串:
classDocument { var name: String? // 该构造器创建了一个 name 属性的值为 nil 的 document 实例 init() {} // 该构造器创建了一个 name 属性的值为非空字符串的 document 实例 init?(name: String) { if name.isEmpty { returnnil } self.name = name } }
下面这个例子,定义了一个 Document
类的子类 AutomaticallyNamedDocument
。这个子类重写了所有父类引入的指定构造器。这些重写确保了无论是使用 init()
构造器,还是使用 init(name:)
构造器,在没有名字或者形参传入空字符串时,生成的实例中的 name
属性总有初始值 "[Untitled]"
:
classAutomaticallyNamedDocument:Document { overrideinit() { super.init() self.name ="[Untitled]" } overrideinit(name: String) { super.init() if name.isEmpty { self.name ="[Untitled]" } else { self.name = name } } }
AutomaticallyNamedDocument
用一个不可失败构造器 init(name:)
重写了父类的可失败构造器 init?(name:)
。因为子类用另一种方式处理了空字符串的情况,所以不再需要一个可失败构造器,因此子类用一个不可失败构造器代替了父类的可失败构造器。
你可以在子类的不可失败构造器中使用强制解包来调用父类的可失败构造器。比如,下面的 UntitledDocument
子类的 name
属性的值总是 "[Untitled]"
,它在构造过程中使用了父类的可失败构造器 init?(name:)
:
classUntitledDocument:Document { overrideinit() { super.init(name:"[Untitled]")! } }
在这个例子中,如果在调用父类的可失败构造器 init?(name:)
时传入的是空字符串,那么强制解包操作会引发运行时错误。不过,因为这里是通过字符串常量来调用它,构造器不会失败,所以并不会发生运行时错误。
init! 可失败构造器
通常来说我们通过在 init
关键字后添加问号的方式(init?
)来定义一个可失败构造器,但你也可以通过在 init
后面添加感叹号的方式来定义一个可失败构造器(init!
),该可失败构造器将会构建一个对应类型的隐式解包可选类型的对象。
你可以在 init?
中代理到 init!
,反之亦然。你也可以用 init?
重写 init!
,反之亦然。你还可以用 init
代理到 init!
,不过,一旦 init!
构造失败,则会触发一个断言。
必要构造器
在类的构造器前添加 required
修饰符表明所有该类的子类都必须实现该构造器:
classSomeClass { requiredinit() { // 构造器的实现代码 } }
在子类重写父类的必要构造器时,必须在子类的构造器前也添加 required
修饰符,表明该构造器要求也应用于继承链后面的子类。在重写父类中必要的指定构造器时,不需要添加 override
修饰符:
classSomeSubclass:SomeClass { requiredinit() { // 构造器的实现代码 } }
注意
如果子类继承的构造器能满足必要构造器的要求,则无须在子类中显式提供必要构造器的实现。
通过闭包或函数设置属性的默认值
如果某个存储型属性的默认值需要一些自定义或设置,你可以使用闭包或全局函数为其提供定制的默认值。每当某个属性所在类型的新实例被构造时,对应的闭包或函数会被调用,而它们的返回值会当做默认值赋值给这个属性。
这种类型的闭包或函数通常会创建一个跟属性类型相同的临时变量,然后修改它的值以满足预期的初始状态,最后返回这个临时变量,作为属性的默认值。
下面模板介绍了如何用闭包为属性提供默认值:
class SomeClass { let someProperty: SomeType = { // 在这个闭包中给 someProperty 创建一个默认值 // someValue 必须和 SomeType 类型相同 return someValue }() }
注意闭包结尾的花括号后面接了一对空的小括号。这用来告诉 Swift 立即执行此闭包。如果你忽略了这对括号,相当于将闭包本身作为值赋值给了属性,而不是将闭包的返回值赋值给属性。
注意
如果你使用闭包来初始化属性,请记住在闭包执行时,实例的其它部分都还没有初始化。这意味着你不能在闭包里访问其它属性,即使这些属性有默认值。同样,你也不能使用隐式的
self
属性,或者调用任何实例方法。
下面例子中定义了一个结构体 Chessboard
,它构建了西洋跳棋游戏的棋盘,西洋跳棋游戏在一副黑白格交替的 8 x 8 的棋盘中进行的:
为了呈现这副游戏棋盘,Chessboard
结构体定义了一个属性 boardColors
,它是一个包含 64
个 Bool
值的数组。在数组中,值为 true
的元素表示一个黑格,值为 false
的元素表示一个白格。数组中第一个元素代表棋盘上左上角的格子,最后一个元素代表棋盘上右下角的格子。
boardColors
数组是通过一个闭包来初始化并设置颜色值的:
struct Chessboard { let boardColors: [Bool] = { var temporaryBoard: [Bool] = [] var isBlack = false for i in 1...8 { for j in 1...8 { temporaryBoard.append(isBlack) isBlack = !isBlack } isBlack = !isBlack } return temporaryBoard }() func squareIsBlackAt(row: Int, column: Int) -> Bool { return boardColors[(row * 8) + column] } }
每当一个新的 Chessboard
实例被创建时,赋值闭包则会被执行,boardColors
的默认值会被计算出来并返回。上面例子中描述的闭包将计算出棋盘中每个格子对应的颜色,并将这些值保存到一个临时数组 temporaryBoard
中,最后在构建完成时将此数组作为闭包返回值返回。这个返回的数组会保存到 boardColors
中,并可以通过工具函数 squareIsBlackAtRow
来查询:
let board = Chessboard() print(board.squareIsBlackAt(row: 0, column: 1)) // 打印“true” print(board.squareIsBlackAt(row: 7, column: 7)) // 打印“false”