Swift2.1-初始化

官方文档

初始化

Initialization是为准备使用类,结构体或者枚举实例的一个过程。这个过程涉及了在实例里的每一个存储属性设置一个初始值,以及在新实例准备使用之前执行任何其他所必须的设置或初始化。

你通过定义初始化器实现这次初始化过程,这是一个特殊的方法可以被调用来创建一个新的实例。不向Objective-C的初始化器,Swift初始化器没有返回值。这些初始化器主要的角色就是确保在第一次使用的时候,能过正确的创建一个新的实例。

类实例也可以实现一个反初始化器,在类的实例被释放之前,反初始化器会执行所有的自定义清理。更多关于反初始化器的信息,请看反初始化器

为存储属性设置初始化值

在一个类或结构体的实例被创建的时候,类和结构体必须为所有的存储属性设置一个合适的初始值。存储属性不能放在不确定的状态中。

你可以在初始化器里为存储属性设置一个初始值,或者通过分配一个默认的属性值作为属性定义的一部分。在下面会描述这些行为。


注意:

当你个一个存储属性分配默认值,或者在一个初始化器里设置它的初始值的时候,属性值就会被直接设置,不用调用任何属性监听者。


初始化器

初始化器被调用来创建一个新的特殊类型的实例。以最简单的形式,一个初始化器就像一个没有形式参数的实例方法,使用init关键字:

init() {
    //perform some initialization here
}

下面的🌰定义了一个名为Fahrenheit结构体,存储以华氏刻度(Fahrenheit scale)表示的温度。Fahrenheit结构体有一个Double类型的存储属性temperature:

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

这个结构体定了一个单一的没有形式参数的初始化器init,初始化存储温度的值为32(在华氏温度下水的冰点)。

默认的属性值

你可以在初始化器里为存储属性设置初始值。另外,指定一个默认属性值作为属性的声明。当属性被定义的时候,你可以通过为这个属性分配一个初始值来指定默认的属性值。


注意:

如果一个属性一直保持相同的初始值,可以提供一个默认值,而不是在初始化器里设置这个值。最终结果是一样的,但是默认值将属性的初始化更紧密地联系到它的声明。它使得你的初始化器更短更清晰,并且可以让你根据默认值推断属性的类型。默认值也让你利用默认初始化和初始化继承更容易。例如本章后面。


在属性声明这一点上,通过提供temperature属性的默认值,你可以把上面的Fahrenheit结构体写的更简单:

struct Fahrenheit {
    var temperature = 32.0
}

自定义初始化

你可以通过输入形式参数和可选(optional)类型自定义初始化过程,或者在初始化的时候分配常量属性,下面的部分将会描述。

初始化形式参数

你可以提供初始化形式参数作为初始化器的一部分,
来自定义初始化器中值的类型和名字。初始化形式参数有相同的功能和语法作为函数和方法的形式参数。

下面的🌰定义了一个名为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。第二个初始化器有一个初始化形式参数,并且也有一个外部变量名fromKelvin和一个局部变量名kelvin。这两个初始化器都把他们实际参数转换为了摄氏度并且把这个值存储到了名为temperatureInCelsius的属性里。

局部和外部形式参数名

与函数和方法形式参数一样,初始化形式参数也可以在初始化器内部有一个局部变量名来使用,然后当调用这个初始化器的时候,会使用外部变量名。

然而,如果初始化器的圆括号阻碍了函数和方法的执行,初始化器不能确定函数名。因此,一个初始化器的参数名称和类型在识别初始化器该调用的时候扮演了一个非常重要的角色。因为这个原因,如果你没有提供一个外部名,Swift在初始化的时候为每一个形式参数自动提供了一个外部名称。

下面的🌰定义了一个名为Color的结构体,有三个常量属性,分别为redgreenblue。这些属性存储了一个介于0。0-1。0之间的值,来表示红色,绿色和蓝色。

Color提供了一个初始化器,设置为Double类型的参数,命名为redgreenblueColor也提供了第二个初始化器,并带有一个white形式参数,用来给三个颜色属性设置为相同的值:

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)

