构造过程 (Initialization)

一、构造器的作用

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

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

存储属性的初始赋值

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

你可以在构造器中为存储型属性赋初值,也可以在定义属性时为其设置默认值。以下小节将详细介绍这两种方法。

注意

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

二、构造器语法

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

init() {

//在此处执行构造过程

}

三、构造参数

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

struct Celsius {

var temeratureInCelsius:Double

init(formFahrenheit fahrenheit:Double){//指定了外部参数名formFahrenheit

temeratureInCelsius = (fahrenheit -32.0) /1.8

}

init(fromKelvin kelvin:Double){//指定了外部参数名fromKelvin

temeratureInCelsius = kelvin -273.15

}

init(kelvin:Double){//构造器中每一个参数默认既是外部参数名也是内部参数名

temeratureInCelsius = kelvin -273.15

}

}

letboilingPointOfWater =Celsius(formFahrenheit:212.0)

print("\(boilingPointOfWater.temeratureInCelsius)")

letfreezingPointOfWater =Celsius(fromKelvin:273.15)

print("\(freezingPointOfWater.temeratureInCelsius)")

letfreezingPointOfWater2 =Celsius(kelvin:274.15)

print("\(freezingPointOfWater2.temeratureInCelsius)")

//1.2如果你在定义构造器时没有提供参数的外部名字,Swift会为每个构造器的每个参数自动生成一个跟内部名字相同的外部名

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

}

}

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

let halfGray =Color(white:0.5)

//1.3不带外部名的构造器参数

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

extension Celsius{

init(_celsius:Double){

temeratureInCelsius= celsius

}

}

//let bodyTemperatue = Celsius(37) //如果传入37会隐式转换成Double

let bodyTemperatue =Celsius(37.0)//这里不需要再添加外部参数名了

print("\(bodyTemperatue.temeratureInCelsius)")


四、可选属性类型

如果你定制的类型包含一个逻辑上允许取值为空的存储型属性——无论是因为它无法在初始化时赋值,还是因为它在之后某个时间点可以赋值为空——你都需要将它定义为可选类型optional

type。可选类型的属性将自动初始化为nil,表示这个属性是有意在初始化时设置为空的。

class  SurveyQuestion {

var text:String

var response:String?

init(text:String){

self.text= text

}

func ask(){

print(text)

//        print( \(text)) //这种写法错误,字符串插值\(item)中的item应是字符串字面值;对String类型的变量不能使用字符串插值

}

}

let cheeseQuestion =SurveyQuestion(text:"Do you like cheese?")

cheeseQuestion.ask()

cheeseQuestion.response="Yes, I do like cheese."

五、构造过程中常量属性(let修饰)的修改

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

注意

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

六、默认构造器

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

class ShoppingListItem {

var name:String?

var quantity =1

var purchased =false

}

var item =ShoppingListItem()//默认构造器

七、结构体的逐一成员构造器

struct Size {

var width =0.0, height =0.0

}

let twoByTwo =Size()//默认构造器

let twoByTwo2 =Size(width:2.0, height:3.0)//逐一成员构造器,只有结构体才可能有这个构造器

八、值类型的构造器代理

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

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

struct Point {

varx =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

}

//假如你希望默认构造器、逐一成员构造器以及你自己的自定义构造器都能用来创建实例,可以将自定义的构造器写到扩展(extension)中,而不是写在值类型的原始定义中.例如如果把下面的这个自定义的构造器写在extension,那么上面的两个构造器就不需要再自己定义了,系统会成功默认构造器和逐一成员构造器

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)

}

}

let basicRect =Rect()

let originRect =Rect(origin:Point(x:2.0, y:2.0),

size:Size(width:5.0, height:5.0))

let centerRect =Rect(center:Point(x:4.0, y:4.0),

size:Size(width:3.0, height:3.0))


九、类的继承和构造过程

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

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

十、指定构造器和便利构造器

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

每一个类都必须拥有至少一个指定构造器。在某些情况下,许多类通过继承了父类中的指定构造器而满足了这个条件。具体内容请参考后续章节构造器的自动继承

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

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

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

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

init(parameters) {

statements

}

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

convenienceinit(parameters) {

statements

}

十一、类的构造器代理规则

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

规则1

指定构造器必须调用其直接父类的的指定构造器。

规则2

便利构造器必须调用同一类中定义的其它构造器。

规则3

便利构造器必须最终导致一个指定构造器被调用。

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

指定构造器必须总是向上代理

便利构造器必须总是横向代理


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

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


十二、两段式构造过程

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

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

注意

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

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

安全检查1

指定构造器必须保证它所在类引入的所有属性都必须先初始化完成,之后才能将其它构造任务向上代理给父类中的构造器。

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

安全检查2

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

安全检查3

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

安全检查4

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

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

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

阶段1

某个指定构造器或便利构造器被调用。

完成新实例内存的分配,但此时内存还没有被初始化。

指定构造器确保其所在类引入的所有存储型属性都已赋初值。存储型属性所属的内存完成初始化。

指定构造器将调用父类的构造器,完成父类属性的初始化。

这个调用父类构造器的过程沿着构造器链一直往上执行,直到到达构造器链的最顶部。

当到达了构造器链最顶部,且已确保所有实例包含的存储型属性都已经赋值,这个实例的内存被认为已经完全初始化。此时阶段1完成。

阶段2

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

最终,任意构造器链中的便利构造器可以有机会定制实例和使用self。

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


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

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

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

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

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


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

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

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


十三、构造器的继承和重写

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

注意

父类的构造器仅会在安全和适当的情况下被继承。具体内容请参考后续章节构造器的自动继承

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

注意

当你重写一个父类的指定构造器时,你总是需要写override修饰符,即使你的子类将父类的指定构造器重写为了便利构造器。

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

十四、构造器的自动继承

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

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

规则1

如果子类没有定义任何指定构造器,它将自动继承所有父类的指定构造器。

规则2

如果子类提供了所有父类指定构造器的实现——无论是通过规则1继承过来的,还是提供了自定义实现——它将自动继承所有父类的便利构造器。(即使属性没有默认值,只要实现了父类的所有指定构造器,就会自动继承父类的所有便利构造器)

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

注意

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

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

推荐阅读更多精彩内容