Swift学习笔记 (二十四) 构造过程(下)

指定构造器和便利构造器实践

接下来的例子将在实践中展示指定构造器、便利构造器以及构造器的自动继承。这个例子定义了包含三个类 Food 、 

RecipeIngredient 以及 ShoppingListItem 的层级结构,并将演示它们的构造器是如何相互作用的。

类层次中的基类是 Food ,它是一个简单的用来封装⻝物名字的类。 Food 类引⼊了一个叫做 name 的 String 类型的属性,并且

提供了两个构造器来创建 Food 实例:

class Food {

    var name: String

    init(name: String) {

        self.name = name

    }

    convenience init() {

        self.init(name: "[Unnamed]")

    }

}

下图中展示了 Food 的构造器链:

类类型没有默认的逐一成员构造器,所以 Food 类提供了一个接受单一参数 name 的指定构造器。这个构造器可以使用一个特定

的名字来创建新的 Food 实例:

let namedMeat = Food(name: "Bacon")

// namedMeat 的名字是 "Bacon"

Food 类中的构造器 init(name: String) 被定义为一个指定构造器,因为它能确保 Food 实例的所有存储型属性都被初始化。 Food 

类没有父类,所以 init(name: String) 构造器不需要调用 super.init() 来完成构造过程。

Food 类同样提供了一个没有参数的便利构造器 init() 。这个 init() 构造器为新⻝物提供了一个默认的占位名字,通过横向代理到

指定构造器 init(name: String) 并给参数 name 赋值为 [Unnamed] 来实现:

let mysteryMeat = Food()

// mysteryMeat 的名字是 [Unnamed]

层级中的第二个类是 Food 的子类 RecipeIngredient 。 RecipeIngredient 类用来表示⻝谱中的一项原料。它引入了 Int 类型的属

性 quantity (以及从 Food 继承过来的 name 属性),并且定义了两个构造器来创建 RecipeIngredient 实例:

class RecipeIngredient: Food {

    var quantity: Int

    init(name: String, quantity: Int) {

        self.quantity = quantity

        super.init(name: name)

    }

    override convenience init(name: String) {

        self.init(name: name, quantity: 1)

    }

}

下图中展示了 RecipeIngredient 类的构造器链:

RecipeIngredient 类拥有一个指定构造器 init(name: String, quantity: Int) ,它可以用来填充 RecipeIngredient 实例的所有属性

值。这个构造器一开始先将传入的 quantity 实参赋值给 quantity 属性,这个属性也是唯一在 RecipeIngredient 中新引入的属性。

随后,构造器向上代理到父类 Food 的 init(name: String) 。这个过程满足两段式构造过程中的安全检查 1。

RecipeIngredient 也定义了一个便利构造器 init(name: String) ,它只通过 name 来创建 RecipeIngredient 的实例。这个便利构造

器假设任意 RecipeIngredient 实例的 quantity 为 1 ,所以不需要显式的质量即可创建出实例。这个便利构造器的定义可以更加方

便和快捷地创建实例,并且避免了创建多个 quantity 为 1 的 RecipeIngredient 实例时的代码重复。这个便利构造器只是简单地横

向代理到类中的指定构造器,并为 quantity 参数传递 1 。

RecipeIngredient 的便利构造器 init(name: String) 使⽤了跟 Food 中指定构造器 init(name: String)相同的形参。由于这个便利构

造器重写了⽗类的指定构造器 init(name: String) ,因此必须在前面使用 override 修饰符(参⻅《构造器的继承和重写》)。

尽管 RecipeIngredient 将父类的指定构造器重写为了便利构造器,但是它依然提供了⽗类的所有指定构造器的实现。因此, 

RecipeIngredient 会自动继承父类的所有便利构造器。

在这个例子中, RecipeIngredient 的父类是 Food ,它有一个便利构造器 init() 。这个便利构造器会被 RecipeIngredient 继承。这

个继承版本的 init() 在功能上跟 Food 提供的版本是一样的,只是它会代理到 RecipeIngredient 版本的 init(name: String) ⽽不是 

Food 提供的版本。

所有的这三种构造器都可以用来创建新的 RecipeIngredient 实例:

let oneMysteryItem = RecipeIngredient()

let oneBacon = RecipeIngredient(name: "Bacon")

let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)

类层级中第三个也是最后一个类是 RecipeIngredient 的子类,叫做 ShoppingListItem 。这个类构建了购物单中出现的某一种食谱

原料。

购物单中的每一项总是从未购买状态开始的。为了呈现这一事实, ShoppingListItem 引⼊了一个 Boolean(布尔类型) 的属性 