需要注意的是,没有外部名称是不可能调用这些初始化器的。外部参数名如果被定义了,那么必须用在初始化器里,省略的话会报一个编译时(compile-tim)错误:

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

初始化器形式参数不用外部名称

如果你不想为初始化器形式参数使用外部名称,可以写一个下划线(_)为那个形式参数替代明确的外部名称,从而可以重写默认的行为。

这里有一个扩大的Celsius类的🌰,有一个额外的初始化器来创建一个新的Celsius类实例:

struct Celsius {
    var temperatureInCelsius:Double
    init(fromFahrengeit fahrenheit: Double) {
        temperatureInCelsius = (fahrenheit - 32.0) / 1.8
    }
    init(fromKevlvin 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值被调用了。

可选的属性类型

如果你的自定义类型有一个存储属性,逻辑上是允许"无值(no value)"的--大概因为它的值在初始化期间不能被设置,或者因为它以后会被允许可以"无值"--声明属性为可选类型。可选类型的属性自动地初始化为nil值,表示该属性在初始化期间故意设为还没有值(no value yet)

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

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 Stirng。当一个新的SurveyQuestion实例被初始化的时候,它会自动分配一个为nil的默认值,意味着"还没有字符串(no string yet)"。

在初始化期间分配常量属性

在初始化期间,你可以给常量属性分配一个值,只要等到初始化完成的时候,它就会被设置一个确定的值。一旦为一个常量属性分配了一个值,它就不能再被修改了。


注意:

对于类实例来说,一个常量属性在初始化期间只能通过类引用(introduces)来修改,它不能被子类修改。


在上面SurveyQuestion的例子,你可以给text使用一个常量属性来修改,而不是使用可变属性来表示,一旦SurveyQuestion的实例被创建了,那个问题将不会改变。尽管现在text属性是一个常量,但是它依然可以在类的初始化器里设置:

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

默认初始化器

Swift为所有的结构体或类提供了一个默认的初始化器。这个默认的初始化器给所有的属性提供了默认值。这个默认的初始化器简单地创建了一个新的实例,这个实例的所有属性都有一个默认值。

下面的🌰定义了一个名为ShoppingListItem的类,封装了name,quantity和purchase属性:

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

由于ShoppingListItem类的所有属性都有一个默认值,并且它是一个没有父类的基类,ShoppingListItem类自动地获得了一个默认的初始化器,创建了一个新的实例,使用默认值设置了它的所有属性(name属性是一个可选的String属性,所以它会自动设置一个为nil的默认值,尽管这个值没有写在代码里。)。上面的🌰给ShoppingListItem类使用了默认的初始化器,创建了一个新的实例,类的初始化器写作ShoppingListItem(),并且给这个实例分配了一个名为item的变量。

结构体类型的成员初始化器

如果没有定义任何自定义的初始化器,结构体类型会自动获得一个成员初始化器。不像默认的初始化器,结构体会接收一个成员初始化器,即使它的存储属性没有默认值。

这个成员初始化器是一个快速初始化新结构体实例成员属性的方式。新实例的属性初始值可以通过名称传递到成员初始化器里。

下面的🌰定义了一个名为Size的结构体,它有两个属性,分别是widthheight。这两个属性通过分配了默认值0。0,从而可以推断为Double类型。

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

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

值类型的初始化委托(代理)

初始化器可以调用其他初始化器来执行部分实例的初始化。这个过程,看做是初始化器委托(代理),避免了通过多个初始化器复制代码。

初始化器委托(代理)如何工作以及允许那些形式的委托(代理),这些规则对于值类型和类类型是不同的。值类型(包括结构体和枚举)不支持继承,所以他它们的初始化器委托(代理)的过程相当简单,因为它们只能提供它们自己为另一个初始化器委托(代理)。然而,类可以从其他类里继承,请看有关继承(Inheritance)的描述。这就意味着类有一些其他的任务(responsibilities)来确保它们继承的所有存储属性在初始化期间都分配了一个合适的值。这些任务描述在(类的继承和初始化)Class Inheritance and Initialization里。

对于值类型,当你写自己自定义的初始化器的时候,你可以使用self.init从相同的值类型里推断其他初始化器。不过,你只能从一个初始化器里调用self.init

注意,如果你为值类型定义了自定义的初始化器,你就不能访问那个类型默认的初始化器(或者结构体是成员初始化器)。这个限制防止初始化器提供的额外必要配置被别人意外地使用自动初始化器替代掉。


注意:

如果你想要使用默认的初始化器和成员初始化器以及你自定义的初始化器来初始化你的自定义值类型,你可以在扩展(extension)里写自定义初始化器而不是把值类型的原始实现作为它的一部分。想要了解更多的信息,请看扩展(Extensions)


下面的🌰定义了一个自定义的Rect结构体代表一个矩形。这个🌰需要两个结构体,分别是SizePoint,都为他们各自的属性提供了默认值0.0:

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

你可以初始化Rect结构体用下面三个方式中的任意一个--通过使用默认赋为0--初始化originsize属性值,通过提供一个具体的原点坐标和大小,或者提供一个具体的中心点和大小。通过三个自定义的初始化器代表这些初始化选项,下面是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(),和默认的初始化器有一样的功能,如果Rect没有自定义的初始化器,它将会使用这个初始化器。这个初始化器是空的,用一个大括号{}来表示,并且不会执行任何初始化。调用这个初始化器返回一个Rect实例,它的originsize属性都被初始化为默认值0.0:

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

第二个Rect初始化器init(origin:size)和成员初始化器功能相同,如果Rect没有自定义的初始化器,它将使用这个初始化器。这个初始化器简单给存储属性分配了originsize的实际参数值。这个初始化器把originsize的实际参数值分配了给了存储属性:

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:)略显复杂。它通过计算一个基于centersize值的原点。然后调用(或是委托/代理)init(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:)初始化器已经为它自己的属性分配了originsize的新值。然而,对于init(center:size:)初始化器可以更方便(更清楚)来利用现有的已经提供了准确功能的初始化器。


注意:

另一种是方法不定义init()init(origin:size:)初始化器,请看扩展Extensions


类继承和初始化

所有类的存储属性--包括从它的父类继承的任何属性--必须在初始化期间分配一个初始值。

Swift为类类型定义了两种初始化器,帮助确保所有的存储属性接收一个初始值。这些被称为指定初始化器和方便初始化器。

指定初始化器和便利初始化器

指定初始化器是类主要的初始化器。一个指定的初始化器可以初始化所有介绍那个类的属性,并且可以调用一个合适的父类初始化器来继续这个初始化过程给父类链。

类往往很少指定初始化器,并且一个类只有一个指定初始化器也是相当普遍的。指定初始化器是初始化开始并持续初始化过程到父类链的“传送”点。

每个类必须至少有一个指定的初始化器。在某些情况下,这就需要通过从父类的一个或更多的指定初始化器继承来满足这个要求,在自动初始化器继承(Automatic Initializer Inheritance)里有描述。

便利初始化器(Convenience initializers)是第二个,为一个类支持初始化器。你可以从相同的类里定义一个便利初始化器,来调用一个指定的初始化器作为便利初始化器,此便利初始化器将指定初始化器的形式参数设置为了默认值。你也可以为具体的使用情况或输入的值类型,定义一个便利初始化器,从而创建一个类的实例。

如果你的类不需要便利初始化器,那么你可以不用。创建便利初始化器时,常见初始化方式将会节省时间或者让类初始化的意图更清楚。

指定初始化器和便利初始化器的语法

用相同的方式来写类的指定初始化器作为值类型的简单初始化器:

init(parameters) {
    statements
}

便利初始化器有着相同的书写方式,但是要用convenience修饰符放到的init关键字前,用空格隔开:

convenience init(parameters) {
    statements
}

类类型的初始化器委托/代理

为了简化指定和便利初始化器之间的调用关系,Swift在初始化器之间的委托/代理调用有下面的三个规则:

规则 1
一个指定初始化器必须从它的直接父类调用一个指定初始化器。

规则 2
一个便利初始化器必须从相同的类里调用另一个初始化器。

规则 3
一个便利初始化器最终必须调用一个指定初始化器。

一个简单记忆的这些规则的方法如下:

  • 指定初始化器必须总是向上委托/代理。
  • 便利初始化器必须总是横向委托/代理。

下面图中表示了这些规则:


规则
规则

如图所示,父类包含一个指定初始化器和两个便利初始化器。一个便利初始化器调用另一个便利初始化器,而后者又调用了指定初始化器。这满足了规则2和规则3。所有的指定初始化器必须从父类调用指定初始化器,这就满足了规则1。


注意:

这些规则不会影响每个类创建实例的使用。任何上图的初始化器都可以用来完整创建对应类的实例。这个规则只在类的实现时有影响。


下图展示了更复杂的类的层级结构。它演示了指定初始化器是如何在此层级结构中充当"管道"作用。在类的初始化链上简化了类之间的内部关系:

两段式初始化器

Swift的类初始化器包含两个过程。在第一个阶段,通过引入类的初始化器为每一个存储属性分配了一个初始值。一旦每个存储属性的值确定后,第二个阶段就开始了,它给每个类一次机会在新的实例准备使用之前来定制它的存储属性。

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


注意:

Swift的两段式初始化过程和Objective-C的初始化相似。主要的不同是在第一阶段,Objective-C为每一个属性分配 0 或空值值(例如0或nil)。Swift的初始化过程更加灵活,它允许你设置自定义的初始值,并可以自如应对0或nil作为合法值的情况。


Swift编译器执行四种有效的安全检查来确保两段式初始化过程能够顺利完成:

安全检查 1

指定初始化器必须保证在向上委托父类初始化器之前,其所在类引入的所有属性都要初始化完成。

如上所述,一个对象的内存只有在其所有存储型属性确定之后才能完全初始化。为了满足这一规则,指定初始化器必须保证它所在类引入的属性在它往上代理之前先完成初始化。

安全检查 2

指定初始化器必须先向上代理调用父类初始化器,然后再为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖。

安全检查 3

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

安全检查 4

初始化器在第一阶段构造完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用self的值。

第一阶段结束的时候类的实例并不是完全合法的。属性只能被读取,方法也只能被调用,直到第一阶段结束的时候,这个类实例才被看做是合法的。

以下是两段初始化过程基于上述四种检查的流程:

阶段 1

