初始化(Initialization)

2,345 阅读41分钟

原文

初始化时准备使用的类,结构体,枚举的实例的过程。这个过程包括为实例上的每个存储属性设置一个初始值并且执行一些其他的在新实例准备好使用之前必需的配置和初始化。

通过定义initializers实现初始过程,像可以调用来创建一个新的特定类型的实例的特殊方法。不想Objective-C,swift的初始化方法没有返回值。他们第一准则是确保一个类型的新的实例在他们第一次使用之前正确的初始化。

类类型的实例可以实现deinitializer,在那个类的实例释放之前执行自定义的清楚。更多关于deinitializers的信息,查看Deinitialization.

为存储属性设置初始化值(Setting Initial Values for Stored Properties)

类和结构体必需在这个类或者结构体的实例创建之前把全部的存储属性设置为合适的初始化值。

你可以在属性的定义中在一个初始化方法中或者通过分配一个默认的属性值来给一个存储属性设置一个初始化值。

当你给存储属性分配一个默认值的时候,或者在初始化方法中设置初始化值,属性的值时直接设置的,没有调用任何属性的观察者。

初始化方法(Initializers)

调用初始化方法来创建一个特殊类型的实例。在他最简单的形式中,初始化方法像灭有参数的方法实例,用init关键字写:

init() {
    // perform some initialization here
}

下面的例子定义了一个新的名为Fahrenheit的新结构体来存储用Fahrenheit温标表示的温度。Fahrenheit结构体有一个存储属性,temperature,是Double类型:

struct Fahrenheit {
    var temperature: Double
    init() {
        temperature = 32.0
    }
}
var f = Fahrenheit()
print("The default temperature is \(f.temperature)° Fahrenheit")
// Prints "The default temperature is 32.0° Fahrenheit"

结构体定义了一个简单的初始化方法,init,没有参数,使用32初始化存储属性temperature(在Fahrenheit中水的冰点)。

默认属性值(Default Property Values)

你可以在初始化方法中设置一个存储属性的初始化方法,像上面展示的。另外可以,在属性声明中指定一个默认的属性值。通过在定义它的时候给属性分配一个初始化值来指定一个默认的属性值。

如果属性一般使用相同的初始化值,在初始化方法中提供一个默认的值而不是设置一个值。最后的结果是一样的,但是默认值把属性的初始化和他的声明绑的更近了。使初始化更简短,清晰并且使你可以从默认值中推导属性的类型。默认初始值也使你更简单的使用默认初始化方法和初始化继承,在这章节后面描述。

可以把上面Fahrenheit结构体用更简单的形式写,通过在属性声明的时候为这个temperature属性提供一个默认的值:

struct Fahrenheit {
    var temperature = 32.0
}

自定义初始化方法(Customizing Inititalization)

你可以用输入的参数和可选的属性类型自定义初始化方法,或者在初始化过程中分配常量属性,像下面章节中描述的。

初始化参数(Initialization Parameters)

在初始化定义中提供初始化参数,来定义自定义初始化过程中值的类型和名称。初始化参数有和函数和方法参数一样的特点和语法。

下面的例子定义了一个名为Celsius的结构体,存储了用Celsius度表示的温度。Celsius结构体实现了两个名为init(fromFahrenheit:)和init(fromKelvin:)的两个自定义初始化方法,从一个不同的温度度标中的值来初始化一个结构体的新的实例:

struct Celsius {
    var temperatureInCelsius: Double
    init(fromFahrenheit fahrenheit: Double) {
        temperatureInCelsius = (fahrenheit - 32.0) / 1.8
    }
    init(fromKelvin kelvin: Double) {
        temperatureInCelsius = kelvin - 273.15
    }
}
let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)
// boilingPointOfWater.temperatureInCelsius is 100.0
let freezingPointOfWater = Celsius(fromKelvin: 273.15)
// freezingPointOfWater.temperatureInCelsius is 0.0

第一个初始化方法有一个简单的有fromFahrenheit参数标签的初始化参数和一个名为fahrenheit的参数。第二个初始化方法有一个名为简单的初始化参数哦raomKelvin和一个名为kelvin的参数形成。两个初始化把单个参数转换为对应的Celsius值并且把这个值存储在名为temperatureInCelsius属性中。

实参名称和形参标签(Parameter Names and Argument Labels)

像函数和方法的参数,初始化参数可以有在初始化方法体中使用的实参和调用初始化方法时使用的形参标签。

不过,初始化方法在括号前没有和函数与方法一样的明确的函数名称。所以,一个初始化方法的名字和类型在确定应该调用哪个初始化方法的时候扮演了一个特别重要的角色。因此,如果你没有提供swift为初始化方法中每一个参数提供了自动的形参标签。

