加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

The Swift Programming Language学习笔记(十五)——构造过程

发布时间:2020-12-14 07:15:47 所属栏目:百科 来源:网络整理
导读:构造过程 存储属性的初始赋值 构造器 默认属性值 自定义构造过程 参数的内部名称和外部名称 不带外部名的构造器参数 可选属性类型 构造过程中常量类型的修改 默认构造器 结构体的逐一成员构造器 值类型的构造器代理 类的继承和构造过程 指定构造器和便利构造
  • 构造过程
    • 存储属性的初始赋值
      • 构造器
      • 默认属性值
    • 自定义构造过程
      • 参数的内部名称和外部名称
      • 不带外部名的构造器参数
      • 可选属性类型
      • 构造过程中常量类型的修改
    • 默认构造器
      • 结构体的逐一成员构造器
    • 值类型的构造器代理
    • 类的继承和构造过程
      • 指定构造器和便利构造器
      • 指定构造器和便利构造器的语法
      • 类的构造器代理规则
      • 两段式构造过程
      • 构造器的继承和重写
      • 构造器的自动继承
      • 指定构造器和便利构造器实践
    • 可失败构造器
      • 枚举类型的可失败构造器
      • 带原始值的枚举类型的可失败构造器
      • 类的可失败构造器
      • 构造失败的传递
      • 重写一个可失败构造器
      • 可失败构造器init
    • 必要构造器
    • 通过闭包或者函数设置属性的默认值

构造过程

本章十分重要,文档原文配有大量的图解,具体请查阅官方文档。

构造过程是使用类、结构体或枚举类型的实例之前的准备过程。在新实例可用前必须执行这个过程,具体操作包括设置实例中每个存储型属性的初始值和执行其他必须的设置或初始化工作。

通过定义构造器(Initializers)来实现构造过程,这些构造器可以看做是用来创建特定类型新实例的特殊方法。与Objective-C中的构造器不同,Swift的构造器无需返回值,它们的主要任务是保证新实例在第一次使用前完成正确的初始化。

类的实例也可以通过定义析构器(deinitializer)在实例释放之前执行特定的清除工作。

存储属性的初始赋值

类和结构体在创建实例时,必须为所有存储型属性设置合适的初始值。存储型属性的值不能处于一个未知的状态。

可以在构造器中为存储型属性赋初值,也可以在定义属性时为其设置默认值。

当你为存储型属性设置默认值或者在构造器中为其赋值时,它们的值是被直接设置的,不会触发任何属性观察者(property observers)。

构造器

构造器在创建某个特定类型的新实例时被调用。它的最简形式类似于一个不带任何参数的实例方法,以关键字init命名。

可以在构造器中为存储型属性设置初始值。

struct Fahrenheit {
    var temperature: Double
    init() {
        temperature = 32.0
    }
}

let f = Fahrenheit()
print(f.temperature)

默认属性值

也可以在属性声明时为其设置默认值。

如果一个属性总是使用相同的初始值,那么为其设置一个默认值比每次都在构造器中赋值要好。两种方法的效果是一样的,只不过使用默认值让属性的初始化声明结合得更紧密。使用默认值能让你的构造器更简洁、更清晰,且能通过默认值自动推导出属性的类型;同时,它也能让你充分利用默认构造器、构造器继承等特性。

struct Fahrenheit {
    var temperature: Double = 32.0
}

let f = Fahrenheit()
print(f.temperature)

自定义构造过程

自定义构造过程时,可以在定义中提供构造参数,指定所需值的类型和名字。构造参数的功能和语法跟函数和方法的参数相同。

struct Celsius {
    var temperatureInCelsius: Double
    init(fromFahrenheit fahrenheit: Double) {
        temperatureInCelsius = (fahrenheit - 32.0) / 1.8
    }
    init(fromKelvin kelvin: Double) {
        temperatureInCelsius = kelvin - 273.15
    }
}

let c1 = Celsius(fromFahrenheit: 52.0)
print(c1.temperatureInCelsius)      // 11.2222222222111
let c2 = Celsius(fromKelvin: 300.0)
print(c2.temperatureInCelsius)      // 26.85

参数的内部名称和外部名称

