构造过程

本页包含内容:

[TOC]

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

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

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

存储属性的初始赋值

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

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

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

构造器

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

init() {
    // 在此处执行构造过程
}

下面例子中定义了一个用来保存华氏温度的结构体Fahrenheit,它拥有一个Double类型的存储型属性temperature

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

var f = Fahrenheit()
print("The default temperature is \(f.temperature)° Fahrenheit")
// 输出 "The default temperature is 32.0° Fahrenheit"

这个结构体定义了一个不带参数的构造器init,并在里面讲存储型属性temperature的值初始化为32.0(华氏温度下水的冰点)。

默认属性值

如前所述,你可以在构造器中为存储型属性设置初始值。同样,你也可以在属性声明时为其设置默认值。

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

你可以使用更简短的方式在定义结构体Fahrenheit时为属性temperature设置默认值:

struct Fahrenheit {
    var temperature = 32.0
}

自定义构造过程

你可以通过输入参数和可选类型的属性来自定义构造过程,也可以在构造过程中给常量属性赋初值。

构造参数

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

下面例子中定义了一个包含摄氏温度的结构体Celsius。它定义了两个不同的构造器:init(formFahrenheit:)init(formKelvin:),二者分别通过接受不同温标下的温度来创建新的实例:

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

let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)
// boilingPointOfWater.temperatureInCelsius 是 100.0
let freezingPointOfWater = Celsius(fromKelvin: 273.15)
// freezingPointOfWater.temperatureInCelsius 是 0.0

第一个构造器拥有一个构造参数,其外部名字为fromFahrenheit,内部名字为fahrenheit;第二个构造器也拥有一个构造参数,其外部名字为fromKelvin,内部名字为kelvin。这两个构造器都将唯一的参数值转换成摄氏温度值,并保存在属性 temperatureInCelsius 中。

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

跟函数和方法参数相同,构造参数也拥有一个在构造器内部内部使用的参数名字和一个在调用时使用的外部参数名字。

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

以下例子中定义了一个结构体 Color,它包含了三个常量:redgreenblue。这些属性可以存储 0.01.0之间的值,用来指示颜色中红、绿、蓝成分的含量。

Color 提供了一个构造器,其中包含三个Double类型的构造参数。Color 也提供了第二个构造器,它只包含名为whiteDouble 类型的参数,它被用于给上述三个构造参数赋予同样的值。

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)
// 报编译时错误,需要外部名称

不带外部名的构造器参数

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

下面是之前 Celsius 例子的扩展,跟之前相比添加了一个带有 Double 类型参数的构造器,其外部名用 _ 代替:

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 为 37.0

调用 Celsius(37.0) 意图明确,不需要外部参数名称。因此适合使用 init(_ celsius: Double) 这样的构造器,从而可以通过提供 Double 类型的参数值调用构造器,而不需要加上外部名。

可选属性类型

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

下面的例子中定义了类SurveyQuestion,它包含一个可选字符串属性response

class SurveQuestion {
    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()
// 输出 "Do you like cheese?"
cheeseQuestion.response = "Yes, I do like cheese."

调查问题的答案在回答前是无法确定的,因此我们将属性 response 声明为 String? 类型,或者说是可选字符串类型。当 SurveyQuestion 实例化时,它将自动赋值为nil,表明此字符串暂时还没有值。

构造过程中常量属性的赋值

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

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

你可以修改上面的SurveQuestion示例,用常量属性替代变量属性text,表示问题内容在SurveQuestion的实例被创建之后不会再被修改。尽管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()
// 输出 "How about beets?"
beetsQuestion.response = "I also like beets. (But not with cheese.)"

默认构造器

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

下面的例子中创建了一个类ShoppingListItem,它封装了购物清单中的某一物品的属性:名字(name)、数量(quantity)和购买状态:

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

var item = ShoppingListItem()

由于shoppingListItem类中的所有属性都有默认值,且它是没有父类的基类,它将自动获得一个可以为所有属性设置默认值的默认构造器。

上面例子中使用默认构造器创造了一个 ShoppingListItem 类的实例(使用 ShoppingListItem() 形式的构造器语法),并将其赋值给变量 item

结构体的逐一成员构造器

除了上面提到的默认构造器,如果结构体没有提供自定义的构造器,它们将自动获得一个逐一成员构造器,即使结构体的存储型属性没有默认值。

逐一成员构造器是用来初始化结构体新实例里成员属性的快捷方法。我们在调用逐一成员构造器时,通过与成员属性名相同的参数名进行传值来完成对成员属性的初始赋值。

下面的例子中,定义了一个结构体Size,它包含两个属性widthheight。Swift可以根据这两个属性的初始赋值为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)