下面的例子定义了一个名为color的结构体,有三个名为red,green和blue的常量属性。这些属性存储了一个0.0到1.0之间的值来指示color中red,green和blue的值。

Color为他的red,green和blue成员提供了一个有三个适当的Double类型的参数的初始化方法。Color也用简单的white参数提供给了第二个初始化方法,用来为全部三个color部分提供相同的值。

struct Color {
    let red, green, blue: Double
    init(red: Double, green: Double, blue: Double) {
        self.red   = red
        self.green = green
        self.blue  = blue
    }
    init(white: Double) {
        red   = white
        green = white
        blue  = white
    }
}

两个初始化方法都可以用来创建一个新的Color实例,通过给每一个初始化参数提供命名的值:

let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)
let halfGray = Color(white: 0.5)

注意不能不适用形参标签来调用这些初始化方法。如果他们定义了参数标签一定用在初始化方法中,忽略他们是一个编译时错误:

let veryGreen = Color(0.0, 1.0, 0.0)
// this reports a compile-time error - argument labels are required

没有参数标签的初始化参数(Initializer Parameters Without Argument Labels)

如果你不想在初始化参数使用参数标签,通过underscore代替那个参数的明确的参数标签来重写默认的表示。

这里是从上面Initialization Parameters中Celsius例子的扩展版本,用一个添加的初始化方法从一个已经在Celsius标度中存在的Double值来创建一个新的Celsius实例。

struct Celsius {
    var temperatureInCelsius: Double
    init(fromFahrenheit fahrenheit: Double) {
        temperatureInCelsius = (fahrenheit - 32.0) / 1.8
    }
    init(fromKelvin kelvin: Double) {
        temperatureInCelsius = kelvin - 273.15
    }
    init(_ celsius: Double) {
        temperatureInCelsius = celsius
    }
}
let bodyTemperature = Celsius(37.0)
// bodyTemperature.temperatureInCelsius is 37.0

初始化器调用Celsius(37.0)不需要参数标签就有清晰地目的。所以适合把这个初始化器写成init(_celsius:DOuble)让他可以通过提供一个未命名的Double值来被调用。

可选属性类型(Optional Property Types)

如果你的自定义类型有一个逻辑上允许没有值得存储属性--可能因为它的值可以在初始化时设置,或者因为允许他在后面的时候是“no value”--用一个可选类型声明属性。可选类型的属性用nil自动初始化,指明属性是有意的想在初始化时是“no value yet”的。

下面的例子定义了一个名为SurveyQuestion的类,用一个名为response的可选String属性:

class SurveyQuestion {
    var text: String
    var response: String?
    init(text: String) {
        self.text = text
    }
    func ask() {
        print(text)
    }
}
let cheeseQuestion = SurveyQuestion(text: "Do you like cheese?")
cheeseQuestion.ask()
// Prints "Do you like cheese?"
cheeseQuestion.response = "Yes, I do like cheese."

调查问题的反馈知道它被询问之前是不知道的,所以response属性用一个String?类型声明,或者“optional String”。它自动分配一个默认的值nil,意味着“no string yet”,当一个新的SurveyQuestion实例被初始化的时候。

在初始化的时候分配常量属性(Assigning Constant Properties During Initialization)

你可以在初始化的任何时候给常量属性分配一个值,只要在初始化结束之前设置为一个确切的值。一旦常量属性分配了值,它永远都不能修改了。

对于类实例,一个常量属性可以只能在初始化的时候被引入它的类修改。不能被一个子类修改。

你可以用上面的SurveyQuestion雷子来用一个常量属性而不是变量属性来表示问题的text属性,来指定一旦SurveyQuestion的实例被创建,问题不能修改。即使现在text属性是一个常量,他仍然可以在类的初始化器中设置:

class SurveyQuestion {
    let text: String
    var response: String?
    init(text: String) {
        self.text = text
    }
    func ask() {
        print(text)
    }
}
let beetsQuestion = SurveyQuestion(text: "How about beets?")
beetsQuestion.ask()
// Prints "How about beets?"
beetsQuestion.response = "I also like beets. (But not with cheese.)"

默认初始化器(Default Initializers)

swift为给他们全部的属性提供了默认值并且自己没有提供初始化器的类或者结构体的提供了默认的初始化器。默认初始化器简单的创建了一个新的全部的属性都设置为他们默认值的实例。

这个例子定义了一个名为ShoppiingLIstItem的类,把一个东西的名字,量,和可能状态放在购物列表中:

class ShoppingListItem {
    var name: String?
    var quantity = 1
    var purchased = false
}
var item = ShoppingListItem()