purchased ,它的默认值是 false 。 ShoppingListItem 还添加了一个计算型属性 description , 它提供了关于 ShoppingListItem 

实例的一些⽂字描述:

class ShoppingListItem: RecipeIngredient {

    var purchased = false

    var description: String {

        var output = "\(quantity) x \(name)"

        output += purchased ? " ✔" : " ✘"

        return output

    }

}

注意

ShoppingListItem 没有定义构造器来为 purchased 提供初始值,因为添加到购物单的物品的初始状态总是未购买。

因为它为⾃己引入的所有属性都提供了默认值,并且自己没有定义任何构造器,ShoppingListItem 将自动继承所有父类中的指定

构造器和便利构造器。

下图展示了这三个类的构造器链:

你可以使用三个继承来的构造器来创建 ShoppingListItem 的新实例:

var breakfastList = [

    ShoppingListItem(),

    ShoppingListItem(name: "Bacon"),

    ShoppingListItem(name: "Eggs", quantity: 6),

]

breakfastList[0].name = "Orange juice"

breakfastList[0].purchased = true

for item in breakfastList {

    print(item.description)

}

// 1 x orange juice ✔

// 1 x bacon ✘

// 6 x eggs ✘

如上所述,例子中通过字⾯量方式创建了一个数组 breakfastList ,它包含了三个 ShoppingListItem 实例,因此数组的类型也能

被自动推导为 [ShoppingListItem] 。在数组创建完之后,数组中第一个 ShoppingListItem 实例的名字从 [Unnamed] 更改为 

Orange juice ,并标记状态为已购买。打印数组中每个元素的描述显示了它们都已按照预期被赋值。

 

可失败构造器

有时,定义一个构造器可失败的类,结构体或者枚举是很有用的。这⾥所指的“失败” 指的是,如给构造器传⼊⽆效的形参,或缺

少某种所需的外部资源,又或是不满足某种必要的条件等。

为了妥善处理这种构造过程中可能会失败的情况。你可以在一个类,结构体或是枚举类型的定义中,添加一个或多个可失败构造

器。其语法为在 init 关键字后面添加问号( init? )。

注意

可失败构造器的参数名和参数类型,不能与其它非可失败构造器的参数名,及其参数类型相同。

可失败构造器会创建一个类型为自身类型的可选类型的对象。你通过 return nil 语句来表明可失败构造器在何种情况下应该 “失

败”。

注意

严格来说,构造器都不支持返回值。因为构造器本身的作用,只是为了确保对象能被正确构造。因此你只是用 return nil 表明可

失败构造器构造失败,⽽不要用关键字 return 来表明构造成功。

例如,实现针对数字类型转换的可失败构造器。确保数字类型之间的转换能保持精确的值,使用这个 init(exactly:)构造器。如果

类型转换不能保持值不变,则这个构造器构造失败。

let wholeNumber: Double = 12345.0

let pi = 3.14159

if let 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 属性被赋值,构造成功。

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”

如果你给该可失败构造器传入一个空字符串到形参 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 值找出合适的枚举成员的可失败构造器:

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.”

 

带原始值的枚举类型的可失败构造器

带原始值的枚举类型会自带一个可失败构造器 init?(rawValue:) ,该可失败构造器有一个合适的原始值类型的 rawValue 形参,

选择找到的相匹配的枚举成员,找不到则构造失败。

因此上面的 TemperatureUnit 的例子可以用原始值类型的 Character 和进阶的 init?(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.”

 

构造失败的传递

类、结构体、枚举的可失败构造器可以横向代理到它们⾃己其他的可失败构造器。类似的,子类的可失败构造器也能向上代理到父

类的可失败构造器。

⽆论是向上代理还是横向代理,如果你代理到的其他可失败构造器触发构造失败,整个构造过程将立即终止,接下来的任何构造代

码不会再被执行。

注意

可失败构造器也可以代理到其它的不可失败构造器。通过这种⽅式,你可以增加一个可能的失败状态到现有的构造过程中。

下⾯这个例子,定义了一个名为 CartItem 的 Product 类的子类。这个类建立了一个在线购物车中的物品的模型, 它有一个名为 

quantity 的常量存储型属性,并确保该属性的值至少为 1 :

class Product {

    let name: String

    init?(name: String) {

        if name.isEmpty { return nil }

        self.name = name

    }

}

class CartItem: Product {

    let quantity: Int

    init?(name: String, quantity: Int) {

        if quantity < 1 { return nil }

        self.quantity = quantity

        super.init(name: name)

    }

}

CartItem 可失败构造器首先验证接收的 quantity 值是否大于等于 1 。倘若 quantity 值无效,则立即终止整个构造过程,返回失败

结果,且不再执⾏余下代码。同样地, Product 的可失败构造器首先检查 name 值,假如 name 值为空字符串,则构造器立即执

⾏失败。

如果你通过传入一个非空字符串 name 以及一个值大于等于 1 的 quantity 来创建一个 CartItem 实例,那么构造方法能够成功被

执行:

if let twoSocks = CartItem(name: "sock", quantity: 2) {

    print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)")

}