  • 一个指定或便利初始化器被调用。
  • 类为这个新的实例分配内存。内存还没有被初始化。
  • 此类的指定初始化器确保所有由此类引入的存储属性都有一个值。现在这些存储属性的内存被初始化了。
  • 指定的初始化器调用父类的初始化器为其存储属性执行相同的任务。
  • 这个调用父类初始化器的过程将沿着初始化器链一直向上进行,直到到达初始化器链的最顶部。
  • 当到达了初始化器链最顶部,在链顶部的类确保所有的存储属性都有一个值,此实例的内存被认为完全初始化了,此时第一阶段完成。

阶段 2

  • 从顶部初始化器往下,链中的每一个指定初始化器都有机会进一步定制实例。初始化器现在能够访问self并且可以修改它的属性,调用它的实例方法,等等。
  • 最后,链中任何便利初始化器都有机会定制实例以及使用slef

下面展示了第一阶段假定的子类如何调用父类:


在这个例子,初始化过程从一个子类的便利初始化器开始。这个便利初始化器还不能修改任何属性。它通过同一个类的初始化器来实现代理。

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

父类的指定初始化器确保所有的父类属性都有值。由于没有更多的父类来初始化,也就不需要更多的代理。

一旦父类中所有属性都有初始值,它的内存就被认为完全初始化了,第一个过程完成。

下图是第二阶段相同的初始化过程:


现在父类的指定初始化器有机会来定制更多实例(尽管没有这种必要)。

一旦父类的指定初始化器完成了调用,子类的指定初始化器就可以执行额外的定制(同样,尽管没有这种必要)。

最后,一旦子类的指定初始化器完成调用,便利初始化器将会执行更多额外的定制操作。

初始化器的继承和重载

不像在Objective-C中的子类,Swift的子类不会默认继承父类的初始化器。Swift的这种机制防止了一个从父类简单的初始化器被一个更专业的子类初继承,并被用来错误的创建一个新的实例。


注意:

在某些情况是继承父类初始化器,但只有这样是安全且合适的时候。想要了解更多信息,请看Automatic Initializer Inheritance


如果你想自定义子类能实现一个或多个和父类相同的初始化器,你可以在子类中为那些初始化器提供定制的实现。

当你写了匹配父类指定初始化器的子类初始化器的时候,你可以重写那个初始化器。因此,在子类的初始化器定义之前,你必须写override修饰符。即使你重写了自动提供默认初始化器,请看描述Default Initializers

作为一个重写的属性,方法或下标脚本,override修饰符的出现提示Swift来检查父类是否有一个匹配的指定初始化器被重写,并且验证了你重写的初始化器已经指定了形式参数。


注意:

当重写父类指定初始化器时,你必须写override修饰符,尽管子类初始化器的实现是一个便利初始化器。


相反,如果你写了一个匹配父类便利初始化器的子类初始化器,父类的便利初始化器将不会在你的子类直接调用,相关描述请看类的初始化代理。因此,子类(严格的说)不会提供一个父类初始化器的重写。当提供一个匹配的父类便利初始化器的实现时,你不用谢override修饰符。

下面的例子定义了一个名为Vehicle的基类。基类声明了一个名为numberOfWheels的存储属性,有一个类型为Int的默认值0numberOfWheels属性通过一个名为description的计算属性来创建一个String类型车辆的字符串描述:

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

Vehicle类只为它的存储属性提供了一个默认值,并且没有提供自定义的初始化器。因此,它会自动收到一个默认初始化器。请看默认初始化器。默认初始化器(当可用的时候)总是一个类的指定初始化器,也可以被用来创建一个新的Vehicle实例,numberOfWheels默认为0:

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

下面的例子定义了一个名为Bicycle子类,继承自Vehicle:

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

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

Bicycle类的init()初始化器一开始调用super。init(),这个方法作用是调用父类的初始化器。这样可以确保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的时候,子类可以实现父类指定初始化器。


指定和便利初始化器的操作

下面的🌰展示了在操作中指定初始化器,便利初始化器和自动初始化器的继承。这个🌰定义了一个包含三个类,分别是FoodRecipeIngredientShoppingListItem,并演示了它们的继承关系是如何相互作用的。

在层次关系中的基类称为Food,一个简单的类,封装了食品的名称。Food类介绍了一个名为nameString类型的属性,以及提供了两个创建Food实例的初始化器:

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

下面的图表展示了类Food的初始化链:

类没有提供一个默认的逐一初始化器,所以Food类提供了一个接受单一实际参数的指定初始化器。这个初始化器可以使用一个具体的名称来创建一个新的Food实例:

let namedMeat = Food(name: "Bacon")
// namedMeat's name is "Bacon"

Foodinit(name: String)初始化器作为一个指定初始化器,因为它确保Food类实例的所有存储属性被完全初始化。Food类没有父类,所以它的init(name: String)初始化器不用调用super。init()来完成初始化

Food类也提供了没有实际参数的便利初始化器init()init()初始化器提供了一个默认的占位名字,通过代理调用同一类中定义的指定初始化器init(name: String)并给参数name传值[Unnamed]来实现:

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

类层级中的第二个类是Food的子类RecipeIngredientRecipeIngredient类模型构建了食谱中一个调味剂。它引入了Int类型的数量属性quantity(以及从Food继承过来的name属性),并且定义了两个初始化器来创建RecipeIngredient实例:

class RecipeIngredient: Food {
    var quartity: 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引入新属性。随后,将向上代理给父类Foodinit(name: String)初始化器。这个过程满足从两段式初始化的安全检查1。

RecipeIngredient类也定义一个便利初始化器init(name: String),它只通过name来创建RecipeIngredient的实例。这个便利初始化器假设任意RecipeIngredient实例的quantity值为1,所以不用致命数量就可以创建实例。便利初始化器的定义让RecipeIngredient更快,更方便地创建实例,并且当创建了多个quantity1的实例时,避免了代码重复。这个便利初始化器只是简单的把代理给了类的指定初始化器,传递了值为1quantity

RecipeIngredient类的init(name: String)便利初始化器使用了Food中指定初始化器init(name: String)相同的形式参数。因为便利初始化器从父类重写了一个指定初始化器,它必须使用override修饰符(在初始化器的继承和重写中描述)。

尽管RecipeIngredient提供了init(name: String)初始化器作为一个便利初始化器,但是RecipeIngredient类没有提供父类指定初始化器的实现。因此,RecipeIngredient类也自动继承了覅类所有的便利初始化器。

在这个🌰中,RecipeIngredient的父类是Food,它有一个init()便利初始化器。因此这个初始化器也被RecipeIngredient类继承。这个init()函数和Food提供的是一样的,除了它是将任务代理给RecipeIngredientinit(name: String)而不是Food

所有的这三种初始化器都可以用来创建新的RecipeIngredient实例:

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

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

在购物表里每一项都是从未购买(unpurchased)状态开始的。为了展现这一事实,ShoppingListItem引入了一个布尔类型的属性purchased,友谊默认值falseShoppingListItem也添加了一个计算属性description属性,它提供了关于ShoppingListItem实例的一些文字描述:

class ShoppingListItem: RecipeIngredient {
    var purchased = flase
    var desription: String {
        var output = "\(quantity) x \(name.lowercaseString)"
        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 ✘

如上所述,通过一个字面量的方式创建了包含三个ShoppingListItem实例的名为breakfastList的数组。[ShoppingListItem]被推断为数组类型。数组创建之后,数组第一个ShoppingListItemname"[Unnamed]"修改为Orange juice,并且它的purchased也标记为了true。然后通过遍历打印每个数组的描述,展示了它们的默认状态都按照预期被设置了。

可失败初始化器

定义一个类,结构体或枚举有事在初始化的时候会失败。这个失败可能由以下几种方式触发,包括给初始化传入无效的形式参数值,或缺少某种外部所需的资源,又或是不满足某种必要的条件等。

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


注意:

你不能定义一个可失败的和非可失败的初始化器有相同形式参数的类型和名称。


一个可失败的初始化器创建了一个可选类型的值。你通过return nil语句,来表明可失败初始化器在何种情况下“失败”。


注意:

严格来说,初始化器不会有返回值。相反,它们的角色是确保在初始化结束时,self能够被正确创建。虽然你写了return nil来触发初始化失败,但是你不能使用return关键字来表示初始化成功了。


下面的🌰定义了一个名为Animal的结构体,有一个名为speciesString类型的常量属性。Animal结构体也定义了一个可失败初始化器,有一个形式参数species。这个初始化器用来检查传入species的字符串是否为空。如果找到了一个空字符串,可失败初始化器被触发。否则,设置species属性的值,然后初始化成功:

struct Animal {
    let speices: Stirng
    init?(speices: 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")和值为nil的可选类型的字符串是两个不一样的的概念。上面的🌰中,空字符串("")是有效的,不是可选的String。然而,让Animal的可失败初始化器构建对象失败,只是因为对于Animal这个类的species属性来说,它更适合有一个具体的值,而不是空字符串。


枚举类型的可失败初始化器

你可以使用一个可失败初始化器来选择一个合适的带一个或多个形式参数的枚举类型。如果提供的形式参数没有匹配的条件,初始化器可能失败。

下面的🌰定义一个名为TemperatureUnit的枚举,有三种可能的状态(KelvinCelsiusFahrenheit)。一个被用来找到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.")
}
// 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."

带有原始值的枚举的可失败初始化器

带有原始值的枚举会自动获得一个可失败初始化器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.")
}
// 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."

类的可失败初始化器

一个值类型(结构体或枚举)的可失败初始化器任何时候在它的初始化实现的时候,可以触发初始化失败。在上面Animal结构体的🌰中,初始化器触发了一个初始化失败,甚至在species属性设置之前。

然而,对于类来说,一个可失败的初始化器只能在所有的类属性被初始化后和所有类之间的初始化器之间的代理调用发生完后,可以触发初始化失败。

下面的🌰展示了你如何使用隐式打开一个可选属性来满足来满足可失败初始化器的需要:

class Product {
    let name: Stirng!
    init?(name: String) {
        self.name = name
        if name.isEmpty { return nil }
    }
}

上面的Product类定义和Animal结构体很相似。Product类有一个String类型的常量属性name,不允许为空值。Product类使用了可失败初始化器来确保在初始化成功之前属性值是非空。

然而,Product是一个类,不是结构体。这就意味着,不像Animal,在触发初始化之前对于Product类必须为name属性提供一个初始值,任何可失败初始化器。

上面的🌰,Product类的name属性有一个隐式可打开的可选类型(String!)。由于它是一个可选类型,这就意味着name属性在初始化分配一个具体值之前,有一个为nil的默认值。nil的默认值反过来意味着由Product类引入的所有的属性都有一个有效的初始值。因此,Product类的可失败初始化器可以触发可视化失败。

因为name属性是一个常量,所以一旦Product类构造成功,name属性肯定有一个非nil的值。因此完全可以放心大胆的直接访问Product类的name属性,而不用考虑去检查name属性是否有值:

f let bowTie = Product(name: "bow tie") {
    // no need to check if bowTie.name == nil
    print("The product's name is \(bowTie.name)")
}
// prints "The product's name is bow tie"

构造失败的传递

允许在同一个类,结构体或枚举的可失败初始化器可以横向传递到另一个可失败初始化器。类似地,一个子类可失败初始化器可以向上代理到父类的可失败初始化器。

在其他的情况,如果你代理到另一个初始化器导致了初始化失败,那么整个初始化过程也会立即失败,并且之后任何初始化代码都不会执行。


注意:

可失败初始化器也可以代理其他的非可失败初始化器。通过这个方法,你可以为已有的初始化过程添加初始化失败的条件。


下面的🌰定义了Product类的子类CartItemCartItem类建立了一个在线购物车中的模型。CartItem引入了一个名为quantity存储的常量属性,并且确保了这个属性至少有一个值:

class CartItem: Product {
    let quantity: Int!
    init?(name: String, quantity: Int) {
        self.quantity = quantity
        super.init(name: name)
        if quantity < 1 { return nil }
    }
}

quantity属性有一个隐式展开的整型(Int!)。和Product类的name属性相似,这就意味着quantity属性在它被分配特定值之前有一个默认初始值nil

CartItem类的可失败初始化器通过向上代理父类Productinit(name:)初始化器。这满足了可失败初始化器在触发构造失败这个行为前必须总是执行构造代理调用这个条件。

如果一个父类由于空name值,而使得初始化失败,那么整个初始化过程会立即失败,并且初始化代码不在执行。如果父类初始化成功,CartItem类初始化会验证,它收到一个或多个quantity值。

如果你创建了一个CartItem实例,并且改实例name属性不为空,则初始化成功:

if let twoSocks = CartItem(name: "sock", quantity: 2) {
    print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)")
}
// prints "Item: sock, quantity: 2"

如果你创建了一个CartItem实例,quantity默认值为0CartItem初始化器会导致初始化失败:

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"

类似地,如果你创建了一个CartItem实例,并且令name为空值,那么父类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"

重写可失败初始化器

你可以在子类里重写父类的可失败初始化器。就好像其他的初始化器。或者,你可以用子类的非可失败初始化器来重写父类可失败初始化器。这样你就可以初始化子类不在失败,尽管父类的初始化允许失败。

注意,如果你用非可失败的子类初始化器重写了一个可失败初始化器时,子类的初始化器将不再能向上代理父类的可失败初始化器。一个非可失败的初始化器永远也不能代理调用一个可失败初始化器。


注意:

你可以用一个非可失败初始化器重写一个可失败初始化器,但反过来却行不通。


下面的🌰定义了一个名为Document的类。这个类中的name属性允许为nil和一个非空字符串,但不能是一个空字符串:

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

下面这个例子,定义了一个名为AutomaticallyNamedDocumentDocument类的子类。这个子类重写了基类的两个指定初始化器。确保了不论在何种情况下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:)初始化器。因为AutomaticallyNamedDocument类用不同的方式处理了空字符串的情况,它的初始化器不会失败,所以它提供了非可失败初始化器来代替。

你可以强制打开一个初始化器来从父类调用一个可失败的初始化器作为子类非可失败初始化器的一部分。例如,UntitledDocument子类将name复制为"[Untitled]",并且在初始化期间它使用了父类的可失败init(name:)初始化器:

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

这种情况,如果父类的init(name:)初始化器没有调用空的name,强制打开操作会导致运行时错误。然而,由于它调用了一个字符串常量,那么你可以看到初始化器不会失败,那样不会有运行时错误发生。

可失败初始化器init!

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

你可以在 init?初始化器中代理调用 init!初始化器,反之亦然。 你也可以用 init?重写 init!,反之亦然。 你还可以用 init代理调用init!,但这会触发一个断言:是否 init! 初始化器会触发构造失败?

必要初始化器

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

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

当子类重写基类的必要初始化器时,必须在子类的初始化器前同样添加required修饰符以确保当其它类继承该子类时,该初始化器同为必要初始化器。在重写基类的必要初始化器时,不需要添加override修饰符:

class SomeSubclass: SomeClass {
    required init() {
        // subclass implementation of the required initializer goes here
    }
}

注意:

如果子类继承的初始化器能满足必要初始化器的需求,则你无需显示的在子类中提供必要初始化器的实现。


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

如果某个存储属性的默认值需要自定义或设置,你可以使用闭包(closure)或全局函数来为属性提供默认值。当某个实例的属性被初始化的时候,闭包或函数就会被调用,并且它的返回值就会作为属性的默认值。

这种闭包或函数会创建一个和属性相同的临时值,来代表初始的状态,并且把这个临时值作为属性的默认值。

下面的代码展示闭包是如何让提供一个默认的属性值:

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属性,或者调用实例的方法。


下面的🌰定义了一个名为Checkerboard结构体,它表示跳棋游戏的棋盘:

跳棋游戏是在一个黑白交替的10×10的棋盘。为了呈现游戏的棋盘,Checkerboard结构体只有一个名为boardColors的属性,它是一个包含100个Bool值的数组。数组中的true代表黑色的格子,false代表白色的格子。数组中第一项代表棋盘的左上角,数组最后一项代表棋盘的右下角。

boardColors数组在一个闭包里被初始化,设置它的颜色值:

struc Checkboard {
    let boardColors: []Bool] = {
        var temporaryBoard = [Bool]()
        var isBlack = false
        for i in 1...10 {
            for j in 1...10 {
                temporaryBoard.append(isBlack)
                isBlack = !isBlack
            }
            isBlack = !isBlack
        }
        return temporaryBoa
    }()
    func squareIsBlackAtRow(row: Int, column: Int) ->Bool {
        return boardColors[(row * 10) + column]
    }
}

当创建了一个新的Checkboard时,闭包就会执行,并且boardColors的默认值就会倍计算和返回。上面🌰中的闭包为在一个名为temporaryBoard的临时数组中为每个方格计算并且设置了合适颜色,然后当设置完毕,就把这个临时数组作为闭包的返回值。返回的数组值存储在boardColors中,并通过squareIsBlackAtRow函数来查询:

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

推荐阅读更多精彩内容