因为ShoppingListItem类型全部属性都有默认值,也因为它是一个没有父类的基础类,ShoppingListItem自动获取一个默认的创建一个全部属性都设置为默认值的新的实例的初始化器实现。(name属性是一个可选的String属性,它自动接受一个默认的值nil,即使这个值没有在代码中写。)上面的例子用初始化器语法使用ShoppingListItem类的默认初始化器来创建一个新的类的实例,写作ShoppingListItem(),并且给这个新的实例分配一个名为Item的变量。

结构体类型的成员构造器(Memberwise Initailizers for Structure Types)

结构体类型如果他们没有定义任何他们自定义的初始化器那么会自动接受一个成员初始化器。不想默认初始化器,结构体接受一个成员初始化器,即使他有一些没有默认值的存储属性。

成员初始化结构起是初始化新结构体实例的成员属性的简写方式。可以把新实例的属性的初始化值用名字传递成员初始化器。

下面的例子定义了一个有两个叫width和height的两个属性的名为Size的结构体。两个属性通过分配一个默认的值0.0推导为Double的类型。

Size结构体自动接收一个Init(width:height:)的成员初始化器,你可以用来初始化一个新的Size实例:

struct Size {
    var width = 0.0, height = 0.0
}
let twoByTwo = Size(width: 2.0, height: 2.0)

当你调用一个成员初始化器时,你可以忽略有默认值的属性的值。在上面的例子中,Size结构体对他的两个height和width属性有默认的值。你可以忽略一个或者两个属性,初始化器对你忽略的属性使用默认的值--例如:

let zeroByTwo = Size(height: 2.0)
print(zeroByTwo.width, zeroByTwo.height)
// Prints "0.0 2.0"

let zeroByZero = Size()
print(zeroByZero.width, zeroByZero.height)
// Prints "0.0 0.0"

值类型的初始化器代理(Initializer Delegation for Value Types)

初始化器可以调用其他初始化器来执行一个实例的初始化的一部分。这个过程,成为初始化代理,避免多个初始化器的重复代码。

初始化器代理如何工作的规则,什么形式的代理是允许的,值类型和类类型是不同的。值类型(structures和enumerations)不支持继承,所以他们的初始化代理过程相对简单,因为他们只可以用他们自己提供的初始化器做代理。类,无论怎样,可以从其他类继承,像在Inheritance中描述的。这意味着那些类有额外的职责来保证他们继承的全部存储属性在初始化的时候能被分配到一个合适的值。这些职责的描述在下面的Class Inheritance and Initialization

对于值类型,当你写自己的自定义初始化器时使用self.init来指向相同值类型的其他初始化器。你只能在初始化器中调用self.int。

注意如果你为值类型定义了一个自定义的初始化器,你就不能在访问那个类型的默认初始化值(或者成员初始化器,如果是一个结构体)。这个约束防止了一个情况,在更复杂的初始化器中提供的额外的基础配置意外的被默认用自动初始化器的人忽略掉。

如果你希望你的自定义值类型使用默认初始化器和成员初始化器来初始化,也能用你自己的自定义初始化器,在你的扩展中写自定义初始化器,而不是在类型的原始的实现中。更多的信息,查看Extensions

下面的例子定义了一个自定义Rect结构体来表示一个几何矩形。例子需要两个支持的名为Size和Point的结构体,他们两个都给他们全部的属性提供默认的值0.0:

struct Size {
    var width = 0.0, height = 0.0
}
struct Point {
    var x = 0.0, y = 0.0
}

你可以用下面三个方式之一来初始化Rect结构体--通过使用它的默认0初始化origin和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)
    }
}

第一个Rect初始化器,init(),功能和如果结构体没有自己的自定义初始化器时接受的默认的的初始化器一样。初始化器有一个空的主体,用空的一对花括号表示。调用这个初始化器返回一个origin和size属性都是用他们属性定义中的默认值Point(x:0.0,y:0.0)和Size(width:0.0,height:0.0)来初始化的Rect实例。

let basicRect = Rect()
// basicRect's origin is (0.0, 0.0) and its size is (0.0, 0.0)

第二个Rect初始化器,init(origin:size:),功能和如果结构体没有自己的自定义初始化器时接收到的成员初始化器一样。这个初始化器简单的把origin和size参数值分配给合适的存储属性:

let originRect = Rect(origin: Point(x: 2.0, y: 2.0),
                      size: Size(width: 5.0, height: 5.0))
// originRect's origin is (2.0, 2.0) and its size is (5.0, 5.0)

