Swift(十九)构造过程

21bc436853ba3d270aa9092a158ff847cebc73c78a952-qEIYI8_fw658.jpeg

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

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

存储型属性的初始赋值

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

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

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

构造器

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

init() {
    // perform some initialization here
}

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

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.0(水华氏度冰点)。

默认属性值

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

注意:如果一个属性总是使用同一个初始值,可以为其设置一个默认值。无论定义默认值还是在构造器中赋值,最终它们实现的效果是一样的,只不过默认值跟属性构造过程结合的更紧密。使用默认值能让你的构造器更简洁、更清晰,且能通过默认值自动推导出属性的类型;同时,它也能让你充分利用默认构造器、构造器继承等特性。

//你可以使用更简单的方式在定义结构体Fahrenheit时为属性temperature设置默认值:
struct Fahrenheit { 
    var temperature = 32.0 
} 

自定义构造过程

你可以通过输入参数和可选属性类型来定制构造过程,也可以在构造过程中修改常量属性。这些都将在后面章节中提到。

构造参数
你可以在定义构造器时提供构造参数,为其提供定制化构造所需值的类型和名字。构造器参数的功能和语法跟函数和方法参数相同。

struct Celsius { 
    var temperatureInCelsius: Double = 0.0 
    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 是 100.0 
let freezingPointOfWater = Celsius(fromKelvin: 273.15) 
// freezingPointOfWater.temperatureInCelsius 是 0.0” 

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

内部和外部参数名

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

然而,构造器并不像函数和方法那样在括号前有一个可辨别的名字。所以在调用构造器时,主要通过构造器中的参数名和类型来确定需要调用的构造器。正因为参数如此重要,如果你在定义构造器时没有提供参数的外部名字,Swift 会为每个构造器的参数自动生成一个跟内部名字相同的外部名,就相当于在每个构造参数之前加了一个哈希符号。
Color提供了一个构造器,其中包含三个Double类型的构造参数:

struct Color { 
//    var red = 0.0, green = 0.0, blue = 0.0  //如果设置属性默认值,则必须声明为可变
   let red, green, blue: Double
//没有声明外部参数值, 则默认使用内部参数值
    init(red: Double, green: Double, blue: Double) { 
        self.red   = red 
        self.green = green 
        self.blue  = blue 
    } 
} 
let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)
//如果不使用外部参数则会报错
//let veryGreen = Color(0.0, 1.0, 0.0) 
//missing argument labels 'red:green:blue:' in call
//let veryGreen = Color(0.0, 1.0, 0.0)
//                     ^
//                      red: green:  blue: 

如果你真的不想写外部参数值则可以把外部参数值以_代替
上面的例子可以改写为


struct Color {
    let red, green, blue: Double
/_与内部参数之间要有一个空格
    init(_ red: Double, _ green: Double, _ blue: Double) {
        self.red   = red
        self.green = green
        self.blue  = blue
    }
}

let magenta = Color(1.0, 1.0, 0.0)

此外,你还可以让某些参数好含有外部参数,某些不含有

struct Color {
    let red, green, blue: Double
//第一个含有外部参数,第二个省略外部参数,第三个则默认使用内部参数值
    init(colorRed red: Double, _ green: Double, blue: Double) {
        self.red   = red
        self.green = green
        self.blue  = blue
    }
}

let magenta = Color(colorRed: 1.0, 1.0, blue: 0.0)

可选属性类型

如果你定制的类型包含一个逻辑上允许取值为空的存储型属性--不管是因为它无法在初始化时赋值,还是因为它可以在之后某个时间点可以赋值为空--你都需要将它定义为可选类型optional type。可选类型的属性将自动初始化为空nil,表示这个属性是故意在初始化时设置为空的。
下面例子中定义了类SurveyQuestion,它包含一个可选字符串属性response:

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

当SurveyQuestion实例化时,它将自动赋值为空nil,表明暂时还不存在此字符串。

