十四 初始化
初始化是类,结构体和枚举类型实例化的准备阶段。这个阶段设置这个实例存储的属性的初始化数值和做一些使用实例之前的准备以及必须要做的其他一些设置工作。
通过定义构造器(initializers)实现这个实例化过程,也就是创建一个新的具体实例的特殊方法。和Objective-C不一样的是,Swift的构造器没有返回值。它们主要充当的角色是确保这个实例在使用之前能正确的初始化。
类实例也能实现一个析构器(deinitializer),在类实例销毁之前做一些清理工作。
1、存储属性的初始化
类和结构体必须在它们被创建时把它们所有的属性设置为合理的值。存储属性不能为不确定状态
你可以在构造方法里面给一个属性设置一个初始值,或者在定义的时候给属性设置一个默认值
注意:当你对给一个属性分配一个默认值的时候,它会调用它相对应的初始化方法,这个值是对属性直接设置的,不会通知它对应的观察者
构造器
构造器是创建一个具体类型实例的方法。最简单的构造器就是一个没有任何参数实例方法,写作init。
在下面的例子定义了一个叫Fahrenheit(华氏度)的新结构体,来储存转换成华氏度的温度。Fahrenheit结构体,有一个属性,叫temperature(温度),它的类型为Double(双精度浮点数):
struct Fahrenheit { var temperature: Double init() { temperature = 32.0 } } var f = Fahrenheit() print("The default temperature is \(f.temperature)° Fahrenheit")
这个结构体定义了一个单一的构造方法init,它没有任何参数,它储存的温度属性初始化为32.0度。(水在华氏度的温度情况下的冰点)。
属性的默认值
如上所述,你可以在构造器中设置它自己的储存属性的初始化值。或者在属性声明时,指定属性的默认值,你指定一个默认的属性值,会被分配到它定义的初始值。
注意:如果一个属性常常使用同样的初始化值 ,提供一个默认值会比在初始化使用一个默认值会更好。
同样的结果,但是默认值与属性的初始化在它定义地时候就紧紧地捆绑在一起。很简单地就能构造器更简洁,和可以让你从默认值中推断出这个属性的类型。
struct Fahrenheit1 { var temperature = 32.0 }
2、自定义初始化(Customizing Initialization)
你可以根据输入的参数来自定义初始化过程和可选的属性类型,或者在初始化的时候修改静态属性。
初始化参数
你可以在构造器定义的时候提供一部分参数,在自定义初始化过程中定义变量的类型和名称。
初始化参和函数或者方法参数一样有着同样的功能。
在下面的例子中,定义了一个结构体Celsius。储存了转换成摄氏度的温度,Celsius结构体实现了从不同的温度初始化结构体的两个方法,init(fromFahrenheit:) 和init(fromKelvin:)。
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) let freezingPointOfWater = Celsius(fromKelvin: 273.15)
第一个构造器只有一个初始化参数,形参(External Parameter Names)fromFahrenheit,和实参(Local Parameter Names)fahrenheit。第二个构造器有一个单一的初始化参数,形参(External Parameter Names)fromKenvin,和实参(Local Parameter Names)kelvin。两个构造器都把单一的参数转换为摄氏度和储存到一个temperatureInCelsius的属性.
实参名(Local Parameter Names)和形参名(External Parameter Names)
和函数参数和方法参数一样,初始化参数拥有在构造器函数体使用的实参,和在调用时使用的形参.
然而,和函数或者方法不同,构造器在圆括号前面没有一个识别函数名称。因此,构造器参数的名称和类型,在被调用的时候,很大程度上扮演一个被识别的重要角色。为此,在构造器中,当你没有提供形参名时,Swift就会为每一个参数提供一个自动的形参名。这个形参名和实参名相同,就像和之前你写的每一个初始化参数的hash符号一样。
注意:如果你在构造器中没有定义形参,提供一个下横线(_)作为区分形参和上面说描述的重写默认行为。
在下面的例子 ,定义了一个结构体Color,拥有三个静态属性red,green和blue。这些属性储存了从0.0到1.0的值,这些值代表红色 ,绿色和蓝色的深度。
Color提供了一个构造器,以及三个双精度(Double)类型的参数:
struct Color { var red = 0.0, green = 0.0, blue = 0.0 init(red: Double, green: Double, blue: Double) { self.red = red self.green = green self.blue = blue } }
无论什么时候,你创建一个Color实例,你必须使用每一个颜色的形参来调用构造器:
let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)
可选类型
如果你储存属性使用的是自定义的类型在逻辑上允许值为空-或者他们的值并不在构造器中初始化,或者他们被允许为空。可以定义一个可选类型的属性。可选类型属性是一个自动初始化值为nil,表示这个属性有意在构造器中设置为“空值”(no value yet)。
在下面的例子中,定义了一个SurveryQuestion类,拥有一个可选的String属性response。
这个回答在他们调查问题在发布之前是无法知道的,所以response定义为类型String? ,或者叫可选String(optional String)。说明它会被自动分配一个默认值nil,意思为当surverQuestion初始化时还不存在。
在初始化时修改静态属性
当你在设置静态属性值时,只要在初始化完成之前,你都可以在初始化时随时修改静态属性。
注意:对于类的实例化,一个静态属性只能在初始化时被修改,这个初始化在类定义时已经确定。
你可以重写SurveryQuestion例子,对于问题的text属性,使用静态属性会比动态属性要好,因为SurveyQuestion实例被创建之后就无法修改。尽管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() beetsQuestion.response = "I also like beets. (But not with cheese.)"
3、默认构造器
Swift为每一个结构或者基类提供了默认的构造器,来初始化它们所包含的所有属性。默认构造器将会创建一个新的实例然后将它们的属性设置为默认值。
下面的例子定义了一个叫ShoppingListItem的类,包含了名称,数量和是否已购买的属性,将会被用在购物清单中:
class ShoppingListItem { var name: String? var quantity = 1 var purchased = false } var item = ShoppingListItem()
因为ShoppingListItem类中所有的属性都有默认值,并且这个类是一个没有父类的基类,所以它默认拥有一个会将所有包含的属性设置为初始值的默认构造器。比如在这个例子中name属性是一个可选String属性,它会被默认设置为nil,尽管在代码中没有指明。上面的例子使用默认构造器创建了一个ShoppingListItem类,记做ShoppingListItem(),然后将它赋值给了变量item。
结构类型的成员逐一构造器
除了上面提到的默认构造器之外,结构类型还有另外一种成员逐一完成初始化的构造器,可以在定义结构的时候直接指定每个属性的初始值。
成员逐一构造器是一种为结构的成员属性进行初始化的简便方法。下面的例子定义了一个叫Size的结构,和两个属性分别叫width和height。每个属性都是Double类型的并且被初始化为0.0。
因为每个存储属性都有默认值,在Size结构创建一个实例的时候就可以自动调用这个成员逐一构造器init(width:height:):
struct Size { var width = 0.0, height = 0.0 } let twoByTwo = Size(width: 2.0, height: 2.0)
4、数值类型的构造器代理
在实例的初始化过程中,构造器可以调用其他的构造器来完成初始化。这个过程叫构造器代理,可以避免多个构造器的重复代码。
对于数值类型和类来说,构造器代理的工作形式是不一样的。数值类型(结构和枚举)不支持继承,因此他们的构造器代理相对简单,因为它们只能使用自己的构造器代理。但是一个类可以继承自另外一个类,所以类需要确保在初始化的时候将它所有的存储属性都设置为正确的值。
对于数值类型来说,可以使用self.init来调用其他构造器,注意只能在这个数值类型内部调用相应的构造器。
需要注意的是如果你为数值类型定义了一个构造器,你就不能再使用默认构造器了。这种特性可以避免当你提供了一个特别复杂的构造器的时候,另外一个人误使用了默认构造器而出错。
注意:如果你想要同时使用默认构造器和你自己设置的构造器,不要将这两种构造器写在一起,而是使用扩展形式。
下面的示例定义了一个结构Rect来表示一个几何中的矩形。这个Rect结构需要另外两个结构来组成,包括Size和Point,初始值均为0.0:
struct Point { var x = 0.0, y = 0.0 }
现在你有三种初始化Rect结构的方式:直接使用为origin和size属性初始化的0值,给定一个指定的origin和size,或者使用中心点和大小来初始化。下面的例子包含了这三种初始化方式:
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) } }
5、类的继承和初始化
自定义初始化方法要先调用自己类默认初始化方法,自己重写默认初始化方法要先调用父类默认初始化方法
应该要先调用父类的构造器或者自身的默认构造器,以防止先给属性赋值了然后才调用父类或者自身的默认构造器把以前的赋值覆盖了
6、通过闭包或者函数来设置一个默认属性值
如果存储属性的默认值需要额外的特殊设置,可以使用闭包或者函数来完成。
闭包或者函数会创建一个临时变量来作为返回值为这个属性赋值。下面是如果使用闭包赋值的一个示意代码:
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的结构,是由游戏Checkers来的
image这个游戏是在一个10×10的黑白相间的格子上进行的。来表示这个游戏盘,使用了一个叫Checkerboard的结构,其中一个属性叫boardColors,是一个100个Bool类型的数组。true表示这个格子是黑色,false表示是白色。那么在初始化的时候可以通过下面的代码来初始化:
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的默认值将会被依次计算并且返回,然后作为结构的一个属性。通过使用squareIsBlackAtRow工具函数可以检测是否被正确设置:
let board = Checkerboard() print(board.squareIsBlackAtRow(0, column: 1)) print(board.squareIsBlackAtRow(9, column: 9))
十五 析构
在一个类的实例被释放之前,析构函数会被调用。用关键字deinit来定义析构函数,类似于初始化函数用init来定义。析构函数只适用于class类型。
1、析构过程原理
Swift 会自动释放不再需要的实例以释放资源。如自动引用计数那一章描述,Swift 通过自动引用计数(ARC)处理实例的内存管理。通常当你的实例被释放时不需要手动地去清理。但是,当使用自己的资源时,你可能需要进行一些额外的清理。例如,如果创建了一个自定义的类来打开一个文件,并写入一些数据,你可能需要在类实例被释放之前关闭该文件。
在类的定义中,每个类最多只能有一个析构函数。析构函数不带任何参数,在写法上不带括号:
deinit { // 执行析构过程 }
析构函数是在实例释放发生前一步被自动调用。不允许主动调用自己的析构函数。子类继承了父类的析构函数,并且在子类析构函数实现的最后,父类的析构函数被自动调用。即使子类没有提供自己的析构函数,父类的析构函数也总是被调用。
因为直到实例的析构函数被调用时,实例才会被释放,所以析构函数可以访问所有请求实例的属性,并且根据那些属性可以修改它的行为(比如查找一个需要被关闭的文件的名称)。
2、析构器操作
这里是一个析构函数操作的例子。这个例子是一个简单的游戏,定义了两种新类型,Bank和Player。Bank结构体管理一个虚拟货币的流通,在这个流通中Bank永远不可能拥有超过 10,000 的硬币。在这个游戏中有且只能有一个Bank存在,因此Bank由带有静态属性和静态方法的结构体实现,从而存储和管理其当前的状态。
struct Bank { static var coinsInBank = 10_000 static func vendCoins(var numberOfCoinsToVend: Int) -> Int { numberOfCoinsToVend = min(numberOfCoinsToVend, coinsInBank) coinsInBank -= numberOfCoinsToVend return numberOfCoinsToVend } static func receiveCoins(coins: Int) { coinsInBank += coins } }
Bank根据它的coinsInBank属性来跟踪当前它拥有的硬币数量。银行还提供两个方法——vendCoins和receiveCoins——用来处理硬币的分发和收集。
vendCoins方法在 bank 分发硬币之前检查是否有足够的硬币。如果没有足够多的硬币,Bank返回一个比请求时小的数字(如果没有硬币留在 bank 中就返回 0)。vendCoins方法声明numberOfCoinsToVend为一个变量参数,这样就可以在方法体的内部修改数字,而不需要定义一个新的变量。vendCoins方法返回一个整型值,表明了提供的硬币的实际数目。
receiveCoins方法只是将 bank 的硬币存储和接收到的硬币数目相加,再保存回 bank。
Player类描述了游戏中的一个玩家。每一个 player 在任何时刻都有一定数量的硬币存储在他们的钱包中。这通过 player 的coinsInPurse属性来体现:
class Player { var coinsInPurse: Int init(coins: Int) { coinsInPurse = Bank.vendCoins(coins) } func winCoins(coins: Int) { coinsInPurse += Bank.vendCoins(coins) } deinit { print("quit") Bank.receiveCoins(coinsInPurse) } }
每个Player实例都由一个指定数目硬币组成的启动额度初始化,这些硬币在 bank 初始化的过程中得到。如果没有足够的硬币可用,Player实例可能收到比指定数目少的硬币。
Player类定义了一个winCoins方法,该方法从银行获取一定数量的硬币,并把它们添加到玩家的钱包。Player类还实现了一个析构函数,这个析构函数在Player实例释放前一步被调用。这里析构函数只是将玩家的所有硬币都返回给银行:
var playerOne: Player? = Player(coins: 100) print("A new player has joined the game with \(playerOne!.coinsInPurse) coins") print("There are now \(Bank.coinsInBank) coins left in the bank")
一个新的Player实例随着一个 100 个硬币(如果有)的请求而被创建。这个Player实例存储在一个名为playerOne的可选Player变量中。这里使用一个可选变量,是因为玩家可以随时离开游戏。设置为可选使得你可以跟踪当前是否有玩家在游戏中。
因为playerOne是可选的,所以由一个感叹号(!)来修饰,每当其winCoins方法被调用时,coinsInPurse属性被访问并打印出它的默认硬币数目。
playerOne!.winCoins(2_000) print("PlayerOne won 2000 coins & now has \(playerOne!.coinsInPurse) coins") print("The bank now only has \(Bank.coinsInBank) coins left")
这里,player 已经赢得了 2,000 硬币。player 的钱包现在有 2,100 硬币,bank 只剩余 7,900 硬币。
playerOne = nil print("The bank now has \(Bank.coinsInBank) coins")
玩家现在已经离开了游戏。这表明是要将可选的playerOne变量设置为nil,意思是“没有Player实例”。当这种情况发生的时候,playerOne变量对Player实例的引用被破坏了。没有其它属性或者变量引用Player实例,因此为了清空它占用的内存从而释放它。在这发生前一步,其析构函数被自动调用,其硬币被返回到银行。