第三个Rect初始化器,init(center:size:),稍微更复杂。开始在一个center点和size值的基础上计算一个合适的原点。然后调用(或者代理)带init(origin:size:)初始化器,将新的origin和size的值存储在合适的属性中:

let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
                      size: Size(width: 3.0, height: 3.0))
// centerRect's origin is (2.5, 2.5) and its size is (3.0, 3.0)

初始化器init(center:size:)可以把新的origin和size的值分配给他自己合适的属性。不过,对于init(center:size:)初始化器来说使用已经存在的提供了那个功能的初始化器更方便。

关于另一种不用自己定义Init()和Init(origin:size:)初始化器来写这个例子的方式,看Extensions

类继承和初始化(Class Inheritance and Initialization)

一个类的全部存储属性--包括任何类从父类继承来的属性--必需要在初始化时分配一个初始化值。

swift为类类型定义了两种初始化器来帮助全部存储属性接受一个初始化值。这些就是设计初始化器和便利初始化器。

设计初始化器和便利初始化器(Designated Initializers and Convenience Initializers)

设计初始化器是一个类的主要初始化器。一个设计初始化器完全初始化类的全部属性并且调用合适的父类初始化器来向上父类链继续初始化过程。

类常常有非常少的设计初始化器,对类来说只有一个使非常同常的情况。设计初始化器发生的“漏斗”点,通过跟着父类链向上继续初始化过程。

每个类必需有至少一个初始化器。在一些情况下,这个需求通过从父类中继承一个或者两个来保证,如下面Automatic Initializer Inheritance描述的。

遍历初始化器是给类的第二个,支持初始化的初始化器。你可以定义一个遍历初始化器来调用一个和遍历构造器在一个类中的设计初始化器来吧设计构造器的参数设置为默认值。你也可以为了特殊的使用情况或者输入值类型来定义一个遍历构造器来创建一个这个类的实例。

如果你的类不需要他们,你不用提供遍历初始化器。当一个通常的初始化模式的便捷方式可以节约时间或者使类的初始化在目的上更明确的时候创建一个遍历初始化器。

设计和便利初始化器的语法(Syntax for Designated and Convenience Initializers)

类的设计初始化器和值类型的简单初始化器用一样的方式书写:

init(parameters) {
    statements
}

遍历构造器用一样的风格书写,但是把convenience修饰词放在init关键字前面,用一个空格分隔:

convenience init(parameters) {
    statements
}

类类型的初始化代理(Initializer Delegation for Class Types)

为了简化设计和便利初始化器之间的关系,swift在初始化器之间为代理调用用了下面三个原则:

原则1

  设计初始化器必需从他的直接父类中调用一个设计初始化器。

原则2

  遍历初始化器必需从相同的类中调用另一个初始化器。

原则3

  遍历初始化器必需最终调用一个设计初始化器。

一个简单的记住这些的方式:

  • 设计初始化器必需向上代理
  • 遍历初始化器必需穿过代理

这些原则在下面的图形中解释:


这里,父类有一个单一的设计初始化器和两个遍历初始化器。一个遍历初始化器调用另一个初始化器,它转而调用单一的设计初始化器。从上面这保证了原则2和3。父类没有更多的父类,所以原则1没有使用。

这个同种的子类有两个设计初始化器,和一个遍历初始化器。遍历初始化器必需调用两个设计初始化器之一,因为它必需调用相同类中的其他遍历器。在上面这确保了2和3.两个设计初始化器必需调用弗雷中的单一的设计初始化器,来确保上面的原则1.

这些原则没有影响你的类的使用者创建每个类的实例。上面图标的任何初始化器可以用来创建一个完全初始化的他们所属于的类的实例。原则只影响你如何写类初始化器的实现。

下面面的图标展示了一个四个类的更加复杂的类继承。他解释了在这个继承中的设计初始化器如何为类初始化执行“funnel”点,简化了链中类之间的内部关系:



两阶段初始化(Two-Phase Initialization)

swift中的类初始化是一个两阶段的过程。在第一个阶段,每个存储属性被引入它的类分配了一个初始化值。一旦每个存储属性的初始状态确定了,第二个阶段开始,在新的实例认为准备使用之前给每个类自定义它的存储属性的机会。

两阶段初始化过程的应用是初始化非常安全,同时给与了类层中每一个类完整的灵活性。两阶段初始化防止属性值在他们初始化之前被访问,防止属性值被另一个初始化器意外的设置为不同的值。

swift的两阶段初始化过程和Objective-C中的初始化相似。主要的不同在阶段1,Objective-C给每个属性分配了null或者0(例如0或者nil)。swift的初始化流更灵活,他让你可以设置自定义的初始值,可以处理0和nil不是合法的默认值的类型。