跟函数和方法参数相同,构造参数也拥有一个在构造器内部使用的参数名字和一个在调用构造器时使用的外部参数名字。然而,构造器并不像函数和方法那样在括号前有一个可辨别的名字。因此在调用构造器时,主要通过构造器中的参数名和类型来确定应该被调用的构造器。正因为参数如此重要,如果你在定义构造器时没有提供参数的外部名字,Swift会为构造器的每个参数自动生成一个跟内部名字相同的外部名。

如果不通过外部参数名字传值,你是没法调用这个构造器的。只要构造器定义了某个外部参数名,你就必须使用它,忽略它将导致编译错误。

struct Color {
    let red,green,blue: Double    // 这些属性可以存储0.0到1.0之间的值,用来指示颜色中红、绿、蓝成分的含量。
    init(red: Double,green: Double,blue: Double) {
        self.red = red
        self.green = green
        self.blue = blue
    }
    init(white: Double) {
        self.red = white
        self.green = white
        self.blue = white
    }
}
let c1 = Color(red: 1.0,green: 0.0,blue: 1.0)
let c2 = Color(white: 0.5)
print(c1)   // Color(red: 1.0,green: 0.0,blue: 1.0)
print(c2)   // Color(red: 0.5,green: 0.5,blue: 0.5)
// let c3 = Color(1.0,0.0,1.0) // 如果不通过外部参数名字传值,你是没法调用这个构造器的。只要构造器定义了某个外部参数名,你就必须使用它,忽略它将导致编译错误:error: missing argument labels 'red:green:blue:' in call

不带外部名的构造器参数

如果不希望为构造器的某个参数提供外部名字,你可以使用下划线(_)来显式描述它的外部名,以此重写上面所说的默认行为。

struct Celsius {
    var temperatureInCelsius: Double
    init(fromFahrenheit fahrenheit: Double) {
        temperatureInCelsius = (fahrenheit - 32.0) / 1.8
    }
    init(fromKelvin kelvin: Double) {
        temperatureInCelsius = kelvin - 273.15
    }
    init(_ celisus: Double) {
        temperatureInCelsius = celisus
    }
}

let c1 = Celsius(fromFahrenheit: 52.0)
print(c1.temperatureInCelsius)      // 11.2222222222111
let c2 = Celsius(fromKelvin: 300.0)
print(c2.temperatureInCelsius)      // 26.85
let c3 = Celsius(100.0)
print(c3.temperatureInCelsius)      // 100.0

可选属性类型

如果你定制的类型包含一个逻辑上允许取值为空的存储型属性——无论是因为它无法在初始化时赋值,还是因为它在之后某个时间点可以赋值为空——你都需要将它定义为可选类型(optional type)。可选类型的属性将自动初始化为nil,表示这个属性是有意在初始化时设置为空的。

class SurveyQuestion {
    var question: String
    var answer: String?
    init(question: String) {
        self.question = question
    }
    func ask() {
        print(question)
    }
}
let s = SurveyQuestion(question: "D U LOVE ME?")
s.ask()

构造过程中常量类型的修改

可以在构造过程中的任意时间点给常量属性指定一个值,只要在构造过程结束时是一个确定的值。一旦常量属性被赋值,它将永远不可更改

注意,对于类的实例来说,它的常量属性只能在定义它的类的构造过程中修改,不能在子类中修改

class SurveyQuestion {
    let question: String    // 声明为常量,依然可在构造器中初始化。
    var answer: String?
    init(question: String) {
        self.question = question
    }
    func ask() {
        print(question)
    }
}
let s = SurveyQuestion(question: "D U LOVE ME?")
s.ask()

默认构造器

如果结构体或类的所有属性都有默认值,同时没有自定义的构造器,那么Swift会给这些结构体或类提供一个默认构造器(default initializers)。这个默认构造器将简单地创建一个所有属性值都设置为默认值的实例

class ShoppingListItem {
    var name: String?       // 下面指定默认值
    var quantity = 1
    var purchased = false
}
let s = ShoppingListItem()
print(s.name)       // nil
print(s.quantity)   // 1
print(s.purchased)  // false

结构体的逐一成员构造器

如果结构体没有提供自定义的构造器,它们将自动获得一个逐一成员构造器即使结构体的存储型属性没有默认值逐一成员构造器是用来初始化结构体新实例里成员属性的快捷方法。我们在调用逐一成员构造器时,通过与成员属性名相同的参数名进行传值来完成对成员属性的初始赋值。