值类型的构造器代理

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

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

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

请注意:如果你为某个值类型定义了一个自定义的构造器,你将无法访问到默认构造器(如果是结构体,还将无法访问逐一成员构造器)。这种限制可以防止你为值类型增加一个额外的且十分复杂的构造器之后,仍然有人错误的使用自动生成的构造器。

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

下面例子将定义一个结构体 Rect,用来代表几何矩形。这个例子需要两个辅助的结构体 SizePoint,它们各自为其所有的属性提供了默认初始值 0.0

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

struct Point {
    var x = 0.0, y = 0.0
}

你可以通过下面三种方式为Rect创建实例:

  • 使用含有默认值的originsize属性来初始化;
  • 提供指定的originsize实例来初始化;
  • 提供指定的centersize来初始化;

在下面的Rect结构体定义中,我们为这三种方式提供了三个自定义的构造器:

struct Rect {
    var origin = Ponit()
    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.0)
        let originY = center.y - (size.height / 2.0)
        self.init(origin: Point(x: originX, y: originY), size: size)
    }
}

第一个Rect构造器init(),在功能上跟没有自定义构造器时自动获得的默认构造器是一样的。这个构造器是一个空函数,使用一对大括号{}来表示。调用这个构造器将返回一个Rect实例,它的originsize属性都使用定义时的默认值Point(x: 0.0, y: 0.0)Size(width: 0.0, height: 0.0)

let basicRect = Rect()
// basicRect 的 origin 是 (0.0, 0.0),size 是 (0.0, 0.0)

第二个Rect构造器init(origin:size:),在功能上跟结构体在没有自定义构造器是获得的逐一成员构造器是一样的。这个构造器只是简单地将originsize的参数值赋给对应的存储型属性:

let originRect = Rect(origin: Point(x: 2.0, y: 2.0), size: Size(width: 5.0, height: 5.0))
// originRect 的 origin 是 (2.0, 2.0),size 是 (5.0, 5.0)

第三个Rect 构造器 init(center:size:) 稍微复杂一点。它先通过 centersize 的值计算出 origin 的坐标,然后再调用(或者说代理给)init(origin:size:) 构造器来将新的 originsize 值赋值到对应的属性中:

let centerRect = Rect(center: Point(x: 4.0, y: 4.0), size: Size(width: 3.0, height: 3.0))
// centerRect 的 origin 是 (2.5, 2.5),size 是 (3.0, 3.0)

构造器init(center:size:)可以直接将originsize的新值赋值给对应的属性中。然而,构造器init(center:size:)通过使用提供了相关功能的现有构造函数将会更加便捷。

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

推荐阅读更多精彩内容

  • 本章将会介绍 存储属性的初始赋值自定义构造过程默认构造器值类型的构造器代理类的继承和构造过程可失败构造器必要构造器...
    寒桥阅读 769评论 0 0
  • 构造过程是使用类、结构体或枚举类型的实例之前的准备过程。在新实例可用前必须执行这个过程,具体操作包括设置实例中每个...
    莽原奔马668阅读 681评论 0 3
  • 构造过程是为了使用某个类、结构体或枚举类型的实例而进行的准备过程。其实就是初始化。构造过程是通过定义构造器(Ini...
    冰三尺阅读 332评论 0 0
  •  构造过程是使用类、结构体或枚举类型一个实例的准备过程。在新实例可用前必须执行这个过程,具体操作包括设置实例中每个...
    EndEvent阅读 629评论 0 3
  • 七月买的书。下定决心要学好英语了。然后是瑜伽。接着可能是化妆。 先将英语学好再说。
    伟几声阅读 176评论 0 0