swift的编译器提供了四个有用的安全检查来确保两阶段初始化没有错误的完成:

安全检查1

  一个设计的初始化器必需确定在它代理到父类的初始化器之前全部由自己引入的属性都被初始    化。

像上面提到的,一个对象的内存只用当它的全部存储属性的状态都知道的时候才认为是完全初始化了。为了可以满足这个原则,设计初始化器必需确保他自己全部的属性在向链上传递之前初始化完了。

安全检查2

  一个设计初始化器必需在给他继承的属性分配值之前代理到它的父类初始化器中。如果没有这样,设计初始化器分配的新值将会被父类的初始化器重写。

安全检查3

  遍历初始化器一定要在给任何属性分配值之前代理到其他初始化器中(包括同一个类中定义的属性)。如果不这样,遍历初始化器分配的新值会被他的类的设计遍历器重写。

安全检查4

  一个初始化器不能调用任何任何实例方法,读取任何实例的属性,或者把self引用为一个值,知道初始化器的第一个阶段完成。

类的实例在第一个阶段结束前不是完全合法的。一旦类实例在第一阶段结束时才当做是有效的时才能访问属性,调用方法。

这里是两阶段初始化如何完成,在上面四个安全检查的基础之上:

阶段1

  • 一个设计或者遍历初始化器在类上被调用
  • 那个类的新实例的内存被分配。内存还没有初始化。
  • 一个类的设计初始化器确认被那个类引入的全部存储属性有一个值。这些存储属性的内存现在初始完了。
  • 设计初始化器处理父类初始化器来为他的存储属性执行一样的任务。
  • 这会按类继承链向上继续指导到达链的最顶部。
  • 一旦到达了类继承链的最顶部,链中最后一个类确保了全部的存储属性有一个值,实例的内存才认为是完全初始化,阶段1结束。

阶段2

  • 从链顶端向会进行,链中的每一个设计初始化器可以选择更多的自定义实例。初始化器现在可以访问self并且可以修改他的属性,调用实例方法,等等。
  • 最后,链中的任何遍历初始化器可以自定义实例和对self进行操作。

这里是对于一个假设地子类和父类的初始化调用的阶段1看起来什么样:


在这个例子中,初始化在子类中调用一个遍历初始化器开始。这个遍历初始化器不能修改任何属性。他代理到同一个类的一个设计初始化器。

设计初始化器确定子类全部的属性有一个值,像安全检查1.然后调用一个父类中的设计初始化器来向链上继续初始化。

父类的设计初始化器确保了所有父类的属性有值。没有更多的父类要初始化,所以不需要更多的代理。

只要全部的父类属性有值,它的内存认为完全初始化了。阶段1结束。

这里是同样的初始化调用的阶段2看起来的样子:


父类的设计初始化器现在有机会自定义实例(即使他不是必须要这样)。

一旦父类的设计初始化器结束,子类的设计初始化器可以执行额外的自定义(同样的,不是一样要这样做)。

最后,一旦子类的设计初始化器结束了,原先调用的遍历初始化器可以执行额外的自定义。

初始化器的继承和重写(Initializer Inheritance and Overriding)

不想Objective-C中的子类,swift的子类不需要默认继承父类的初始化器。swift的方案防止了父类中的简单初始化器被一个更特殊的子类继承而用来创建了一个不完整或者正确的初始化的子类的新的实例的情况,

父类初始化器在明确的环境下继承,但是只有当他是安全的而且适合这样做。更多的信息,查看下面的Automatic Initializer Inheritance

如果你想自定义子类来呈现一个或者多个和父类一样的初始化器,你可以在子类中提供一个这些初始化器的自定义的实现。

当你写了一个匹配一个父类的设计初始化器的子类初始化器的时候,你实际上提供了那个设计初始化器的一个重写。所以,你必须在子类的初始化器定义前写override修饰词。即使你重写了自动提供的默认初始化器也要这样做,像Default Initializers中描述的。

像一个重写的属性,方法或者下标,override修饰词的出现提示swift来检查父类有一个匹配的设计初始化器来重写,并且验证你重写的初始化器的参数是按需求一样指定的。

当重写父类的设计初始化器的时候你通常写override修饰词,即使你子类的初始化器的实现是一个遍历初始化器。

相反的,如果你写了一个子类的匹配父类遍历初始化器的初始化器,那个父类的初始化器可以不被你的子类直接调用,像上面Initializer Delegation for Class Types每条原则所表述的。所以,你的子类没有(严格来讲)提供父类初始化器的重写。结果就是,当提供一个父类的遍历初始化器的匹配实现时不写override修饰词。