struct Size {
    var width: Double = 0.0
    var height: Double = 0.0
}
let s = Size(width: 1.0,height: 2.0)    // 自动获得的逐一成员构造器:init(width:height:)
print(s)    // Size(width: 1.0,height: 2.0)

值类型的构造器代理

构造器可以通过调用其它构造器来完成实例的部分构造过程。这一过程称为构造器代理,它能减少多个构造器间的代码重复

构造器代理的实现规则和形式在值类型类类型中有所不同。值类型(结构体和枚举类型)不支持继承,所以构造器代理的过程相对简单,因为它们只能代理给自己的其它构造器。类则不同,它可以继承自其它类,这意味着类有责任保证其所有继承的存储型属性在构造时也能正确的初始化。

对于值类型,你可以使用self.init在自定义的构造器中引用类型中的其它构造器。并且你只能在构造器内部调用self.init

如果你为某个值类型定义了一个自定义的构造器,你将无法访问到默认构造器(如果是结构体,还将无法访问逐一成员构造器)。这个限制可以防止你为值类型定义了一个进行额外必要设置的复杂构造器之后,别人还是错误地使用了一个自动生成的构造器。

注意,假如你希望默认构造器、逐一成员构造器以及你自己的自定义构造器都能用来创建实例,可以将自定义的构造器写到扩展(extension)中,而不是写在值类型的原始定义中

struct Size {
    var width = 0.0,height = 0.0
}

struct Point {
    var x = 0.0,y = 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 a = center.x - size.width / 2
        let b = center.y - size.height / 2
        self = Rect(origin: Point(x: a,y: b),size: size)  // 代理给init(origin:size:)构造器来初始化
    }
}

let a = Rect()
print(a)    // Rect(origin: Point(x: 0.0,y: 0.0),size: Size(width: 0.0,height: 0.0))
let b = Rect(origin: Point(x: 1.0,y: 2.0),size: Size(width: 1920.0,height: 1080.0))
print(b)    // Rect(origin: Point(x: 1.0,y: 2.0),size: Size(width: 1920.0,height: 1080.0))
let c = Rect(center: Point(x: 1000.0,y: 2000.0),height: 1080.0))
print(c)    // Rect(origin: Point(x: 40.0,y: 1460.0),height: 1080.0))

在上面的代码中,如果你想用另外一种不需要自己定义init()init(origin:size:)的方式来实现这个例子,请使用扩展

类的继承和构造过程

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

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

指定构造器和便利构造器

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

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

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

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

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

类的指定构造器的写法跟值类型简单构造器一样,便利构造器也采用相同样式的写法,但需要在init关键字之前放置convenience关键字,并使用空格将它们俩分开。

init(parameters) { statements } convenience init(parameters) { statements }

类的构造器代理规则

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

  • 规则1——指定构造器必须调用其直接父类的的指定构造器。
  • 规则2——便利构造器必须调用同一类中定义的其它构造器。
  • 规则3——便利构造器必须最终导致一个指定构造器被调用。

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

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

两段式构造过程

Swift中类的构造过程包含两个阶段。第一个阶段,每个存储型属性被引入它们的类指定一个初始值。当每个存储型属性的初始值被确定后,第二阶段开始,它给每个类一次机会,在新实例准备使用之前进一步定制它们的存储型属性。

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

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

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

  • 安全检查1——指定构造器必须保证它所在类引入的所有属性都必须先初始化完成,之后才能将其它构造任务向上代理给父类中的构造器。如上所述,一个对象的内存只有在其所有存储型属性确定之后才能完全初始化。为了满足这一规则,指定构造器必须保证它所在类引入的属性在它往上代理之前先完成初始化

  • 安全检查2——指定构造器必须先向上代理调用父类构造器,然后再为继承的属性设置新值。如果没这么做,指定构造器赋予的新值将被父类中的构造器所覆盖。

  • 安全检查3——便利构造器必须先代理调用同一类中的其它构造器,然后再为任意属性赋新值。如果没这么做,便利构造器赋予的新值将被同一类中其它指定构造器所覆盖。

  • 安全检查4——构造器在第一阶段构造完成之前,不能调用任何实例方法,不能读取任何实例属性的值,不能引用self作为一个值

