类的继承和构造过程

本页包含内容:

[TOC]

类里面的所有存储型属性——包括所有继承自父类的属性——都必须在构造过程中设置初始值。

Swift为类类型提供了两种构造器来确保实例中所有存储型属性都能获得初始值,分别是:

  • 指定构造器
  • 便利构造器

指定构造器和便利构造器

指定构造器是类中最主要的构造器。一个指定构造器将初始化类中提供的所有属性,并根据父类链往上调用父类合适的构造器来实现父类的初始化。

类倾向于拥有少量指定构造器,普遍的是一个类拥有一个指定构造器。指定构造器在初始化的地方通过“管道”将初始化过程持续到父类链。

每一个类都必须至少拥有一个指定构造器。在某些情况下,许多类通过继承父类中的指定构造器而满足了这个条件。

遍历构造器是类中比较次要的、辅助型的构造器。你可以定义便利构造器来调用一个类中的指定构造器,并为其参数提供默认值。你也可以定义便利构造器来创建一个特殊用途或特定输入值的实例。

你应当只在必要的时候为类提供便利构造器,比方说某种情况下通过使用便利构造器来快捷调用某个指定构造器,能够节省更多开发时间并让类的构造过程更加清晰明了。

指定构造器和便利构造器的语法

类的指定构造器写法跟值类型简单构造器一样:

init(parameters) {
    // statements
}

便利构造器也采用相同样式的写法,但需要在init关键字之前放置convenience关键字,并使用空格将它们俩分开:

convenience init(parameters) {
    // statements
}

类的构造器代理规则

为了简化指定构造器和便利构造器之间的调用关系,Swift采用了一下三条规则类限制构造器之间的代理调用:

  1. 指定构造器必须调用其直接父类的指定构造器。
  2. 便利构造器必须调用同类中定义的其它构造器。
  3. 便利构造器最后必须调用指定构造器。

一个更方便的记忆方法是:

  • 指定构造器必须总是向上代理。
  • 便利构造器必须总是横向代理。

这些规则可以通过下面的图例来说明:

[图片上传失败...(image-4e3b82-1520508470408)]

如图所示,父类中包含一个指定构造器和两个便利构造器。其中一个便利构造器调用了另外一个便利构造器,而后者又调用了唯一的指定构造器。这满足了上面提到的规则 2 和 3。这个父类没有自己的父类,所以规则 1 没有用到。

子类中包含两个指定构造器和一个便利构造器。便利构造器必须调用两个指定构造器中的任意一个,因为它只能调用同一个类里的其他构造器。这满足了上面提到的规则 2 和 3。而两个指定构造器必须调用父类中唯一的指定构造器,这满足了规则 1。

下面图例中展示了一种涉及四个类的更复杂的类层级结构。它演示了指定构造器是如何在类层级中充当“管道”的作用,在类的构造器链上简化了类之间的相互关系。

[图片上传失败...(image-2ff209-1520508470408)]

两段式构造过程

Swift中类的构造过程包含两个阶段:

  • 第一个阶段,类中的每个存储型属性赋一个初始值。当每个存储型属性的初始值被赋值后,第二阶段开始。
  • 第二阶段,它给每个类一次机会,在新实例准备使用之前进一步定制它们的存储型属性。

两段式构造过程的使用让构造过程更安全,同时在整个类层级结构中给予了每个类完全的灵活性。两段式构造过程可以防止属性值在初始化之前被访问,也可以防止属性被另外一个构造器意外地赋予不同的值。

注意:Swift 的两段式构造过程跟 Objective-C 中的构造过程类似。最主要的区别在于阶段 1,Objective-C 给每一个属性赋值 0 或空值(比如说0nil)。Swift 的构造流程则更加灵活,它允许你设置定制的初始值,并自如应对某些属性不能以 0nil 作为合法默认值的情况。

4种安全检查

Swift编译器将执行4种有效的安全检查,以确保两段式构造过程不出错地完成:

  1. 指定构造器必须保证它所在类的所有属性都必须先初始化完成,之后才能将其它构造任务向上代理给父类中的构造器。一个对象的内存只有在其所有存储型属性确定之后才能完全初始化。为了满足这一规则,指定构造器必须保证它所在类的属性在它往上代理之前先完成初始化。
  1. 指定构造器必须在为继承的属性设置新值之前向上代理调用父类构造器,如果没有这么做,指定构造器赋予的新值将被父类中的构造器所覆盖。
  2. 便利构造器必须为任意属性赋新值之前代理调用同一类中的其他构造器,如果没有这么做,便利构造器赋予的新值将被同一类中其它指定构造器所覆盖。
  3. 构造器在第一阶段完成之前,不能调用任何实例方法,不能读取任何实例属性的值,不能引用self作为一个值。类实例在第一阶段结束以前并不是完全有效的。只有在第一阶段完成后,该实例才会成为有效实例,才能访问属性和调用方法。

以下是两段式构造过程中基于上述安全检查的构造流程展示:

阶段1:

  • 某个指定构造器或遍历构造器被调用。
  • 完成新实例内存的分配,但此时内存还没有被初始化。
  • 指定构造器确保其所在类引入的所有存储型属性都已经赋值,存储型属性所属的内存完成初始化。
  • 指定构造器将调用父类的构造器,完成父类属性的初始化。
  • 这个调用父类构造器的过程沿着构造器链一直往上执行,直到到达构造器链的最顶部。
  • 当到达构造器链的最顶部,且已确保所有实例包含的存储型属性都已经赋值,这个实例的内存被认为已经完全初始化。此时阶段1完成。

阶段2:

  • 从顶部构造器链一直往下,每个构造器链中类的指定构造器都有机会进一步定制实例。构造器此时可以访问self、修改它的属性并调用实例方法等等。
  • 最终,任意构造器链中的遍历构造器可以有机会定制实例和使用self

下图展示了在假定的子类和父类之间的构造阶段1:

[图片上传失败...(image-8cb387-1520508470408)]

在这个例子中,构造过程从对子类中一个便利构造器的调用开始。这个便利构造器此时没法修改任何属性,它把构造任务代理给同一类中的指定构造器。

如安全检查1所示,指定构造器将确保所有子类的属性都有值。然后它将调用父类的指定构造器,并沿着构造器链一直往上完成父类的构造过程。

父类中的指定构造器确保所有父类的属性都有值。由于没有更多的父类需要初始化,也就无需继续向上代理。

一旦父类中所有属性都有了初始值,实例的内存被认为是完全初始化,阶段1完成。

以下展示了相同构造过程的阶段2:

[图片上传失败...(image-529f06-1520508470408)]

父类中的指定构造器现在有机会进一步来定制实例(尽管这不是必须的)。

一旦父类中的指定构造器完成调用,子类中的指定构造器可以执行更多的定制操作(这也不是必须的)。

最终,一旦子类的指定构造器完成调用,最开始被调用的遍历构造器可以执行更多的定制操作。

构造器的继承和重写

跟Objective-C中的子类不同,Swift中的子类默认情况下不会继承父类的构造器。Swift的这种机制可以防止一个父类的简单构造器被一个更精细的子类继承,并被错误地用来创建子类的实例。

假如你希望自定义的子类中能够提供一个或多个跟父类相同的构造器,你可以在子类中提供这些构造器的自定义实现。

当你在编写一个和父类中指定构造器时,你实际上是在重写父类的这个指定构造器。因此,你必须在定义子类构造器时带上override修饰符。即使你重写的是系统自动提供的默认构造器,也需要带上override修饰符。

相反,如果你编写了一个和父类遍历构造器相匹配的子类构造器,由于子类不能直接调用父类的便利构造器,因此,严格意义上来讲,你的子类并未对一个父类构造器提供重写。最后的结果就是,你在子类“重写”一个父类便利构造器时,不需要加override修饰符。

在下面的例子中定义了一个叫 Vehicle 的基类。基类中声明了一个存储型属性 numberOfWheels,它是默认值为 0Int 类型的存储型属性。numberOfWheels 属性用于创建名为 descrpiptionString 类型的计算型属性:

class Vehicle {
    var numberOfWheels = 0
    var description: String {
        return "\(numberOfWheels) wheel(s)"
    }
}

Vehicle 类只为存储型属性提供默认值,也没有提供自定义构造器。因此,它会自动获得一个默认构造器。自动获得的默认构造器总是类中的指定构造器,它可以用于创建numberOfWheels0Vehicle实例:

let vehicle = Vehicle()
print("Vehicle: \(vehicle.description)")
// Vehicle: 0 wheel(s)

下面的例子中定义了一个Vehicle的子类Bicycle

class Bicycle: Vehicle {
    override init() {
        super.init()
        numberOfWheels = 2
    }
}

子类Bicycle定义了一个自定义指定构造器init()。这个指定构造器和父类的指定构造器相匹配,所以Bicycle中的指定构造器需要带上override修饰符。

Bicycle的构造器init()以调 super.init()方法开始,这个方法的作用是调用Bicycle的父类Vehicle的默认构造器。这样可以确保Bicycle在修改属性之前,它所继承的属性numberOfWheels能被Vehicle类初始化。在调用super.init()之后,属性numberOfWheels的原值被新值 2 替换。

如果你创建了一个Bicycle实例,你可以调用继承的description计算型属性去查看属性numberOfWheels是否有改变:

let bicycle = Bicycle()
print("Bicycle: \(bicycle.description)")
// 打印 "Bicycle: 2 wheel(s)"

注意:子类可以在初始化时修改继承来的变量属性,但是不能修改继承来的常量属性。

构造器的自动继承

如上所述,子类在默认情况下不会继承父类的构造器。但是如果满足特定条件,父类构造器是可以被自动继承的。事实上,这意味着对于许多常见场景你不必重写父类的构造器,并且可以在安全的情况下以最小的代价继承父类的构造器。

假设你为子类中引入的所有新属性都提供了默认值,一下2个规则适用:

  1. 如果子类没有定义任何指定构造器,它将自动继承父类所有的指定构造器。
  2. 如果子类提供了所有父类指定构造器的实现 - - 无论是通过规则1继承过来的,还是提供了自定义实现 - - 它将自动继承父类所有的便利构造器。

