初始化
初始化是准备类、结构体或者枚举的一个实例供使用的过程。这个过程包括给实例上的所有存储属性初始值和实例在使用前必要的操作(setup or initialization)。
实现这个初始化过程通过定义一个构造方法(译者:initializers),构造方法看起来像特殊的方法,可以被调用来创建一个特定类型的新实例。不同于OC的构造方法,Swift的构造方法不返回值。构造方法的主要作用是在类型的实例被首次使用前,确保该实例被正确的初始化。
类的实例同样可以实现一个析构方法(译者:deinitializer),析构方法执行一些定制的清理工作,在该实例被释放前。更多的关于析构方法的信息,参见:Deinitialization。
给存储属性初始值
类和结构体的实例被创建时,它们所有的存储属性必须要被设置恰当的初始值。存储属性的状态不能是不明确的。
可以在构造方法中给存储属性设置初始值,也可以在属性定义时指定默认值。这些行为在接下来的内容会有具体描述。
NOTE
在给存储属性指定默认值、或者在构造方法中给它初始值的时候,数值是被直接设置的,任何属性的观察者都不会被调用。
初始化方法
初始化方法被调用来创建一个特定类型的新实例。初始化方法最简单的形式像一个没有参数的实例方法,使用init关键字定义:
init() { // perform some initialization here }
下面例子中定义了一个新的结构体叫做Fahrenheit来存储表现华氏温度计的刻度。Fahrenheit 结构体有一个Double类型存储属性叫做temperature:
struct Fahrenheit { var temperature: Double init() { temperature = 32.0 } } var f = Fahrenheit() println("The default temperature is \(f.temperature)° Fahrenheit") // prints "The default temperature is 32.0° Fahrenheit"
这个结构体定义了唯一一个构造方法:init,它没有参数,它里面给存储属性一个32.0(华氏温度的冰点表示)的初始值。
默认属性值(Default Property Values)
可以像上面一样在构造方法中给存储属性设置初始值。另外一种选择是在属性定义时指定一个默认属性值(Default Property Values)。通过在定义时指定一个初始值给出默认属性值。
NOTE
如果一个属性总是带有同样的初始值,提供默认属性值比在构造方法中设置值要好。虽然二者结果一样,但默认属性值将属性初始化和属性的定义联系的更加紧密。默认属性值使得初始过程更加简短和明晰,能够根据默认值推断出属性的类型。另外的好处是便于使用默认构造方法和构造方法继承的好处,这在后面的会讲到。
可以在结构体Fahrenheit的temperature定义时采用提供默认值的方式重写这个结构体:
struct Fahrenheit { var temperature = 32.0 }
自定义初始化
可以通过输入参数和可选类型或者修改常量属性的方式定制初始化过程,接下来是具体的描述。
初始化参数(Initialization Parameters)
可以将初始化参数作为构造方法定义的一部分,来定义定制的初始化过程需要的值的类型和名称。初始化参数和函数、方法的参数有同样的能力和语法。
下面的例子定义了一个叫做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
第一个构造方法有唯一一个初始化参数,这个初始化参数有一个外部名称forFahrenheit和一个本地名称叫做fahrenheit。第二个构造方法有唯一一个初始化参数,它有一个外部名称叫做fromKelvin和一个本地名称叫做kelvin。这两个构造方法都会将它们的参数转化为摄氏制式,并将这个值存储在一个叫做temperatureInCelsius的属性中。
本地和外部参数名
同函数和方法的参数一样,初始化参数可以同时拥有本地名称和外部名称,一个是在构造方法体内使用,一个是在构造方法被调用时使用。
然而,在圆括号前,构造方法没有函数和方法拥有的名称。 所以,构造方法参数的名称和类型扮演了一个非常重要的角色:用来区分调用的是哪个构造方法。因此在没有提供外部名称的情况下,Swift为每一个参数提供一个自动的外部名称。自动的外部名称和本地名称相同,就好像在每个初始化参数前使用了井号一样。
下面的例子定义了一个结构体叫做Color,它有三个常量属性叫做red、green、blue。这些属性存储从0.0到1.0的值来表示红、绿、蓝三种颜色的量。
Color提供了一个构造方法,拥有红、绿、蓝对应的三个Double类型参数。Color提供了第二个构造方法,它有唯一的一个参数叫做white,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)
要记住调用这些构造方法时不能不通过外部参数名称。外部名称一旦被定义就必须被使用,省略它们会导致编译时错误:
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(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 type)。可选类型的属性会自动被初始化为nil,表明在初始化阶段这些属性是可以没有值的。
下面的例子定义了一个叫做SurverQuestion的类,它有一个可选的String类型的属性叫做resopnse:
class SurveyQuestion { var text: String var response: String? init(text: String) { self.text = text } func ask() { println(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?类型,或者可选String。在一个新的surveyQuestion实例被初始化时,这个属性被自动的初始化为nil,意味着“还没有字符串内容”。
初始化过程中修改常量参数
可以修改常量属性的值在初始化的任何一点,直到初始化结束前它都可以被设置一个明确的值。
NOTE
对于类实例,它的常量属性只能定义它的在初始化阶段被修改,不能被子类修改。
可以修订上面的SurveyQuestion例子,使用常量属性而不是变量属性定义text属性,表明一旦SurveyQuestion实例被创建后,text属性就不能被修改了。尽管text属性现在是常量了,但它仍然可以在类构造方法中被修改:
class SurveyQuestion { let text: String var response: String? init(text: String) { self.text = text } func ask() { println(text) } } let beetsQuestion = SurveyQuestion(text: "How about beets?") beetsQuestion.ask() // prints "How about beets?" beetsQuestion.response = "I also like beets. (But not with cheese.)"
默认构造方法
Swift为结构体或者基本类提供默认的构造方法,给所有它的属性提供默认值但连一个构造方法都不提供。默认构造方法简单的创建了一个各个属性都被设置为默认值的实例。
这个例子定义了一个叫做ShoppingListItem的类,包括名称、数量和购买状态的购物列表:
class ShoppingListItem { var name: String? var quantity = 1 var purchased = false } var item = ShoppingListItem()
因为ShoppingListItem类的所有属性都有默认值,还因为这个类是一个基本类没有子类,ShoppingListItem 自动获得了一个默认的构造方法实现,创建了一个所有属性被设置为了默认值的实例。(name属性是一个可选String类型的,所以它会自动获得默认值nil,尽管它的值没有提供。)上面例子中通过初始化语法(写做:ShoppingListItem())使用了ShoppingListItem的默认构造方法,创建了这个类的一个实例,将这个新的实例赋值给了一个变量叫做item。
结构体类型的成员构造函数(Memberwise Initializers for Structure Types)
如果没有定制构造方法,结构体类型自动获取一个成员构造方法(Memberwise Initializers)。即使结构图的构造属性没有默认值也可以。
成员构造方法是给一个新的结构体实例初始化成员的简化方式。新实例属性的初始值可以传递给成员构造方法,通过名字。
下面的例子定义了一个叫做Size的结构体,它有两个属性叫做width和height。这两个属性可以被推测是Double类型的,因为它们都被赋了默认值0.0.
Size结构体自动获得一个init(width:height:)成员构造函数,可以使用它来初始化一个Size结构体的实例:
struct Size { var width = 0.0,height = 0.0 } let twoByTwo = Size(width: 2.0,height: 2.0)
值类型的构造方法代理(Initializer Delegation for Value Types)
构造方法可以调用其它构造方法来实现一个实例初始化的一部分。这个过程,叫做构造方法代理(initializer delegation),这样做避免了在多个构造方法中出现重复代码。
构造方法代理的工作规则和什么形式的代理是被允许的,这些对于值类型和类类型而言是不相同的。值类型(结构体和枚举)不支持继承,所以它们的构造方法代理相对简单,因为它们的构造方法只能代理给他们自身的其它构造方法。但是,类可以从其它类继承,就像Inheriatance描述的一样。这就意味着类有额外的功能,要确保在初始化阶段中它继承来的所有存储属性被赋予恰当的值。这些内容将在下面的Class Inheritance and Initialization 中描述。
对于值类型,在定制构造方法时,使用selft.init和对应的值类型来引用其它构造方法。只能在构造方法中调用self.init。
记住,如果给一个值类型定义了定制构造方法,那就不能再访问它的默认构造方法(或者是成员初始化方法,当值类型是结构体的时候)。这是为了避免意外使用默认构造方法,而没有使用提供了必要操作的稍微复杂的自定义构造方法。
NOTE
如果想让值类型的初始化采用默认构造方法和成员构造方法,同时也采用定制的构造方法,那么将自定义的构造方法写在一个扩展(entension)中而不是作为值类型的原始实现的一部分。更多的信息参见: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属性;使用明确的orignin和size;使用明确的中心点和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()和默认的构造方法功能一致,没有任何定制内容。这个构造方法的方法体是空的,用一对空的花括号{}表示,里面没有任何初始化的操作。调用这个构造方法将会返回一个Rect实例,依据它们的定义,这个实例的origin和size属性都将会被赋予默认值:Point(x: 0.0,y: 0.0) 和Size(width: 0.0,height: 0.0):
let basicRect = Rect() // basicRect's origin is (0.0,0.0) and its size is (0.0,0.0)
第二个构造方法: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)
第三个构造方法:init(center:size:),稍微复杂一些。开始它会根据center点和size的值计算原点的位置。然后它会调用(或者是委托)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:) 构造方法本来可以做到将新的origin和size值赋值给对应的属性。但是,对于init(center:size:)构造方法最方便(也是意图最明确的)是使用已经存在的构造方法,前提是后者已经提供了需要的功能。
NOTE
不定义init() 和init(origin:size:)构造方法,上面例子的另外的一种实现方式,详见Extensions。
类的继承和初始化
一个类的所有存储属性——包括全部从超类所有属性——必须在初始化过程中被给初始值。
对于类,Swift定义了两种类型的构造方法,来确保所有的存储属性获得一个初始值。它们就是指定构造方法(designated initializers)和便利构造方法(convenience initializers)。
指定构造方法和方便构造方法
指定构造方法是类的主要构造方法。指定构造方法初始化类的所有属性并且调用超类的一个适当的构造方法继续初始化过程,沿着超类链。
类偏向使用较少的指定构造方法,通常只有一个。指定构造方法是初始化发生的切入点,也是初始化过程沿着继承链继续的切入点。
每个类必须至少有一个指定构造方法。某些情况下,继承来自超类的一个或多个指定构造方法也是满足这个条件的,就像下面 Automatic Initializer Inheritance 描述的一样。
方便构造方法是给类提供的第二位的构造方法。可以定义一个方便构造方法来调用同一类的指定构造方法,使用指定构造方法的相同的参数设置,指定默认值。也可以定义一个方便构造方法来创建一个类的实例,来应对特定的使用场景或者输入的值类型。
不必要的情况下,可以不提供方便构造方法。创建构造方法的目的是简化普通的初始化模式或者使得类的初始化意图更明显。
指定构造方法和方便构造方法的语法
init(parameters) { statements }
方便构造方法的写法风格一样,但是要在init关键字前面添加convenience 修饰符,二者之间用一个空格分隔开:
convenience init(parameters) { statements }
类类型构造方法代理(Initializer Delegation for Class Types)
为了简化指定构造方法和方便构造方法之间的关系,Swift构造方法之间调用代理遵循以下三条规则:
规则1
一个指定构造方法必须调用它的直接集成超类的一个指定构造方法。
规则2
一个方便构造方法必须调用同一个类的另外一个构造方法。
规则3
一个方便构造方法必须最终调用一个指定构造方法。
记住这些的一个简单途径:
指定构造方法总是被委托方;
方便构造方法总是委托方。
这些规则体现在下面的图表中:
这里,超类拥有唯一一个指定构造方法和两个方便构造方法。一个方便构造方法调用了另外一个方便构造方法(这个构造方法本身调用了那个唯一的指定构造方法)。这满足上面所说的第2和第3条规则。因为这里的超类没有更上的超类了,所以第1条规则不适用它。
图中的子类有两个指定构造方法和一个方便构造方法。那个方便构造方法必须要调用两个指定方法中的一个,因为它只能调用同一个类的另外一个构造方法。这满足了上面第2和第3条规则。两个指定构造方法必须调用超类中唯一的那个指定构造方法,来满足前面的第1条规则。
NOTE
这些规则不影响类的使用者创建类的实例。上面图表中任意的构造方法都可以用来创建一个完全初始化的类实例。这些规则只影响怎么样写类的实现。
下面的图表展示了一个更加复杂的四个类的类继承关系。它展示了指定构造方法在继承层级中是如何作为一个切入点,进行类的初始化、简化类的继承链中的相互关系的:
初始化的两个阶段(Two-Phase Initialization)
在Swift中,类的初始化有两个阶段。第一个阶段,每个存储属性被赋予一个初始值。一旦每个存储属性的初始状态被确定下来,第二个阶段就开始了,每个类被给予了一个机会可以进一步的定制它的存储属性,在实例被使用前。
初始化的两个阶段使得初始化过程是安全的,同时给予了在类的继承关系中的每个类以灵活性。初始化的两个阶段阻止了属性的值在被初始化前就被访问,同时阻止了属性意外的被另一个构造方法赋值。
NOTE
Swift的初始化两阶段和OC中的初始化类似。主要的区别在第一阶段,OC会给每个属性赋值0或者null(就像0或者nil)。Swift的初始化更加灵活,可以设置自定义的初始值,也可以应对0或者nil不是合法默认值的类型。
Swift的编译器会做四项有用的安全检查,来确保初始化的两个阶段没有错误:
安全检查1
在将自己委托给超类构造方法之前,一个指定构造方法必须确保它自身定义的所有属性被初始化了。
像上面陈述的一样,只有当一个对象的所有存储属性初始状态都确定了才会为它分配内存。为了使这个规则被满足,一个指定构造方法必须确保在进入(初始化)链的下一个环节前所有它自己的属性都被初始化了。
安全检查2
在给继承来的属性赋值前,一个指定构造方法必须委托超类的构造方法。如果不这样做,指定构造方法的赋值会被超类的初始化方法覆盖掉。
安全检查3
在给任何属性(包括同一个类定义的属性)赋值前,一个方便构造方法必须委托另外一个构造方法。如果不这样做,方便构造方法的赋值会被它自身类的指定构造方法覆盖掉。
安全检查4
一个构造方法不能调用任何实例方法、读取任何实例属性,在初始化的第一阶段结束前不能使用self。
直到第一阶段结束,类的实例才是完全有效的。在第一阶段结束时,类实力是合法的了,属性只能被访问、方法只能被调用。
依据上面四条安全检查,这里展示了初始化两阶段是如何进行的:
阶段1
给那个类的一个实例分配了内存。那块内存还没有被初始化。
直到到达继承链的顶端,这个行为一直向上进行。
一旦到达继承链的顶端,而且最后一个雷确保自身的存储属性都有值了,这个实例的内存就被认为完成初始化了,第一阶段结束。
阶段2
- 从继承链顶端向下工作,链上的每个指定构造方法有进一步定义实例的选择。构造方法现在可以访问self,可以修改它的属性,可以调用他的实例方法,等等。
- 最终,链上的任意一个方便构造方法有定义实例的选择,也可以用self。
这里是阶段1在假设的子类和超类如何中如何查找一个初始化调用:
这个例子中,初始化过程以调用子类上的方便构造方法开始。这个方便构造方法还不能修改任何属性。它委托了同一个类的指定构造方法。
那个指定构造方法确保所有的子类属性都有值了,就像安全检查1种要求的一样。然后它调用它超类的一个指定构造方法,在继承链上继续进行初始化。
超类的指定构造方法确保所有的超类属性都有值了。这里没有更上面的超类要初始化,所以不必进一步向上委托。
一旦超类所有的属性都有初始值了,对应的内存就被认为初始化完了,第一阶段结束。
这里是同一个初始化的第二阶段:
超类的指定构造方法有一个进一步定制实例的机会(尽管它不必一定这样做)。
一旦超类的指定构造方法结束,子类的指定构造方法可以做额外的定制(同样,尽管它也不必一定这样做)。
最终,一旦子类的指定构造方法结束,发起最原始调用的方便构造方法可以做额外的定制了。
构造方法的继承和重写
和OC中的子类不同,Swift的子类不会默认继承它们超类的构造方法。Swift直接避免了出现这样的情形:一个超类的简单构造方法被一个相对特殊的子类构造方法继承,这个子类构造方法被用来创建出了一个并不完全或者正确初始化的实例。
NOTE
超类构造方法在特定环境中被继承,只在这样做是安全而且恰当的时候才行。更多的信息参看下面的: Automatic Initializer Inheritance。
如果子类想要针对一个或多个超类的构造方法进行定制,需要提供这些构造方法的子类实现。
当子类构造方法写的和超类的一个指定构造方法一致时,实际上是重写了那个指定构造方法。因此需要在子类的构造方法定义前写上override 修饰符。即使重写一个自动获得的默认构造方法也要这么做,参见: Default Initializers.
像重写属性、方法或者脚本一样,override修饰符督促Swift检查超类是否拥有一个与之匹配的指定构造方法可以被重写,检查重写的构造方法参数。
NOTE
重写超类的指定构造方法需要一直都写override修饰符,即使子类实现时一个方便构造方法。
相反的,如果子类的一个构造方法和超类的方便构造方法匹配,超类的方便构造方法永远不会被子类直接调用,就像 Initializer Delegation for Class Types描述的规则一样。因此,子类没有(严格说来)提供一个超类构造方法的重写。结果就是不必因为提供了和超类方便构造方法匹配的构造方法,而使用override修饰符。
下面的例子定义了一个基本类叫做Vehicle。这个类定义了一个存储属性叫做numberOfWheels,这个属性有一个默认的整型值0.numberOfWheels属性被一个计算属性description调用,来创建一个Stirng类型的交通工具描述内容:
class Vehicle { var numberOfWheels = 0 var description: String { return "\(numberOfWheels) wheel(s)" } }
Vehicle类给它唯一的一个存储属性提供了一个默认值,没有提供任何定制的构造方法。作为结果,它会自动获得一个默认的构造方法,就像Default Initializers描述 的一样。默认的构造方法(当有用时)总是类的制定构造方法,可以用来创建创建Vehicle类的一个实例,给numberOfWheels赋值为0:
let vehicle = Vehicle() println("Vehicle: \(vehicle.description)") // Vehicle: 0 wheel(s)
下面的类型定义Vehicle的一个子类叫做Bicycle:
class Bicycle: Vehicle { override init() { super.init() numberOfWheels = 2 } }
子类Bicycle自己定义了一个指定构造方法:init()。这个制定构造方法和Bicycle的超类的一个指定构造方法匹配,所以Bicycle版本的构造方法就需要用override修饰符标记。
Bicycle的init()构造方法开始调用了super.init(),就是调用了Bicycle的超类Vehicle的默认构造方法。这样确保了从Vehicle继承来的numberOfWheels属性在被Bicycle修改前就已经被初始化好了。调用super.init()之后,numberOfWheels的初始值会被新值2替换。
如果创建了一个Bicycle实例,可以调用继承来的description计算属性查看它的numberOfWheels属性被修改成了什么:
let bicycle = Bicycle()
println(”Bicycle: (bicycle.description)”)
// Bicycle: 2 wheel(s)
NOTE
初始化阶段子类可以修改继承来的变量属性,但是不能修改继承来的常量属性。
继承来的自动构造方法(Automatic Initializer Inheritance)
像上面提到的一样,子类不能默认继承它超类的构造方法。然而,特定条件下超类的构造方法会自动被继承。实践中,这意味着多数情况下不需要重写构造方法,在安全的前提下可以继承超类的构造方法,做到代价最小。
假设子类新加的属性都提供了默认值,下面两条规则要遵守:
规则1
如果子类没有定义任何指定构造方法,它会默认继承超类的指定构造方法。
规则2
如果子类提供了它超类所有指定构造方法的实现,或是通过规则1种的继承而来,或是在它的定义中提供了自定义实现,那么子类会自动继承超类的全部方便构造方法。
NOTE
一个子类可以实现超类的指定构造方法作为子类的方便构造方法,部分满足规则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类提供了一个只带一个参数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],委托Food类的init(name: String)构造方法:
let mysteryMeat = Food() // mysteryMeat's name is "[Unnamed]"
继承谱系中的第二个类是Food的子类,叫做RecipeIngredient。RecipeIngredient类表现的是一道菜的菜谱。它定义了一个Int属性叫做quality(在继承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属性赋值,quantity是RecipeIngredient唯一自己定义的属性。然后,委托Food类的构造方法init(name: String)。这个过程满足前面Two-Phase Initialization一节中的安全检查1。
RecipeIngredient 同时也定义了一个方便构造方法: init(name: String),它被用来只根据一个名字创建一个RecipeIngredient 实例。这个构造方法会将所有的RecipeIngredient实例的数量都赋值为1.这个方便构造方法使得创建一个RecipeIngredient实例更加快捷和方便,避免了构造若干个数量都是1的duplication实例时产生的代码重复。这个方便构造方法仅仅是委托了类自身的指定构造方法,传递给quantity的值为1.
RecipeIngredient提供的init(name: String)方便初始化方法和超类Food中的指定构造方法init(name: String)带的参数是一样的。因为这个方便构造方法重写了它超类的指定构造方法,它必须被override修饰符标记(和Initializer Inheritance and Overriding描述的一样)。
尽管RecipeIngredient提供了init(name: String)初始化方法作为一个方便构造方法,RecipeIngredient仍然提供了它超类的指定构造方法。因此,RecipeIngredient自动继承了所有超类的方便构造方法。
这个例子中,RecipeIngredient的超类是Food,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类表现菜谱在购物列表上的内容。
购物列表中的每一项开始都是“未购买”。为了表现这些,ShoppingListItem 定义了一个叫做purchased的布尔类型的属性,它有一个默认的false属性。ShoppingListItem 同时添加 了一个计算属性叫做description,它给ShoppingListItem 实例提供一个文本描述内容:
class ShoppingListItem: RecipeIngredient { var purchased = false var description: String { var output = "\(quantity) x \(name)" output += purchased ? " ✔" : " ✘" return output } }
NOTE
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 { println(item.description) } // 1 x Orange juice ✔ // 1 x Bacon ✘ // 6 x Eggs ✘
这里,一个新的数组breakfastList 被用字面包含的方式创建,其中是三个ShoppingListItem 类实例。数组的类型是[ShoppingListItem]。数组被创建后,数组中第一项的名字被从“[Unnamed]”修改为“Orange juice”同时被标记为已经购买。打印它们每一项的描述,显示它们的默认状态按照预期的被修改了。
可以失败的构造方法
定义一个可以初始化失败的类、结构体或者枚举,有时是非常有用的。这类失败可能由错误的初始化参数、缺失必要的外部资源或者其他阻止初始化成功的条件而触发。
为了应对引起初始化错误的条件,可以给一个类、结构体或者枚举定义一个或多个可以失败的构造方法。在init关键字后添加一个问号(init?)就定义了一个可以失败的构造方法。
NOTE
不能定义拥有相同参数类型和名称的可以失败的和不可失败的构造方法。
可以失败构造方法创建了一个可选类型。当初始化失败被触发时,在可以失败的构造方法中写上:return nil。
NOTE
严格说来,构造方法并不返回一个值。它们的作用是确保在初始化结束后,self被完全而且正确的初始化。尽管写return nil 触发初始化失败,但是不能使用return关键字表示初始化成功。
下面例子定义了一个叫做Animal的结构体,它有一个Stirng类型的常量属性叫做species。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 { println("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 { println("The anonymous creature could not be initialized") } // prints "The anonymous creature could not be initialized"
NOTE
检查一个空的字符串(比如“”而不是“Giraffe”)和检查表示没有值的可选String值是不一样的。上面的例子中,一个空的字符串“”是合法的,不是可选的String。但是,一个动物的species属性是一个空字符串是不恰当的。为了模拟这个限制,当一个空字符串被发现时,可以失败的构造方法触发初始化失败。
枚举的可以失败构造方法
可以根据一个或多个参数使用可以失败构造方法选择一个合适的枚举成员。当提供的参数不匹配合适枚举成员,这种构造方法可以失败。
下面的例子定义了一个叫做TemperatureUnit的枚举类型,它有三个可能的状态(Kelvin,Celsius和Fahrenheit)。根据一个描述温度符号的Character类型值,一个可以失败的构造方法被用来挑选一个合适的枚举成员:
enum TemperatureUnit { case Kelvin,Celsius,Fahrenheit init?(symbol: Character) { switch symbol { case "K": self = .Kelvin case "C": self = .Celsius case "F": self = .Fahrenheit default: return nil } } }
可以使用可以失败的构造方法选择一个恰当的枚举成员表示三个可能的状态,没有匹配到三个中的任意一个会导致初始化失败:
let fahrenheitUnit = TemperatureUnit(symbol: "F") if fahrenheitUnit != nil { println("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 { println("This is not a defined temperature unit,so initialization Failed.") } // prints "This is not a defined temperature unit,so initialization Failed."
采用原始值的枚举的可以失败的构造方法(Failable Initializers for Enumerations with Raw Values)
有原始值的枚举自动获得一个可以失败的构造方法: init?(rawValue:),这个构造方法有一个叫做rawValue的参数,它的类型和原始值的类型一致,如果找到匹配的枚举成员会选择它,在不存在匹配值时会触发初始化失败。
下面重写上面的TemperatureUnit 例子,采用Character类型的原始值,使用init?(rawValue:)构造方法:
enum TemperatureUnit: Character { case Kelvin = "K",Celsius = "C",Fahrenheit = "F" } let fahrenheitUnit = TemperatureUnit(rawValue: "F") if fahrenheitUnit != nil { println("This is a defined temperature unit,so initialization succeeded." let unknownUnit = TemperatureUnit(rawValue: "X") if unknownUnit == nil { println("This is not a defined temperature unit,so initialization Failed."
类的可以失败的构造方法(Failable Initializers for Classes)
值类型(就是结构体或者枚举)的可以失败的构造方法会触发一个初始化失败,在其构造方法实现 的任意一个时间。在上面的Animal结构体中,构造方法触在方法很开头的地方就发一个初始化失败,在species属性被设置之前。
然而对于类,可失败的构造方法只能在这个类定义的所有存储属性被给予初始值而且构造方法代理已经发生之后才能触发一个初始化失败。
下面的例子展示了在一个类的可失败构造方法中如何使用一个自拆包可选属性满足这个要求:
class Product { let name: String! init?(name: String) { if name.isEmpty { return nil } self.name = name } }
上面Product类的定义和前面Animal结构体很像。Product类有一个常量属性叫做name,它不能被赋予一个空字符串。为了做到这个要求,Product类使用了一个可以失败的构造方法。
然而,Product是一个类而不是一个结构体。这意味着和Animal不同,Product类的可失败构造方法在触发初始化失败之前,必须给name属性提供一个初始值。
上面的例子中,Product类的那么属性被定义为隐式自解包的可选字符串类型(String!)。因为它是可选类型的,这意味着name属性在初始化阶段被赋予一个特定值之前,有一个默认值nil。作为一个结果,在构造方法中给name属性赋值之前,如果传递进来一个空字符串,Product的可失败构造方法会在构造方法一开始就触发构造失败。
因为name属性是常量,如果初始化过程成功了,可以确信这个属性是非空(译者:non-nil)的。尽管它的定义是一个默认自动拆包的可选类型,但可以尽管放心的使用它拆包后的值,无需做是否为nil的检查:
if let bowTie = Product(name: "bow tie") { // no need to check if bowTie.name == nil println("The product's name is \(bowTie.name)") } // prints "The product's name is bow tie"
可以失败构造方法的传递(Propagation of Initialization Failure)
一个类、结构体或者枚举的可失败构造方法可以委托同一类、结构体或者枚举的可失败构造方法。类似的,子类的可失败构造方法可以委托超类的可失败构造方法。
两种情况下,委托的构造方法如果失败,整个初始化过程就立即失败了,进一步的初始化代码不会被执行。
NOTE
一个可以失败的构造方法同样可以委托另外一个构造方法。这个用法可以给一个已经存在的不会失败的构造方法添加一个潜在失败的状态。
下面例子定义了一个Product的子类叫做CartItem。CartItem模拟了在线购物车中的一项。CartItem定义了一个常量存储类型叫做quantity并且确保它始终有值而且至少为1:
class CartItem: Product { let quantity: Int! init?(name: String,quantity: Int) { super.init(name: name) if quantity < 1 { return nil } self.quantity = quantity } }
quantity属性是隐式自拆包的整型(Int!)。像Product类中的name属性一样,这意味着quantity属性在初始化阶段没有被赋值前,有一个默认值nil。
CartItem的可失败构造方法一开始委托了它超类Product的构造方法:init(name:)。这么做遵循了这样一条规则:可以失败的构造方法在触发一个初始化失败前先进行构造方法委托。
如果因为一个空的name数值导致超类初始化失败,整个初始化过程就立即失败,而且后续的初始化代码不会被执行。如果超类初始化成功,CartItem的构造方法会验证它收到的quantity是否大于等于1.
如果使用一个不为空的名字和不小于1的数量创建一个CartItem实例,初始化会成功:
if let twoSocks = CartItem(name: "sock",quantity: 2) { println("Item: \(twoSocks.name),quantity: \(twoSocks.quantity)") } // prints "Item: sock,quantity: 2"
如果试着用0做quantity的值创建一个CartItem实例,CartItem的构造方法会引起初始化失败:
if let zeroShirts = CartItem(name: "shirt",quantity: 0) { println("Item: \(zeroShirts.name),quantity: \(zeroShirts.quantity)") } else { println("Unable to initialize zero shirts") } // prints "Unable to initialize zero shirts"
重写可失败构造方法(Overriding a Failable Initializer)
在子类中可以重写超类的可失败构造方法,就像其他构造方法一样。或者,可以将超类的可失败构造方法重写为子类的不可失败构造方法。这样,能够定义子类的一个不会失败的构造方法,尽管超类中的初始化过程是允许失败的。
记住,如果用子类的不可失败构造方法重写了一个超类的可失败构造方法,子类的构造方法就不能委托超类的构造方法了。一个不可失败的构造方法永远不能(将自己)委托(给)一个可以失败的构造方法。
NOTE
可以用一个不可失败的构造方法重写一个可失败的构造方法,而不是相反的。
下面的类定义了一个叫做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) { if name.isEmpty { return nil } self.name = name } }
接下来的例子定义了一个叫做AutomaticallyNameDocument的Document子类。AutomaticallyNamedDocument子类重写了Document类的两个指定构造方法。这些重写确保了一个AutomaticallyNamedDocument的实例在没有被初始化的时候或者一个空字符串被传给构造方法init(name:),它的name有一个“[Untitled]”的名字:
class AutomaticallyNamedDocument: Document { override init() { super.init() self.name = "[Untitled]" } override init(name: String) { super.init() if name.isEmpty { self.name = "[Untitled]" } else { self.name = name } } }
AutomaticallyNamedDocument重写超类的可失败构造方法init?(name:)为一个不可失败的构造方法:init(name:)。因为AutomaticallyNamedDocument处理空字符串的方式和它的超类不同了,它的构造方法不会失败,所以它提供了一个不可失败的构造方法版本作为替代。
init!可失败构造方法(The init! Failable Initializer)
可以定义一个可以失败的构造方法创建一个可选的对应类型的实例:采用init关键字后跟一个问号(init?)。同样的,可以定义一个可以失败的构造方法来创建一个可以隐式拆包对应类型实例。要做到这样,只需要用叹号替换问号即可(init !)。
可以init?委托给init!,反之亦然,可以用init!重写init?,反之亦然。init可以委托给init!,尽管这么做在init!失败的时候会触发断言。
必须的构造方法(required Initializers)
在一个类的构造方法定义前写上required修饰符,这表明这个类的所有子类都要实现这个构造方法:
class SomeClass { required init() { // initializer implementation goes here } }
每个子类的必须构造方法实现前,也需要required修饰符,这表明这个构造方法的需求会继续像子类的子类传递。一个必须的制定构造方法(a required designated initializer)不需要使用override修饰符:
class SomeSubclass: SomeClass { required init() { // subclass implementation of the required initializer goes here } }
NOTE
如果可以通过继承可以满足必要性的需求,可以不提供必要构造方法的实现。
为一个 闭包或者函数设置一个默认属性值(Setting a Default 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 }() }
NOTE
如果使用闭包初始化一个属性,要记得在闭包执行时,实例的其他部分还没有初始化。这意味着在闭包中不能访问其他属性的值,如果这些属性有默认值也不行。同样不能使用self属性,或者调用任何实例方法。
下面的的例子定义了一个结构体叫做Checkerboard(译者:西洋跳棋盘),它模拟了西洋跳棋(也没称作国际跳棋)的棋盘:
西洋跳棋在一个10*10的格子的棋盘上 玩,棋盘上用黑白两种颜色标识相邻的格子。为了表现棋盘,Checkerboard结构体定义了唯一的一个属性叫做boardColors,这个属性是一个有100个Bool值的数组。数组中的true表示的是一个黑色的方格,flase表示的是一个白色的方格。数组最开始的项表示的是棋盘上左上角的方格,最后一个项比啊是的是棋盘上右下角的方格。
boardColors数组初始化是通过一个闭包来设置它里面颜色来实现的:
struct Checkerboard { 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 temporaryBoard }() func squareIsBlackAtRow(row: Int,column: Int) -> Bool { return boardColors[(row * 10) + column] } }
每当一个Checkerboard 实例被创建,那个闭包就被执行一次,boardColor的默认值被计算和返回。上面的闭包计算和设置每个方格对应的颜色,结果存放在临时数组temporaryBoard中,当计算完毕后,这个临时数组会作为闭包的返回值返回。返回的数组值存储在boardColors中,可以使用squareIsBlackAtRow 查询其中的内容:
let board = Checkerboard() println(board.squareIsBlackAtRow(0,column: 1)) // prints "true" println(board.squareIsBlackAtRow(9,column: 9)) // prints "false"