// 打印“Item: sock, quantity: 2”

倘若你以一个值为 0 的 quantity 来创建一个 CartItem 实例,那么将导致 CartItem 构造器失败:

if let 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 的构造过程失败:

if let 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 ,

但不能是一个空字符串:

class Document {

    var name: String?

    // 该构造器器创建了了⼀一个 name 属性的值为 nil 的 document 实例例

    init() {}

    // 该构造器器创建了了⼀一个 name 属性的值为⾮非空字符串串的 document 实例例

    init?(name: String) {

        if name.isEmpty { return nil }

        self.name = name

    }

}

下⾯这个例子,定义了一个 Document 类的子类 AutomaticallyNamedDocument 。这个子类重写了所有父类引入的指定构造

器。这些重写确保了无论是使用 init() 构造器,还是使用 init(name:) 构造器,在没有名字或者形参传入空字符串时,⽣成的实例

例中的 name 属性总有初始值 "[Untitled]" :

class AutomaticallyNamedDocument: Document {

    override init() {

        super.init()

        self.name = "[Untitled]"
    }

    override init(name: String) {

        super.init()

        if name.isEmpty {

            self.name = "[Untitled]"

        } else {

            self.name = name

        }

    }

}

AutomaticallyNamedDocument ⽤一个不可失败构造器 init(name:) 重写了父类的可失败构造器 init? (name:) 。因为子类用另一种

方式处理了空字符串的情况,所以不再需要一个可失败构造器,因此子类用一个不可失败构造器代替了父类的可失败构造器。

你可以在子类的不可失败构造器中使用强制解包来调用⽗类的可失败构造器。⽐如,下⾯的 UntitledDocument 子类的 name 属

性的值总是 "[Untitled]" ,它在构造过程中使用了父类的可失败构造器 init?(name:) :

class UntitledDocument: Document {

    override init() {

        super.init(name: "[Untitled]")!

    }

}

在这个例子中,如果在调用父类的可失败构造器 init?(name:) 时传入的是空字符串,那么强制解包操作会引发运⾏时错误。不

过,因为这里是通过字符串常量来调用它,构造器不会失败,所以并不会发生运⾏时错误。

 

init! 可失败构造器

通常来说我们通过在 init 关键字后添加问号的⽅式 ( init? ) 来定义一个可失败构造器,但你也可以通过在 init 后⾯添加感叹号的

⽅式来定义一个可失败构造器 ( init! ) ,该可失败构造器将会构建一个对应类型的隐式解包可选类型的对象。

你可以在 init? 中代理到 init! ,反之亦然。你也可以用 init? 重写 init! ,反之亦然。你还可以用 init 代理到 init! ,不过,一旦 

init! 构造失败,则会触发一个断言。

 

必要构造器

在类的构造器前添加 required 修饰符表明所有该类的子类都必须实现该构造器:

class SomeClass {

    required init() {

        // 构造器的实现代码

    }

}

在子类重写父类的必要构造器时,必须在子类的构造器前也添加 required 修饰符,表明该构造器要求也应用于继承链后面的子

类。在重写父类中必要的指定构造器时,不需要添加 override 修饰符:

class SomeSubclass: SomeClass {

    required init() {

        // 构造器的实现代码

    }

}

注意

如果⼦类继承的构造器能满足必要构造器的要求,则无须在子类中显式提供必要构造器的实现。

 

通过闭包或函数设置属性的默认值

如果某个存储型属性的默认值需要一些自定义或设置,你可以使用闭包或全局函数为其提供定制的默认值。每当某个属性所在类型

的新实例被构造时,对应的闭包或函数会被调用,而它们的返回值会当做默认值赋值给这个属性。

这种类型的闭包或函数通常会创建一个跟属性类型相同的临时变量,然后修改它的值以满足预期的初始状态,最后返回这个临时变

量,作为属性的默认值。

下⾯模板介绍了如何用闭包为属性提供默认值:

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”

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值