类实例在第一阶段结束以前并不是完全有效的。只有第一阶段完成后,该实例才会成为有效实例,才能访问属性和调用方法。

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

  • 阶段1

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

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

构造器的继承和重写

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

注意,父类的构造器仅会在安全和适当的情况下被继承。

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

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

正如重写属性,方法或者是下标,override修饰符会让编译器去检查父类中是否有相匹配的指定构造器,并验证构造器参数是否正确。

注意,当你重写一个父类的指定构造器时,你总是需要写override修饰符,即使你的子类将父类的指定构造器重写为了便利构造器
相反,如果你编写了一个和父类便利构造器相匹配的子类构造器,由于子类不能直接调用父类的便利构造器(每个规则都在上文类的构造器代理规则有所描述),因此,严格意义上来讲,你的子类并未对一个父类构造器提供重写。最后的结果就是,你在子类中“重写”一个父类便利构造器时,不需要加override前缀

自动获得的默认构造器总会是类中的指定构造器。

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

class Vehicle {
    var numberOfWheels = 0
    var description: String {
        return "(numberOfWheels) wheels"
    }
}

class Bicycle: Vehicle {
    override init() {
        super.init()    // 必须调用父类的指定构造器!这样可以确保Bicycle在修改属性之前,它所继承的属性numberOfWheels能被Vehicle类初始化!否则报错:error: use of 'self' in property access 'numberOfWheels' before super.init initializes self。
        numberOfWheels = 2
    }
}

let v = Vehicle()
print(v.description)
let b = Bicycle()
print(b.description)

构造器的自动继承

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

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

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

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

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

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

class Food {
    var name: String
    init(name: String) {    // 注意:类类型没有默认的逐一成员构造器,只好自定义一个类似的了。
        self.name = name
    }
    convenience init() {
        self.init(name: "[Unnamed]")    // 横向代理
    }
}

let f1 = Food(name: "Meat")
print(f1.name)      // Meat
let f2 = Food()
print(f2.name)      // [Unnnamed]

/** * 调味剂 */
class RecipeIngredient: Food {
    var quantity: Int
    init(name: String,quantity: Int) {
        self.quantity = quantity
        super.init(name: name)  // 向上代理。不写会报错:error: super.init isn't called before returning from initializer
    }
    // 尽管RecipeIngredient将父类的指定构造器重写为了便利构造器,它依然提供了父类的所有指定构造器的实现(作为指定构造器的默认构造器已经没了)。因此,RecipeIngredient会自动继承父类的所有便利构造器。
    override convenience init(name: String) {   // 这个便利构造器重写了父类的指定构造器
        self.init(name: name,quantity: 1)
    }
    // 此时,RecipeIngredient还有一个继承自父类Food的便利构造器init(),它会横向代理到RecipeIngredient版本的init(name: String)而不是Food提供的版本!
}

let r1 = RecipeIngredient()
print(r1.name,r1.quantity)     // [Unnamed] 1
let r2 = RecipeIngredient(name: "Bacon")
print(r2.name,r2.quantity)     // Bacon 1
let r3 = RecipeIngredient(name: "Eggs",quantity: 6)
print(r3.name,r3.quantity)     // Eggs 6

class ShoppingListItem: RecipeIngredient {  // 由于它为自己引入的所有属性都提供了默认值,并且自己没有定义任何构造器,ShoppingListItem将自动继承所有父类中的指定构造器和便利构造器。
    var purchased = false
    var description: String {
        return "(quantity) x (name) " + (purchased ? "?" : "?")
    }
}