下面的例子定义了一个名为Vehicle的基础类。基础类声明了一个名为numberOfWheels的存储属性,使用一个默认Int值0.numberOfWheels属性被名为description的计算属性用来创建一个String的vehicle的特性的描述:

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

如果一个子类的初始化器在初始化过程的阶段2没有执行自定义,父类有一个0参数的设计初始化器,你可以在给子类的全部存储属性分配值之后忽略调用super.init()。

这个例子定义了另一个Vehicle的子类,名为Hoverboard。在它的初始化器中,Hoverboard类只设置它的color属性。替代了明确的调用super.init,初始化器依靠隐式的调用它的父类的初始化器来完成过程。

class Hoverboard: Vehicle {
    var color: String
    init(color: String) {
        self.color = color
        // super.init() implicitly called here
    }
    override var description: String {
        return "\(super.description) in a beautiful \(color)"
    }
}

一个Hoverboard的实例使用Vehicle初始化器提供的wheels的默认数字。

let hoverboard = Hoverboard(color: "silver")
print("Hoverboard: \(hoverboard.description)")
// Hoverboard: 0 wheel(s) in a beautiful silver

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

自动初始化器继承(Automatic Initializer Inheritance)

像上面提到的,子类默认不继承父类的初始化器。不过,如果确定的条件满足父类的初始化器会自动被继承。实际中,这意味着在通常情况下你不需要写初始化器的重写,并且任何时候都是安全的来花费很小的努力而继承父类的初始化器。

假设在一个子类中你为拟引入的新的属性提供了默认的值,应用下面的两个原则:

原则1

  如果你的子类没有定义设计初始化器,自动继承他父类全部的设计初始化器

原则2

  如果你的子类提供了父类设计初始化器的全部实现--即使按原则1的方式继承的他们,或者在他自己的定义中提供了一个自定义的实现--他自动继承父类的全部遍历初始化器。

即使你的子类添加了更多的遍历初始化器这些原则也适用。

子类可以把父类设计初始化器的实现为子类便利初始化器来作为满足原则2的一部分

设计和便利初始化器使用(Designated and Convenience Initializers in Action)

下面的例子展示了设计初始化器,便利初始化器,和自动初始化器的继承。这个例子定义了一个名为Food,RecipeIngredient,和ShoppingListItem三个类的继承,并且解释了他们的初始化器的交互。

继承中的基础类名为Food,是一个简单的封装了一个食品名字的类。Food类引入了一个String名为name的属性并未创建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's name is "Bacon"

Food类中的init(name:String)初始化器是一个设计初始化器,因为它确保了一个新的Food实例的全部存储属性都初始化。Food类没有父类,所以init(name:String)初始化器不需要调用super.init()来完成他的初始化。

Food类也提供了一个便利初始化器,init(),没有参数。init()初始化器通过用[Unnamed]的name值代理到Food类的init(name:String)来微信的food提供一个默认参数占位名:

let mysteryMeat = Food()
// mysteryMeat's name is "[Unnamed]"

层级中的第二个类是Food的子类名为RecipeIngredient。类RecipeIngredient模型了做饭食谱中的原料。它引入了一个名为Quantity的Int属性(出去它从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)。这个过程确保了上面Two-Phase Initialization安全检查1。

RecipeIngredient也定义了一个便利初始化器,init(name:String),只通过name来创建RecipeIngredient实例。这个便利初始化器假设每个没有明确quantity的RecipeIngredient实例的quantity为1。这个便利初始化器的定义使REcipeIngredient实例更快更方便的来创建,避免在创建多个单quantityRecipeIngredient实例的时候重复代码。这个便利初始化器简单的代理到类的设计初始化器,传了一个值为1的quantity。

RecipeIngredient提供的init(name:String)便利初始化器和Food中init(name:String)设计初始化器采用一样的参数。因为这个便利初始化器重写了它父类中的设计初始化器,必须使用override修饰词标记(像在Initializer Inheritance and Overriding中描述的)。

即使recipleImg像个便利初始化器一样提供init(name:String),但是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类对出现在购物列表中的食谱原料进行建模。

在购物列表中的每一项开始是“unpurchased”。表示这个事实,ShoppingListItem引入一个名为purchase的布尔属性,有一个false的默认值。ShoppingListItem也增加了一个计算属性description,提供了一个ShoppingListItem实例的文本描述:

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

ShoppingListItem没有定义一个初始化器来给purchased提供一个初始值,因为购物列表中的项通常在开始的时候都是unpurchased。

因为它为它引入的全部属性提供了默认的值并且没有定义自己的初始化器,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 ✘