默认构造器
Swift 将为所有属性已提供默认值的且自身没有定义任何构造器的结构体或基类,提供一个默认的构造器。这个默认构造器将简单的创建一个所有属性值都设置为默认值的实例。

class ShoppingListItem {
//与OC不同, 类里面的所以属性,无论是使用默认属性赋值还是构造器赋值,必须有要赋初值。而只有可选类型不需要,可选类型如果没有初始值,会自动推断。
    var name: String?
    var quantity = 1
    var purchased = false
}
var item = ShoppingListItem()

结构体的逐一成员构造器

除上面提到的默认构造器,如果结构体对所有存储型属性提供了默认值且自身没有提供定制的构造器,它们能自动获得一个逐一成员构造器。

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

下面例子中定义了一个结构体Size,它包含两个属性width和height。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,用来展现几何矩形。这个例子需要两个辅助的结构体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属性;使用特定的origin和size实例来初始化;使用特定的center和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 的原点是 (0.0, 0.0),尺寸是 (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 的原点是 (2.0, 2.0),尺寸是 (5.0, 5.0) 

第三个Rect构造器init(center:size:)稍微复杂一点。它先通过center和size的值计算出origin的坐标。然后再调用(或代理给)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 的原点是 (2.5, 2.5),尺寸是 (3.0, 3.0)

构造器init(center:size:)可以自己将origin和size的新值赋值到对应的属性中。然而尽量利用现有的构造器和它所提供的功能来实现init(center:size:)的功能,是更方便、更清晰和更直观的方法。

注意:如果你想用另外一种不需要自己定义init()和init(origin:size:)的方式来实现这个例子,请参考扩展。

指定构造器和便利构造器

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

init(parameters) { 
    statements 
} 

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

convenience init(parameters) { 
   statements 
} 

指定构造器和便利构造器实战
接下来的例子将在实战中展示指定构造器、便利构造器和自动构造器的继承。它定义了包含三个类Food、RecipeIngredient以及ShoppingListItem的类层次结构,并将演示它们的构造器是如何相互作用的。

类层次中的基类是Food,它是一个简单的用来封装食物名字的类。Food类引入了一个叫做name的String类型属性,并且提供了两个构造器来创建Food实例:

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

下图中展示了Food的构造器链

8370_140612135829_1.png

类没有提供一个默认的逐一成员构造器,所以Food类提供了一个接受单一参数name的指定构造器。这个构造器可以使用一个特定的名字来创建新的Food实例:

let namedMeat = Food(name: "Bacon") 
// namedMeat 的名字是 "Bacon” 

Food类中的构造器init(name: String)被定义为一个指定构造器,因为它能确保所有新Food实例的中存储型属性都被初始化。Food类没有父类,所以init(name: String)构造器不需要调用super.init()来完成构造。

Food类同样提供了一个没有参数的便利构造器 init()。这个init()构造器为新食物提供了一个默认的占位名字,通过代理调用同一类中定义的指定构造器init(name: String)并给参数name传值[Unnamed]来实现:

let mysteryMeat = Food() 
// mysteryMeat 的名字是 [Unnamed] 

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

class RecipeIngredient: Food { 
    var quantity: Int 
    init(name: String, quantity: Int) { 
        self.quantity = quantity 
        super.init(name: name) 
    } 
    convenience init(name: String) { 
        self.init(name: name, quantity: 1) 
    } 
} 

下图中展示了RecipeIngredient类的构造器链:

8370_140612135926_1.png

RecipeIngredient类拥有一个指定构造器init(name: String, quantity: Int),它可以用来产生新RecipeIngredient实例的所有属性值。这个构造器一开始先将传入的quantity参数赋值给quantity属性,这个属性也是唯一在RecipeIngredient中新引入的属性。随后,构造器将任务向上代理给父类Food的init(name: String)。这个过程满足两段式构造过程中的安全检查1。

RecipeIngredient也定义了一个便利构造器init(name: String),它只通过name来创建RecipeIngredient的实例。这个便利构造器假设任意RecipeIngredient实例的quantity为1,所以不需要显示指明数量即可创建出实例。这个便利构造器的定义可以让创建实例更加方便和快捷,并且避免了使用重复的代码来创建多个quantity为 1 的RecipeIngredient实例。这个便利构造器只是简单的将任务代理给了同一类里提供的指定构造器。

注意,RecipeIngredient的便利构造器init(name: String)使用了跟Food中指定构造器init(name: String)相同的参数。尽管RecipeIngredient这个构造器是便利构造器,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。这个类构建了购物单中出现的某一种调味料。

购物单中的每一项总是从unpurchased未购买状态开始的。为了展现这一事实,ShoppingListItem引入了一个布尔类型的属性purchased,它的默认值是false。ShoppingListItem还添加了一个计算型属性description,它提供了关于ShoppingListItem实例的一些文字描述:

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

注意:ShoppingListItem没有定义构造器来为purchased提供初始化值,这是因为任何添加到购物单的项的初始状态总是未购买。

由于它为自己引入的所有属性都提供了默认值,并且自己没有定义任何构造器,ShoppingListItem将自动继承所有父类中的指定构造器和便利构造器。

下图种展示了所有三个类的构造器链:

8370_140612140024_1.png

你可以使用全部三个继承来的构造器来创建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[]。在数组创建完之后,数组中第一个ShoppingListItem实例的名字从[Unnamed]修改为Orange juice,并标记为已购买。接下来通过遍历数组每个元素并打印它们的描述值,展示了所有项当前的默认状态都已按照预期完成了赋值。

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

如果某个存储型属性的默认值需要特别的定制或准备,你就可以使用闭包或全局函数来为其属性提供定制的默认值。每当某个属性所属的新类型实例创建时,对应的闭包或函数会被调用,而它们的返回值会当做默认值赋值给这个属性。

这种类型的闭包或函数一般会创建一个跟属性类型相同的临时变量,然后修改它的值以满足预期的初始状态,最后将这个临时变量的值作为属性的默认值进行返回。

下面列举了闭包如何提供默认值的代码概要:

class SomeClass { 
    let someProperty: SomeType = { 
        // 在这个闭包中给 someProperty 创建一个默认值 
        // someValue 必须和 SomeType 类型相同 
        return someValue 
        }() 
} 

注意闭包结尾的大括号后面接了一对空的小括号。这是用来告诉 Swift 需要立刻执行此闭包。如果你忽略了这对括号,相当于是将闭包本身作为值赋值给了属性,而不是将闭包的返回值赋值给属性。

注意:如果你使用闭包来初始化属性的值,请记住在闭包执行时,实例的其它部分都还没有初始化。这意味着你不能够在闭包里访问其它的属性,就算这个属性有默认值也不允许。同样,你也不能使用隐式的self属性,或者调用其它的实例方法。

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

推荐阅读更多精彩内容

  • 本章将会介绍 存储属性的初始赋值自定义构造过程默认构造器值类型的构造器代理类的继承和构造过程可失败构造器必要构造器...
    寒桥阅读 766评论 0 0
  • 构造过程是使用类、结构体或枚举类型的实例之前的准备过程。在新实例可用前必须执行这个过程,具体操作包括设置实例中每个...
    莽原奔马668阅读 680评论 0 3
  • 123.继承 一个类可以从另外一个类继承方法,属性和其他特征。当一个类继承另外一个类时, 继承类叫子类, 被继承的...
    无沣阅读 1,376评论 2 4
  • 下标脚本 下标脚本 可以定义在类、结构体和枚举这些目标中,可以认为是访问集合(collection),列表(li...
    cht005288阅读 440评论 0 0
  • 我遇见你 在我最美丽的年纪 可不巧 正逢那是六月雨季 匆忙的我们相撞 作业飘落一地 我遇见你 在七月的雨里 慌乱之...
    归蜜娅娅阅读 189评论 0 0