var breakfastList = [
    ShoppingListItem(),ShoppingListItem(name: "Bacon"),ShoppingListItem(name: "Bacon",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 Bacon ? */

可失败构造器

如果一个类、结构体或枚举类型的对象,在构造过程中有可能失败,则为其定义一个可失败构造器。这里所指的“失败”是指,如给构造器传入无效的参数值,或缺少某种所需的外部资源,又或是不满足某种必要的条件等。

为了妥善处理这种构造过程中可能会失败的情况。你可以在一个类,结构体或是枚举类型的定义中,添加一个或多个可失败构造器。其语法为在init关键字后面添加问号(init?)。

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

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

注意,严格来说,构造器都不支持返回值。因为构造器本身的作用,只是为了确保对象能被正确构造。因此你只是用return nil表明可失败构造器构造失败,而不要用关键字return来表明构造成功

struct Animal {
    let species: String
    init?(species: String) {
        if species.isEmpty { return nil }
        self.species = species
    }
}

let a = Animal(species: "")
print(a)    // nil

上面的代码中,空字符串(如"")和一个值为nil的可选类型的字符串是两个完全不同的概念。上面的空字符串("")其实是一个有效的,非可选类型的字符串。这里之所以让Animal的可失败构造器构造失败,只是因为对于Animal这个类的species属性来说,它更适合有一个具体的值,而不是空字符串。

枚举类型的可失败构造器

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

enum TemperatureUnit {
    case Kelvin,Celsius,Fahrenheit
    init?(symbol: String) {
        switch symbol {
        case "K":
            self = .Kelvin
        case "C":
            self = .Celsius
        case "F":
            self = .Fahrenheit
        default:
            return nil
        }
    }
}

let t = TemperatureUnit(symbol: "X")
print(t)    // nil

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

带原始值的枚举类型会自带一个可失败构造器init?(rawValue:),该可失败构造器有一个名为rawValue的参数,其类型和枚举类型的原始值类型一致,如果该参数的值能够和某个枚举成员的原始值匹配,则该构造器会构造相应的枚举成员,否则构造失败。

enum TemperatureUnit: Character {
    case Kelvin = "K",Celsius = "C",Fahrenheit = "F"
}

let a = TemperatureUnit(rawValue: "X")
print(a)    // nil

类的可失败构造器

值类型(也就是结构体或枚举)的可失败构造器,可以在构造过程中的任意时间点触发构造失败。比如在前面的例子中,结构体Animal的可失败构造器在构造过程一开始就触发了构造失败,甚至在species属性被初始化前。

而对类而言,可失败构造器只能在类引入的所有存储型属性被初始化后,以及构造器代理调用完成后,才能触发构造失败

注意:本文参照的文档翻译团队认为本小节英文原文表述有误,具体请查看原文。

下面的代码在类的可失败构造器中使用隐式解包可选类型来满足上述要求。

class Product {
    let name: String!   // 隐式解包可选类型,因为是let,且构造器中有属性初始化语句,所以此处不会赋默认值,必须等构造器初始化属性后,才能在构造器中返回nil
    init?(name: String) {
        self.name = name    // 必须先初始化,之后才能触发构造失败!如果把这句放在下面一行,报错:error: variable 'self.name' used before being initialized
        if name.isEmpty { return nil }
    }
}

let p1 = Product(name: "Book")
print(p1)           // Optional(Product)
print(p1?.name)     // Optional("Book")
let p2 = Product(name: "")
print(p2)           // nil
print(p2?.name)     // nil

if let p = Product(name: "Car") {
    print(p.name)   // Car。注意此处不需要检测name属性是否是nil,因为一定是非nil的。注意,要是把name属性声明为String?,此处打印Optional("Car")。
}

if let p = Product(name: "") {
    print(p.name)   // 没有打印!实例构造失败
}

print("==========")

class Product2 {
    var name: String!       // 声明为var
    init?(name: String) {
        if name.isEmpty { return nil }      // 声明为var时,可以放在前面,因为前面可以被赋值,不用等构造器中的属性初始化
        self.name = name
    }
}

let p3 = Product2(name: "Book")
print(p3)           // Optional(Product2)
print(p3?.name)     // Optional("Book")
let p4 = Product2(name: "")
print(p4)           // nil
print(p4?.name)     // nil

if let p = Product2(name: "Car") {
    print(p.name)   // Car
}

if let p = Product2(name: "") {
    print(p.name)   // 没有打印!
}

上面定义的Product类和之前的Animal结构体很相似。Product类有一个不能为空字符串("")的常量属性name。为了强制这个要求,Product类使用了可失败构造器确保这个属性的值不是空字符串后,才允许构造成功。

毕竟,Product是一个类而不是结构体,这意味着不同于AnimalProduct类的所有可失败构造器必须给name属性一个初始值,然后才能触发构造失败。

上面的例子中,Product类的name属性被定义为隐式解包可选字符串类型String!)。因为它是一个可选类型,所以它在构造过程中被赋值前,具有默认值nil(???)。这个默认值nil意味着Product类引入的所有存储型属性都有一个有效的初始值。因此,一旦传入一个空字符串,该可失败构造器可以在name属性被赋值前触发构造失败。

译者注

上面的示例代码和描述并不相符,根据描述,if name.isEmpty { return nil }这句代码应该在self.name = name之前,而这却会导致编译错误error: all stored properties of a class instance must be initialized before returning nil from an initializer,除非将let name: String!改为var name: String!

根据前面的介绍和实测,常量存储属性,只能被初始化一次。上面的代码,因为在构造器中有初始化常量属性name的过程(即self.name = name),所以在此之前不会被赋予默认值nil,而在构造器的初始化name过程之前,因为并不是所有存储型属性都被初始化,是不可以返回nil的,(所以return nil语句不能放前面?);而如果将name声明为变量存储属性,那么就可以被多次赋值,所以在执行构造器方法之前,就会获得默认值nil,和构造器中的再次赋值并不冲突,所以此时,if name.isEmpty { return nil }才可以放在构造器前面。英文原文在这里表述有误。

因为name属性是一个常量,所以一旦构造成功,name属性肯定有一个非nil的值。即使它被定义为隐式解包可选类型,也完全可以放心大胆地直接访问,而不用检查name属性的值是否为nil。(即,一旦Product实例能构造成功,那么实例里面的属性name一定不可能是nil,省略了判断属性是否是nil的代码,同样是安全的)

构造失败的传递

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

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

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

注意:本文参照的文档翻译团队认为本小节英文原文表述有误,具体请查看原文。

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 }
    }
}