这里,由含有三个新的ShoppingListItem实例的字面数组创建了一个新的名为breakfastList的数组。数组的类型推导为[ShoppingListItem]。在数组创建之后,数组开始的ShoppingListItem的名字有[Unnamed]改为Orange juice并且标记为已经purchased。打印数组中每个对象的description显示他们默认的状态按期望的改变了。

可失败初始化器(Failable Initializers)

有时候定义一个初始化器可能失败的类,结构体,或者枚举是有用处的。这个失败可能由无效的初始化参数值触发的,必需外部数据的缺失,或者一些其他阻止初始化成功的情况。

要处理可能失败的初始化情况,在类,结构体,或者枚举定义中定义一个或者多个可失败的初始化器。通过在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)")
}
// Prints "12345.0 conversion to Int maintains value of 12345"

let valueChanged = Int(exactly: pi)
// valueChanged is of type Int?, not Int

if valueChanged == nil {
    print("\(pi) conversion to Int does not maintain value")
}
// Prints "3.14159 conversion to Int does not maintain value"

下面的例子定义了一个名为Animal的结构体,有一个名为species的String常量属性。Animal结构体也定义了一个接受一个单独名为species的参数的可能失败的初始化器。这个初始化器价差传给初始化器的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 is of type Animal?, not Animal

if let giraffe = someCreature {
    print("An animal was initialized with a species of \(giraffe.species)")
}
// Prints "An animal was initialized with a species of Giraffe"

如果你传了一个空的字符串值给可失败的初始化器的species参数,初始化器触发一个初始化失败:

let anonymousCreature = Animal(species: "")
// anonymousCreature is of type Animal?, not Animal

if anonymousCreature == nil {
    print("The anonymous creature could not be initialized")
}
// Prints "The anonymous creature could not be initialized"

检查空的字符串值(例如“”而不是“Giraffe”)和检查来指明可选String值的缺失不一样。在上面的例子中,空字符串是有效的,非可选String。不过,animal不适合拥有一个和他的species属性一样的空字符串。为了模型化这个限制,如果发现了空字符串可失败初始化器触发初始化失败。

枚举的可失败初始化器(Failable Initializer for Enumerations)

可以使用可失败的初始化器来在一个或者多个参数基础上选择一个合适的枚举情况。如果提供的参数没有匹配合适的枚举情况初始化器可能失败。

下面的例子定义了一个名为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
        }
    }
}

可以使用可失败的初始化器来为三个可能的状态选择一个合适的枚举case并且如果参数没有匹配三个状态之一的时候使初始化失败:

let fahrenheitUnit = TemperatureUnit(symbol: "F")
if fahrenheitUnit != nil {
    print("This is a defined temperature unit, so initialization succeeded.")
}
// Prints "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.")
}
// Prints "This is not a defined temperature unit, so initialization failed."

有RawValues枚举的可失败初始化器(Failable Initializers for Enumerations with Raw Values)

有raw values的枚举自动接收一个可失败的初始化器,init?(rawValue:),接受一个合适的raw-value类型的名为rawValue的参数并且如果发现了一个则选择一个匹配的枚举情况,或者如果没有匹配的值存在触发一个初始化失败。

你可以重写上面离得TemperatureUnit来使用Character类型的raw values并且使用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.")
}
// Prints "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.")
}
// Prints "This is not a defined temperature unit, so initialization failed."

失败初始化器的传播(Propagation of Initialization Failure)

一个类,结构体或者枚举的可失败的初始化器可以代理到同一个类,结构体或者枚举中的其他可失败的初始化器。相似的,一个子类可失败初始化器可以代理到父类可失败初始化器。

在这些情况中,如果你代理另外一个能导致失败的初始化,整个初始化过程立马失败,不再有初始化代码执行。

一个可失败的初始化器可以代理到不可失败的初始化器。如果你需要给已存在的不会失败的初始化过程增加潜在的失败状态使用这种方式。

下面的例子定义了一个名为CartItem的子类。CartItem类模型化了一个在线购物车中的对象。CartItem引入了存储的常量属性名为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)
    }
}

CaritItem的可失败初始化器开始验证它接受了一个1或者更大的quantity。如果quantity是无效的,整个初始化过程立即失败并且没有更多的初始化代码执行。同样的,Product的可失败的初始化器检查name值,并且如果name是空字符串初始化器过程立即失败。

如果你用一个非空的name和1或者大于1的quantity创建一个CartItem实例,初始化成功:

if let twoSocks = CartItem(name: "sock", quantity: 2) {
    print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)")
}
// Prints "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")
}
// Prints "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")
}
// Prints "Unable to initialize one unnamed product"

重写可失败初始化器(Overriding a Failable Initializer)