即使你在子类中添加了更多的遍历构造器,这两条规则任然适用。

注意:对于规则2,子类可以将父类的指定构造器实现为便利构造器。

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

接下来的例子将在实践中展示指定构造器、遍历构造器以及构造器的自动继承。

这个例子定义了包含三个类:FoodRecipeIngredient以及ShoppingListItem的类层次结构,并将演示它们的构造器是如何相互作用的。

类层次中的基类是Food,它是一个简单的用来封装食物名字的类。Food类引入了一个叫做nameString类型的属性,并且提供了两个构造器来创建Food实例:

class Food {
    var name: String
    init(name: String) {
        self.name = name
    }
    convenience init() {
        self.init(name: "[Unnamed]")
    }
}

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

[图片上传失败...(image-c81f51-1520508470408)]

类类型没有默认的逐一成员构造器,所以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的子类RecipeIngredientRecipeIngredient类用来表示食谱中的一项原料。它引入了Int类型的属性quantity,并且定义了两个构造器来创建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类的构造链:

[图片上传失败...(image-a5036a-1520508470408)]

RecipeIngredient类拥有一个指定构造器init(name: String, quantity: Int),它可以用来填充RecipeIngredient实例的所有属性值。这个构造器一开始先将传入的quantity参数赋值给quantity属性,这个属性也是唯一在RecipeIngredient中新引入的属性。随后,构造器向上代理到父类Foddinit(name: String)。这个过程满足两段式构造中的安全检查1。

RecipeIngredient也定义了一个便利构造器init(name: String),它只通过name来创建RecipeIngredient的实例。这个便利构造器假设任意RecipeIngredient实例的quantity1,所以不需要显示指明数量即可创建出实例 。这个便利构造器的定义可以更加方便和快捷地创建实例,并且避免了创建多个quantity1RecipeIngredient实例时的重复代码。这个便利构造器只是简单地横向代理到类中的指定构造器,并为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,它的默认值是 falseShoppingListItem 还添加了一个计算型属性 description,它提供了关于 ShoppingListItem 实例的一些文字描述:

class ShoppingListItem: RecipeIngredient {
    var purchased = false
    var description: String {
        var output = "\(quantity) x \(name)"
        output += purchased ? " ✔" : " ✘"
        return output
    }
}

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

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

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

[图片上传失败...(image-faf8ca-1520508470408)]

你可以使用三个继承来的构造器来创建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的结构体,其中一个名为speciesString类型的常量属性。同时该结构体还定义了一个接受一个名为speciesString类型参数的可失败构造器。这个可失败的构造器检查传入的参数是否为一个空字符串。如果为空字符串,则构造失败、否则,sepcied属性被赋值,构造成功。

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"

枚举类型的可失败构造器

你可以通过一个带一个或多个参数的可失败构造器来获取枚举类型中特定的枚举成员。如果提供的参数无法匹配任何枚举成员,则构造失败。

下例中,定义了一个名为 TemperatureUnit 的枚举类型。其中包含了三个可能的枚举成员(KelvinCelsius,和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 的例子可以重写为:

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

构造失败的传递

类、结构体和枚举的可失败构造器可以横向代理到同类型中的其他可失败构造器。类似的,子类的可失败构造器也能向上代理到父类的可失败构造器。

无论是向上代理还是横向代理,如果你代理到其他可失败构造器触发构造失败,整个构造过程将立即终止,接下来的任何构造代码不会再被执行。

下面这个例子,定义了一个名为CartItemProduct类的子类。这个类建立了一个在线购物车中的物品的模型,它有一个名为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.init()
        } 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 的棋盘中进行的:

[图片上传失败...(image-5e833d-1520508470408)]

为了呈现这副游戏棋盘,Chessboard结构体定义了一个属性 boardColors,它是一个包含 64Bool值的数组。在数组中,值为 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))
// Prints "true"
print(board.squareIsBlackAt(row: 7, column: 7))
// Prints "false”
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容

  • 本章将会介绍 存储属性的初始赋值自定义构造过程默认构造器值类型的构造器代理类的继承和构造过程可失败构造器必要构造器...
    寒桥阅读 766评论 0 0
  • 构造过程是使用类、结构体或枚举类型的实例之前的准备过程。在新实例可用前必须执行这个过程,具体操作包括设置实例中每个...
    莽原奔马668阅读 680评论 0 3
  • 构造过程 构造过程是使用类、结构体或枚举类型的实例之前的准备过程。在新实例可用前必须执行这个过程,具体操作包括设置...
    蛊毒_阅读 723评论 0 2
  • " 做个入世的俗人,不悲不喜,小心安命,侧身修行。" 感恩节值得过分解读吗? “我用传统的方式庆祝感恩节。我要请所...
    法律界的小哥哥阅读 341评论 0 1
  • 首先去官网下载maven,然后解压,要配置JAVA_HOME,M2_HOME,最好加上MAVEN_HOME,mav...
    popli阅读 134评论 0 0