let c1 = CartItem(name: "Book",quantity: 3)
print(c1)       // Optional(CartItem),返回可选类型
let c2 = CartItem(name: "Book",quantity: -2)
print(c2)       // nil
let c3 = CartItem(name: "",quantity: 1)
print(c3)       // nil
let c4 = CartItem(name: "",quantity: -12)
print(c4)       // nil

if let c = CartItem(name: "Book",quantity: 2) {
    print(c)            // CartItem。此处进行了可选绑定,不会返回可选类型
    print(c.quantity)   // 2
    print(c.name)       // Book
}
if let c = CartItem(name: "Book",quantity: -2) {
    print(c)            // 不会打印
}
if let c = CartItem(name: "",quantity: 1) {
    print(c)            // 不会打印
}
if let c = CartItem(name: "",quantity: -12) {
    print(c)            // 不会打印
}

Product类中的name属性类似,CartItem类中的quantity属性也是隐式解包可选类型。这意味着在构造过程中,该属性在被赋予特定的值之前能有一个默认的初始值nil(???)。

该可失败构造器以向上代理到父类的可失败构造器init(name:)开始。这满足了可失败构造器在触发构造失败前必须总是完成构造器代理调用这个条件。

如果由于name的值为空字符串而导致父类的可失败构造器构造失败,则CartIem类的整个构造过程都将立即失败,之后的构造代码将不会再被执行。如果父类构造成功,CartIem的可失败构造器会进一步验证quantity的值是否不小于1

译者注

上面的示例代码和描述也不相符,name属性在被赋予特定的值之前不能获得一个默认的初始值nil,并且根据描述,self.quantity = quantity这句代码应该放在最后一行(???),而这却会导致编译错误error: property 'self.quantity' not initialized at super.init call,除非将let quantity: Int!改为var quantity: Int!

重写一个可失败构造器

如同其它的构造器,你可以在子类中重写父类的可失败构造器。或者你也可以用子类的非可失败构造器重写一个父类的可失败构造器。这使你可以定义一个不会构造失败的子类,即使父类的构造器允许构造失败。

注意,当你用子类的非可失败构造器重写父类的可失败构造器时,向上代理到父类的可失败构造器的唯一方式是对父类的可失败构造器的返回值进行强制解包

注意,可以用非可失败构造器重写可失败构造器,但反过来却不行。

可以在子类的非可失败构造器中使用强制解包来调用父类的可失败构造器。

class Document {
    var name: String?   // 声明为String?,表示name属性的值必须为一个非空字符串或nil,但不能是一个空字符串
    init() {}
    init?(name: String) {
        self.name = name
        if name.isEmpty { return nil }      // 检测输入参数name!
    }
}