你可以在子类中重写父类的可失败初始化器,就像其他初始化器一样。或者,你可以用子类的不可失败初始化器来重写父类的可失败初始化器。这是你可以为初始化不能失败的子类,即使父类的初始化器是可以失败的。

注意如果你用子类的不可失败初始化器来重写一个可是白的父类初始化器,唯一代理到父类初始化器的方式是对父类可失败的初始化器进行强解。

你可以用不可失败的初始化器重写一个可失败的初始化器但是反过来不行。

下面的例子定义了一个名为Doucument的类。这个类模型化了一个文档,可以用一个非空字符串或者nil的name属性来初始化,但是不能用空字符串:

class Document {
    var name: String?
    // this initializer creates a document with a nil name value
    init() {}
    // this initializer creates a document with a nonempty name value
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

下面的例子定义了一个名为AutomaticallyNamedDocument的Document的子类。AutomaticallyNamedDocument子类重写了Document的两个设计初始化器。这些重写确保了AutomaticallyNamedDocument实例有一个初始化name值“[Untitled]”,如果实例没有name初始化,或者如果一个空字符串传给了init(name:)初始化器:

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:)初始化器。因为AutomaticallyNamedDocument用和他的父类不同的方式处理空字符串的情况,它的初始化器不需要失败,所以它提供了一个非失败版本的初始化器代替。

在初始化器中你可以使用强解包在子类的非失败初始化器实现中来调用一个父类中的可失败的初始化器。例如下面的UntitledDoucment子类通常命名为“[Untitled]”,并且在初始化时使用父类中的可失败初始化器init(name:)。

class UntitledDocument: Document {
    override init() {
        super.init(name: "[Untitled]")!
    }
}

这种情况,如果父类的init(name:)初始化器调用空字符串的name,强解包操作会导致运行时错误。不过,因为他用一个常量字符串调用,可以看到初始化器不会失败,所以没有运行时错误在这个时候发生。

init!可失败初始化器(The init! Failable Initializer)

通常通过在init关键字后面放置一个问号定义一个可失败的初始化器来创建一个适当类型的可选实例。或者,可以定义一个创建一个隐式解包了可选适当类型的实例初始化器。通过在init关键字后面替代问号放置一个感叹号实现。

可以从init?代理到init!,反之亦然,可以用init!重写init?,反之亦然。也可以从init代理到init!,如果iinit!初始化器引起了初始化错误会触发一个断言。

必需的初始化器(Required Initializers)

在类初始化器前些required修饰词来指定这个类的每个子类必需实现这个初始化器:

class SomeClass {
    required init() {
        // initializer implementation goes here
    }
}

在子类的必需初始化器实现之前也必需写required修饰词,来指明初始化器必需性在链中用于更多的子类。当重写一个必需的设计初始化器时不写override修饰词。

如果能用继承的初始化器满足必需性那你可以不用提供必需初始化器的明确实现。


用闭包或者函数设置一个默认的属性值(Setting a Defult Property Value with a Closure or Function)

如果一个存储属性的默认值需要一些自定义或者配置,你可以使用闭包或者全局函数来给属性提供一个自定义的默认值。不管任何时候包含属性的实例的初始化,闭包或者函数被调用,它的返回值作为属性的默认值分配给他。

这种闭包或者函数一般创建一个和属性一样类型的临时值,修改那个值来表示想要的初始状态,然后把临时值返回作为属性的默认值。

这里是闭包如何用来提供默认属性值的一个大概说明:

class SomeClass {
    let someProperty: SomeType = {
        // create a default value for someProperty inside this closure
        // someValue must be of the same type as SomeType
        return someValue
    }()
}

注意闭包最后的花括号后面跟着一对空的括号。这告诉swift立即执行闭包。如果你忽略了这些括号,你是把闭包分配给了属性,并不是闭包的返回值。

如果你使用一个闭包来初始化属性,记住实例剩下的部分在闭包执行的时候还没有初始化。这意味着你不能在闭包中访问任何其他属性值,即使这些属性有默认值。也不能使用隐式的self属性,后者调用任何实例方法。

下面的例子定义了一个名为Chessboard的结构体,模型了一个国际象棋的板子。国际象棋在8*8的板子上玩,有黑色和白色的方格。


要表示这个游戏板子,Chessboard结构体只有一个名为boardColors的属性,是一个64个布尔值的数组。数组中的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中并且可以用工具函数squareIsBlackAt(row:colum:)查询:

let board = Chessboard()
print(board.squareIsBlackAt(row: 0, column: 1))
// Prints "true"
print(board.squareIsBlackAt(row: 7, column: 7))
// Prints "false"