class AutomaticallyNamedDocument: Document {
    override init() {
        super.init()    // 先调用!否则报错,不管加不加self关键字:error: use of 'self' in property access 'name' before super.init initializes self
        self.name = "Untitled"
    }
    override init(name: String) {   // 因为子类用另一种方式处理了空字符串的情况,所以不再需要一个可失败构造器,因此子类用一个非可失败构造器代替了父类的可失败构造器。
        super.init()
        if name.isEmpty {   // 注意,必须判断输入参数的name是否是空字符串,而不是属性name!
            self.name = "Untitled"
        } else {
            self.name = name
        }
    }
}

class UntitledDocument: Document {
    override init() {
        super.init(name: "[Untitled]")!     // 在子类的非可失败构造器中使用强制解包来调用父类的可失败构造器。
    }
}

let u1 = UntitledDocument()
print(u1)   // UntitledDocument

class UntitledDocument2: Document {
    override init() {
        super.init(name: "")!
    }
}

// let u2 = UntitledDocument2() // fatal error: unexpectedly found nil while unwrapping an Optional value
// print(u2)

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

可失败构造器init!

通常来说我们通过在init关键字后添加问号的方式(init?)来定义一个可失败构造器,但你也可以通过在init后面添加惊叹号的方式来定义一个可失败构造器((init!)),该可失败构造器将会构建一个对应类型的隐式解包可选类型的对象

你可以在init?中代理到init!,反之亦然。你也可以用init?重写init!,反之亦然。你还可以用init代理到init!,不过,一旦init!构造失败,则会触发一个断言。

必要构造器

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

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

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

class A {
 required init() {
 print("A.init()")
 }
}

class B: A {
 required init() {
 // super.init()
 print("B.init()")
 // super.init() // 注意,默认的super.init()调用放在最后一行。
 }
}

class C: B {
 required init() {
 // super.init()
 print("C.init()")
 // super.init()
 }
}

print(A())
print("==========")
print(B())
print("==========")
print(C())
/*
A.init()
A ==========
B.init()
A.init()
B ==========
C.init()
B.init()
A.init()
C
*/

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

如果某个存储型属性的默认值需要一些定制或设置,你可以使用闭包或全局函数为其提供定制的默认值。每当某个属性所在类型的新实例被创建时,对应的闭包或函数会被调用,而它们的返回值会当做默认值赋值给这个属性。

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

闭包结尾的大括号后面接了一对空的小括号。这用来告诉Swift立即执行此闭包。如果你忽略了这对括号,相当于将闭包本身作为值赋值给了属性,而不是将闭包的返回值赋值给属性。

注意,如果你使用闭包来初始化属性,请记住在闭包执行时,实例的其它部分都还没有初始化。这意味着你不能在闭包里访问其它属性,即使这些属性有默认值。同样,你也不能使用隐式的self属性,或者调用任何实例方法

/** * 文档中的西洋跳棋游戏的棋盘。西洋跳棋游戏在一副黑白格交替的10x10的棋盘中进行。为了呈现这副游戏棋盘,Checkerboard结构体定义了一个属性boardColors,它是一个包含100个Bool值的数组。在数组中,值为true的元素表示一个黑格,值为false的元素表示一个白格。数组中第一个元素代表棋盘上左上角的格子(白格),最后一个元素代表棋盘上右下角的格子(白格),颜色交替出现。 */
struct Checkerboard {
    let boardColors: [Bool] = {     // 通过一个闭包来初始化并设置颜色值
        var tempBoard = [Bool]()    // 注意,不能使用self关键字,也就不能访问属性,只好创建临时变量,处理后return了
        var isBlack = false
        for i in 0..<10 {
            for j in 0..<10 {
                tempBoard.append(isBlack)
                isBlack = !isBlack
            }
            isBlack = !isBlack
        }
        return tempBoard
    }()

    func squareIsBlackAtRow(row: Int,column: Int) -> Bool {
        assert(row >= 0 && row < 10 && column >= 0 && column < 10)
        return boardColors[row * 10 + column]
    }
}

let c = Checkerboard()
print(c.squareIsBlackAtRow(0,column: 1))
print(c.squareIsBlackAtRow(9,column: 9))

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读