Swift 4.0 编程语言(五)

123.继承

一个类可以从另外一个类继承方法,属性和其他特征。当一个类继承另外一个类时, 继承类叫子类, 被继承的类叫超类。继承是类区别于Swift其他类型的基本行为。

Swift 中的类可以调用和访问超类的方法,属性和下标,并且可以覆盖这些方法,属性和下标来重定义或者修改它们的行为。 Swift 会通过判断覆定义在超类是否有对应定义来确保覆盖的正确。

类可以给继承属性添加属性观察者,在属性值改变时可以接到通知。属性观察者可以添加给任何一个属性, 不管它本来定义成存储还是计算属性。

124.定义一个基类

任何不继承其他类的类就叫做基类。

备注:Swift 的类不会继承一个通用基类。如果你定义的类没有超类,它就会自动变成基类。

下面的例子定义了一个基类 Vehicle. 这个基类定义了一个存储属性 currentSpeed, 有个默认值 0.0 (推断是一个浮点类型的属性)。 currentSpeed 属性被只读计算字符串属性 description 用来创建一个汽车的描述。

Vehicle 基类同时定义了一个方法 makeNoise. 这个方法实际上不对基类实例做任何事, 但是后面会被子类定制行为:

class Vehicle {

var currentSpeed = 0.0

var description: String {

return "traveling at \(currentSpeed) miles per hour"

     }

func makeNoise() {

// do nothing - an arbitrary vehicle doesn't necessarily make a noise

    }   

}

使用初始化语法创建一个 Vehicle 实例, 写作类型名后面跟着括号:

let someVehicle = Vehicle()

已经创建了一个新的 Vehicle 实例, 你可以访问它的 description 属性,来打印汽车当前速度的人类可读的信息:

print("Vehicle: \(someVehicle.description)")

// Vehicle: traveling at 0.0 miles per hou

Vehicle 类为任意汽车定义了通用特性, 但是对于自己没有多少用处。为了让它变的更有用, 你需要充完善它来描述更具体的汽车。

子类化

子类化的行为是基于现有类的一个新类。子类从现有类继承特性, 然后你可以完善。你也可以给子类添加新的特性。

为了标明子类有一个超类, 子类名写在超类名之前,用冒号分开:

class SomeSubclass: SomeSuperclass {

// subclass definition goes here

}

下面的例子定义了一个子类 Bicycle, 有一个超类 Vehicle:

class Bicycle: Vehicle {

var hasBasket = false

}

新的 Bicycle 类自动获得 Vehicle 的所有特性, 比如 currentSpeed 和 description 属性还有它的 makeNoise() 方法。

除了继承的特性, Bicycle 类还定义了一个新的存储属性, hasBasket, 默认值是 false (属性类型推断为布尔型).

默认情况下, 你创建的任何 Bicycle 实例都不会有个篮子。你可以在Bicycle 实例创建后给把它的 hasBasket 属性设置为真:

let bicycle = Bicycle()

bicycle.hasBasket = true

你可以修改继承来的 currentSpeed 属性, 同时查询实例继承的 description 属性:

bicycle.currentSpeed = 15.0

print("Bicycle: \(bicycle.description)")

// Bicycle: traveling at 15.0 miles per hour

子类本身也可以子类化。下面的例子创建了一个双座自行车,作为 Bicycle 的子类:

class Tandem: Bicycle {

var currentNumberOfPassengers = 0

}

Tandem 从Bicycle继承了所有的属性和方法, Bicycle 依次继承了Vehicle的所有属性和方法。 Tandem 子类也添加了一个新的存储属性 currentNumberOfPassengers, 默认值是 0.

如果你创建一个 Tandem 实例, 你可以使用它的新的和继承来的任意属性, 也可以查询继承自Vehicle的只读 description 属性:

let tandem = Tandem()

tandem.hasBasket = true

tandem.currentNumberOfPassengers = 2

tandem.currentSpeed = 22.0

print("Tandem: \(tandem.description)")

// Tandem: traveling at 22.0 miles per hour

重写

一个子类可以自定义实现另外继承超类的一个实例方法,类型方法,实例属性,类型属性或者下标。

为了重写另外继承的特性, 在需要重写的定义前书写 override 关键字。 做这样的澄清,你想要提供一个重写,却没有提供一个匹配的定义的错误。 意外重写会导致不希望的行为, 重写不带上 override 关键字,代码编译时会报错。

override 关键字提醒 Swift 编译器去检查超类是否有一个匹配的声明。这个判断确保你的重新定义是正确的。

访问超类的方法,属性和下标

当你为子类提供方法,属性和下标的重写时, 使用超类实现作为子类重写的部分是很有用的。比如, 你可以完善已存在实现的行为, 或者在一个已存在的继承变量存储改变的值。

任何合适的地方, 你都可以用super 前缀访问超类版本的方法,属性或者下标:

重写的方法 someMethod() 可以在实现里通过super.someMethod() 调用超类的 someMethod().

重写的属性 someProperty 可以在重写的setter和getter 实现里通过super.someProperty访问超类的 someProperty.

重写的下标 someIndex 可以在重写的下标实现里通过super[someIndex] 访问超类的下标 super[someIndex].

重写方法

你可以重写一个实例或者类型方法,在你的子类中提供一个定制的方法实现。

下面的例子定义了一个 Vehicle 的子类 Train, 它重写了继承自Vehicle 的 makeNoise()方法:

class Train: Vehicle {

override func makeNoise() {

print("Choo Choo")

    }

}

如果你创建了一个新的 Train 实例并且调用它的 makeNoise() 方法, 你可以看到 Train 子类版本的方法被调用了:

let train = Train()

train.makeNoise()

// 打印 "Choo Choo"

重写属性

你可以重写一个继承实例或者类型属性,来给属性提供自定义的 getter 和 setter, 或者添加属性观察者确保重写属性可以观察潜在的属性值的改变。

重写属性的 Getters 和 Setters

你可以提供一个自定义的 getter (和setter, 如果合适) 来重写任何继承的属性, 不管继承的属性是存储属性还是计算属性。 子类并不知道继承属性是计算的还是存储的—它只知道继承来的属性有一个名字和类型。 你应该声明重写属性的名字和类型, 来确保编译器判断你重新的属性和超类的属性有一样的名字和类型。

你可以在子类重写的时候提供setter和getter,把继承的只读属性变成读写属性。不过你不能把继承的读写属性变为只读属性。

备注:如果你提供一个setter作为重写的一部分, 你必须也提供一个getter. 如果你不想在getter里改变继承过来的属性的值, 你可以简单通过返回 super.someProperty 获取这个值, someProperty 是你要重写属性的名字。

下面的例子定义了一个新的类 Car, 它是 Vehicle 的子类。 Car 类引入了一个新的存储属性 gear, 默认值是 1. Car 类同时重写从Vehicle 继承来的 description 属性, 提供包含当前档位的自定义描述:

class Car: Vehicle {

var gear = 1

override var description: String {

return super.description + " in gear \(gear)"

   }

}

description 属性重写首先调用 super.description, 它放回 Vehicle 类的description 属性。Car 类的 description 版本在描述后添加了一些额外的文本来提供当前档位的信息。

如果你创建一个新的 Car 实例并且设置它的 gear 和 currentSpeed 属性, 你可以看到它会返回自定义的描述信息:

let car = Car()

car.currentSpeed = 25.0

car.gear = 3

print("Car: \(car.description)")

// Car: traveling at 25.0 miles per hour in gear 3

重写属性观察者

你可以用属性重写给继承来的属性添加观察者。这个确保继承来的属性改变时你会得到通知, 不管属性原先是怎么实现的。

备注:你不能给继承来的常量存储属性或者只读计算属性添加属性观察者。这些属性的值不能设置, 因此不适合在重写时提供 willSet 或者 didSet.

注意你也不能对同一个属性同时提供重写的 setter 和 重写的属性观察者。 如果你想观察属性值的变化, 而且你已经给属性提供了自定义的 setter, 你可以在自定义的setter里观察任何值的变化。

下面的例子定义了一个新的类 AutomaticCar, 它是Car的子类。 AutomaticCar 类表示一辆车是自动挡的。它会基于当前速度自动选择合适的档位:

class AutomaticCar: Car {

override var currentSpeed: Double {

didSet {

gear = Int(currentSpeed / 10.0) + 1

   }

   }

}

只要你设置 AutomaticCar 实例的 currentSpeed 属性, 属性的didSet 观察者会为新速度设置合适的档位。特别的是, 属性观察选择的档位等于新的 currentSpeed 值除以 10, 然后转换为最接近的整数再加1. 35.0 的速度产生的档位是 4:

let automatic = AutomaticCar()

automatic.currentSpeed = 35.0

print("AutomaticCar: \(automatic.description)")

// AutomaticCar: traveling at 35.0 miles per hour in gear 4

防止重写

你可以通过标记为final来防止方法,属性或者下标被重写。 你可以在方法,属性或者下标的关键字前书写final标识符来实现这个 (比如 final var, final func, final class func, 和 final subscript)。

在子类任何尝试去重写一个 final 方法,属性或者下标都会触发编译器错误。 在扩展中添加到类里的方法,属性或者下标也可以在扩展定义时标记为 final.

你可以在类定义的关键字前标记final来表明整个类都是不能重写的。任何尝试去子类化这个类都会报一个编译器错误。

初始化

初始化是类,结构体或者枚举使用前的准备过程。这个过程包括给每个存储属性设置初始化值并且执行其他实例使用前的设置和初始化工作。

通过定义初始化方法来实现这个过程, 它很像特殊函数用来创建特定类型的实例。跟 Objective-C 初始化方法不同, Swift 初始化方法不会返回值。它们的主要任务是确保一个类型的实例在使用前可以正确的初始化。

类类型的实例还可以实现去初始化方法, 它在类销毁之前执行清理工作。

为存储属性设置初始值

类或者结构体实例创建的时候,它们的存储属性必须设置成合适的初始值。存储属性不可以是未设置状态。

你可以在初始化方法内给存储属性设置初始值, 或者指定一个默认属性值作为属性定义的部分。这些操作下面会描述。

备注:当你给存储属性指定默认值, 或者在初始化方法里给它设置初始值, 这个设置是直接的, 没有调用任何属性观察者。

初始化方法

初始化方法用来创建特定类型的实例。最简单的形式, 一个初始化方法很像一个没有参数的实例方法, 用关键字 init 书写:

init() {

// perform some initialization here

}

下面的例子定义了一个新的结构体 Fahrenheit,它存储了 Fahrenheit 温度表达式。Fahrenheit 结构体有一个存储属性temperature, 类型是 

Double:

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 温度的冰点)。

默认属性值

你可以在初始化方法内部设置存储属性的值, 就像上面展示的那样。 另外一种方法是, 可以指定默认属性值作为属性定义的一部分。

备注:如果一个属性总是带有相同的初始值, 提供一个默认值而不是在初始化中设置一个值。结果是一样的, 不过默认值把属性的初始化跟它的定义联系的更紧密。 它更短,更清晰。保证你可以根据默认值推断属性的类型。默认值让你更容易利用默认初始化方法和初始化方法继承, 本章稍后会描述。

你可以用简单的形式写上面的结构体 Fahrenheit ,在属性声明的时候给它提供一个默认值:

struct Fahrenheit {

var temperature = 32.0

}

自定义初始化

你可以用输入参数和可选属性类型来自定义初始化过程。或者通过初始化时赋值常量属性, 下面会描述。

初始化参数

你可以提供初始化参数作为初始化方法定义的部分, 定义类型和值名。初始化参数跟函数和方面参数的语法和能力是一样的。

下面的例子定义了一个结构体 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

第一个初始化方法只有一个参数,这个参数有一个参数标签 fromFahrenheit 和一个参数名 fahrenheit. 第二个初始化方法也只有一个参数,这个参数有一个标签 fromKelvin 和一个参数名 kelvin. 两个初始化方法都是把参数转换为对应的摄氏温度值并把它存在属性 temperatureInCelsius 里。

参数名和参数标签

跟函数和方法参数一样, 初始化参数可以用参数名和参数标签。

不过, 初始化方法不能像函数和方法那样在括号前有一个标示函数名。因此, 所以初始化方法的参数名和参数类型扮演很重要的角色,用来区分应该调用哪个初始化方法。因为这个, 如果你不能提供参数标签,Swift 会自动提供一个这样的标签。

下面的例子定义了一个结构体 Color, 带有三个常量属性 red, green, 和 blue. 这些属性存储0.0到1.0之间的值来表示 red, green, 和 blue 在颜色中的数量。

Color 为三原色提供了一个初始化方法,带有三个有命名的参数,类型是 Double. Color 同时提供了带有一个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)

// 这个会报编译错误 - 需要参数标签

没有参数标签的初始化参数

如果你不想使用参数标签, 使用下划线 (_) 替代参数的显式参数标签来重写默认行为。

这里有一个早前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) 很合适, 通过无名浮点值可以调用它。

可选属性类型

如果你自定义类型有一个存储属性允许无值—可能因为它的值在初始化的时候不能设置, 或者因为它在某个时刻允许无值—定义这个属性为可选类型。可选类型属性自动初始化为nil, 表明这个属性故意在初始化时“没有值”。

下面的例子定义了一个类 SurveyQuestion, 带有一个可选字符串属性 response:

class SurveyQuestion {

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? 类型, 或者 “可选 String”. 当一个新的 SurveyQuestion 实例初始化的时候, 它自动赋默认值为nil, 意思是“还没有字符串”。

初始化时访问常量属性

在初始化时,你可以在任何时候给常量属性赋值, 只要在初始化完成时给它设置一个确定值。一旦一个常量属性指定一个值,后面就不能再修改了。

备注:对于类实例, 被类引入的常量属性在初始化时只能被这个类改变。 它不能被子类修改。

你可以修改上面的e SurveyQuestion 例子, 对问题文本使用常量属性而不是变量属性, 来表明一旦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()

// 打印 "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 属性是一个可选字符串类型属性, 它会自动设为 nil, 即使这个值没有写在代码里) 这个例子使用默认初始化方法来创建新实例, 初始化语法是 ShoppingListItem(), 然后赋值给一个变量 item.

结构体类型成员初始化方法

Structure 如果没有定义自己的初始化方法, 它会自动获取到一个成员初始化方法。跟默认初始化方法不同, 即使结构体有未设置默认值的存储属性,它也会接收到一个成员初始化方法。

成员初始化方法是简写方式,用来初始化新结构体实例的成员属性。新实例的初始值可以通过名字传给成员初始化方法。

下面的例子定义了一个结构体 Size, 它有两个属性 width 和 height. 两个属性根据初始值推断为 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.

注意,如果你为一个值类型定义一个初始化方法, 你将不再访问那种类型的默认初始化方法 (或者成员初始化方法, 如果它是一个结构体)。这种情况下复合初始化方法提供的额外必要的设置, 这个防止不小心被使用自动初始化方法规避。

备注:如果你想自定义值类型使用默认初始化方法和成员初始化方法来初始化, 同时使用你自定义的初始化方法, 那就在扩展里实现自定义初始化方法而不是作为值类型原先实现的部分。更多信息, 参见扩展。

下面的例子定义了一个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, 提供一个特定的原点和大小, 或者指定一个特定的中心点和大小。三个初始化选择用三个自定义的初始化方法表示:

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)

 }

}

第一个初始化方法, init(), 功能和结构体未定义初始化方法自动分配的默认初始化方法一样。这个初始化方法函数体为空, 有一堆空的大括号表示 {}. 调用这个初始化方法,返回一个 Rect 实例, 它的原点和大小都是默认值, 分别是 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:), 功能和结构体未定义初始化方法自动分配的成员初始化方法一样。这个初始化方法简单指定原点和大小参数值给对应的存储属性:

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:), 稍微有点复杂。开始基于中心点和大小值计算一个合适的原点值。然后调用 (或者代理) 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:) 初始化方法可以给对应的属性指定新的原点值和大小。不过, 对于 init(center:size:) 初始化方法来说采用已存在的构造器更加遍历。

类继承和初始化

类的所有存储属性—包含来自超类的任何属性—初始化的时候必须分配一个初始值。


125.指定构造器和便利构造器

指定初始化方法是类的主要初始化方法。一个指定初始化方法会完全初始化类引入的所有属性,同时会调用合适的超类初始化方法来继续超类链的初始化过程。

Classes 倾向于有很少的指定初始化方法, 对于类来说只有一个很平常。 指定构造器是通过初始化发生地的漏斗点, 通过它初始化继续往超类链上走。

每个类至少应该有一个指定构造器。在某些情况下, 这个需求通过继承超类的一个或者多个指定构造器来实现。

便利构造器是次要的, 对类来说只是支持初始化方法。你可以定义一个便利构造器来调用同类的指定构造器, 这个便利构造器设置指定构造器的参数为默认值。你可以定义一个便利构造器来创建类的实例,用作特定用途或者输入值类型。

如果你的类不需要便利构造器,你就不必要提供它们。什么时候要简化构造形式来节省时间或者让类的初始化意图更清楚,可以创建便利构造器。

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

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

init(parameters) {

statements

}

便利构造器写法一样, 但是init 关键字之前有个 convenience 修饰符, 用空格分开:

convenience init(parameters) {

statements

}

类的初始化代理

为了简化指定构造器和便利构造器之间的关系, Swift 使用下面三种规则在构造器之间代理调用:

规则 1

一个指定构造器必须调用它的直接超类的指定构造器。

规则 2

一个便利构造器必须调用同类的其他构造器。

规则 3

一个便利构造器最后必须调用一个指定构造器。

一个简单的方式可以记住这个:

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

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

下图列出了这三个规则:


这里, 超类有一个指定构造器和两个便利构造器。一个便利构造器调用另外一个便利构造器, 它依次调用这个单独的指定构造器。这个满足了上面的规则2和3. 超类本身没有超类, 所以第1条规则不适用。

图中的子类有两个指定构造柱和一个便利构造器。便利构造器必须调用这两个指定构造器之一, 因为它只能调用同类的另外的构造器。这个满足上面的规则2和3. 两个指定构造器必须调用超类的指定构造器, 这个满足上面的规则1.

备注:这个规则不影响你类的使用者如何创建每个类的实例。 上图中的任何构造器都可以用来完全初始化它们所属类的实例。这个规则只是影响你对类构造器的实现。

下面的图展示了更加复杂的四个类继承关系。 它展示了继承关系中的指定构造器如何扮演类构造器的漏斗点, 简化继承链中的类的相互关系:

两阶段初始化

Swift 中的类初始化是两阶段的过程。在第一阶段, 每个存储属性被指定一个初始值。一旦每个存储属性初始状态确定了, 第二阶段就开始了, 在新实例将要使用之前, 每个类都有机会去定制存储属性。

两阶段初始化过程的使用使得初始化更安全, 在类层次中依然给予完全的灵活性。两阶段初始化防止属性值在初始化前被访问, 同时也防止了属性值被另外的构造器意味的设置成一个不同的值。

备注:Swift 的两阶段初始化过程很像 Objective-C 的初始化。 主要区别是阶段 1, Objective-C 会给每个属性设定零或者空值 (比如 0 或者 nil). Swift 的初始化更加灵活, 可以让你自定义初始值, 可以应对0或者nil不是有效默认值的情况。

Swift 的编译器执行四项安全检查来确保两阶段初始化无错误的完成:

安全检查 1

一个指定构造器必须确保, 在向上代理超类构造器之前,所属类的所有属性都被初始化。

就像上面提及的, 对象的内存只考虑完全初始化一次所有的存储属性的初始状态。为了让这条规则满足, 一个指定构造器必须保证在向上传递前自己所有的属性都已经初始化。

安全检查 2

在给继承来的属性赋值前,一个指定构造器必须向上委托超类的构造器。如果不这样做, 指定构造器指定的新值会被超类自己的构造器给覆盖了。

安全检查 3

在给任意属性赋值前, 一个便利构造器必须委托另外一个构造器。(包括相同类定义的属性)。如果不这样做, 便利构造器指定的新值会被本类的指定构造器覆盖。

安全检查 4

一个构造器不能调用任何实例方法, 不能读任何实例属性的值, 或者直到第一阶段初始化完成才可以引用self.

第一阶段初始化结束后类的实例才完全有效。第一阶段类的实例有效后,属性只能被访问, 方法只能被调用。

这里是两阶段初始化的呈现, 基于以上四个安全检查:

阶段 1

一个指定构造器或者便利构造器在类里调用。

给类的实例分配内存。内存尚未初始化。

类的指定构造器确保所有存储属性都有一个值。这些存储属性的内存开始初始化。

指定构造器放手给超类的构造器,对它的存储属性执行同样的操作。

沿着继承链继续往上直到尽头。

一旦到了继承链的尽头, 继承链最后的类已经确保所有存储属性都有一个值。实例的内存被认为已经全部初始化, 第一阶段完成。.

阶段 2

从继承链尽头回来, 链中的每个指定构造器都有选择去继续定制实例。构造器现在可以访问 self 和改变属性, 也可以调用函数。

最后, 链中的任何便利构造器都有选择去定制实例并且使用 self.

这里展示第一阶段如何为一个假设的子类和超类寻找初始化调用的:


在这个例子中, 初始化开始于调用子类的便利构造器。这个便利构造器尚不能修改任何属性。它委托了本类的一个指定构造器。

指定构造器确保所有的子类属性都有一个值, 根据安全检查 1. 然后在它的超类调用一个指定构造器继续沿着链初始化。

超类指定构造器确保超类属性都有一个值。因为没有更深的超类要初始化, 所以也不需要更深的委托。

只要所有超类的属性都有一个初始值, 它的内存就被认为已经完成初始化, 那么阶段1就完成了。

这里是阶段2如何寻找相同的初始化调用:在这个例子中, 初始化开始于调用子类的便利构造器。这个便利构造器尚不能修改任何属性。它委托了本类的一个指定构造器。

指定构造器确保所有的子类属性都有一个值, 根据安全检查 1. 然后在它的超类调用一个指定构造器继续沿着链初始化。

超类指定构造器确保超类属性都有一个值。因为没有更深的超类要初始化, 所以也不需要更深的委托。

只要所有超类的属性都有一个初始值, 它的内存就被认为已经完成初始化, 那么阶段1就完成了。

这里是阶段2如何寻找相同的初始化调用:

子类的指定构造器现在有机会更进一步去定制实例 (尽管它不必要这么做)。

一旦子类指定构造器完成, 超类的指定构造器可以执行额外的定制(尽管它没有必要这么做)。

最后, 一旦子类的指定构造器完成, 原先调用的便利构造器就可以执行额外的定制。

初始化方法的继承和重写

不像 Objective-C 中的子类, Swift 子类默认不继承它的超类的初始化方法。 Swift 的方法防止了一种情况,在这种情况下来自一个超类的初始化方法被一个更专业的子类继承,并且用来这个子类的新实例,但是这个实例并未完全或者正确的初始化。

备注:超类的初始化方法在特定情况下被继承, 只有安全而且合适才会这么做。

如果你想让自定义子类展示和超类一样的一个或者多个初始化方法, 你可以在子类内提供这些初始化方法的自定义实现。

当你写一个子类初始化方法,这个方法匹配一个超类的指定构造器, 你可以提供这个指定构造器的重写。因此, 你必须在子类初始化方法定义前写上 override 修饰符。 即使你重写一个自动提供的默认初始化方法,也是这样。

跟一个重写的属性,方法或者下标一样, override 修饰符提醒Swift 去判断超类有这么一个指定构造器可以重写, 并且验证重写初始化方法的参数已经有目的的指定了。

备注:当重写超类指定构造器时你应该总是写上 override 修饰符, 即使子类初始化方法的实现是一个便利构造器。

相反, 如果你写一个子类初始化方法匹配超类的便利构造器, 超类便利构造器不能直接被子类调用。因此, 你的子类不能提供一个超类初始化方法的重写。结果是, 当提供一个超类便利构造器的匹配实现时,你不能写上 override 修饰符。

下面的例子定义了一个基类 Vehicle. 这个基类声明了一个存储属性 numberOfWheels, 带有一个默认值 0. numberOfWheels 被一个计算属性 description用来创建汽车特征的字符串描述:

class Vehicle {

var numberOfWheels = 0

var description: String {

return "\(numberOfWheels) wheel(s)"

  }

}

Vehicle 类给它唯一的存储属性提供了一个默认值, 本身没有提供任何自定义的初始化方法。结果是, 它会自动收到一个默认的初始化方法。这个默认的初始化方法总是一个指定构造器, 可以用来创建一个 numberOfWheels 为0的Vehicle 实例:

let vehicle = Vehicle()

print("Vehicle: \(vehicle.description)")

// Vehicle: 0 wheel(s)

下面的例子定义了一个 Vehicle 的子类 Bicycle:

class Bicycle: Vehicle {

override init() {

super.init()

numberOfWheels = 2

    }

}

Bicycle 定义了一个自定义指定构造器init(). 这个构造器匹配来着超类的指定构造器, 所以 Bicycle 版本的这个构造器用override 修饰符标记。

init() 初始化方法先调用 super.init(), 这是超类的默认初始化方法。这个确保 numberOfWheels 属性在被子类修改前可以被 Vehicle 初始化。调用 super.init() 后, numberOfWheels 原有值会被新值2替代。

如果你创建一个 Bicycle 实例, 你可以调用它的继承来的计算属性 description 来查看 numberOfWheels 属性是否被更新了:

let bicycle = Bicycle()

print("Bicycle: \(bicycle.description)")

// Bicycle: 2 wheel(s)

备注:子类在初始化的时候可以改变继承来的变量属性, 但是无法改变继承来的常量属性。

自动初始化方法的继承

就像上面提到的, 默认情况子类并不继承超类的初始化方法。不过, 不过如果某种条件满足了,超类的初始化方法还是可以被自动继承的。在实践中, 这意味着你通常不需要重写初始化方法, 只要这样做安全,你可以用最小的代价继承超类的初始化方法。

假设你给子类的任何新属性都设定了默认值, 下面的规则适用:

规则 1

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

规则 2

如果你的子类提供了超类所有指定构造器的实现—或者通过像规则1那样的继承, 或者提供自定义实现—然后它会自动继承超类所有的便利构造器。

即使你的子类添加更深的便利构造器,这个规则都适用。

备注:一个子类可以实现一个超类的指定构造器,来作为自己的便利构造器,这个是满足规则2的部分。

指定和便利构造器的初始化

下面的例子展示了指定构造器,便利构造器和自动构造器继承的活动。这个例子定义了三个类 Food, RecipeIngredient 和 ShoppingListItem 的继承关系, 并且展示了它们如何交互。

这个继承关系中的基类是 Food, 它是封装了一个食品名字的简单类。Food 类引入了一个单独的字符串属性 name 并且提供了两个构造器来创建 Food 实例:

class Food {

var name: String

init(name: String) {

self.name = name

   }

convenience init() {

self.init(name: "[Unnamed]")

   }

}

下图展示了Food 类的构造链:


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

let namedMeat = Food(name: "Bacon")

// namedMeat's name is "Bacon"

init(name: String) 构造器是一个指定构造器, 因为它可以保证一个新的Food 实例的所有存储属性完全初始化。Food 类没有超类, 所以 init(name: String) 不需要调用 super.init() 来完成初始化。

Food 类同时提供了一个便利构造器, init(), 没有任何参数。 通过委托Food 类的init(name: String), init() 为新食物提供了一个默认的占位符名字 [Unnamed]:

let mysteryMeat = Food()

// mysteryMeat's name is "[Unnamed]"

继承关系中的第二个类 RecipeIngredient 是Food 类的子类。RecipeIngredient 类模拟了烹调配方的成分。它引入了一个 Int 属性quantity (除了从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 属性, 这个是 RecipeIngredient 唯一的新属性。这个操作之后, 构造器向上委托基类的 init(name: String) 构造器。这个过程符合两阶段初始化的安全检查1.

RecipeIngredient 同时定义了一个便利构造器, init(name: String), 它用名字来创建一个 RecipeIngredient 实例。任何 RecipeIngredient 实例没有显式数量的,这个便利构造器都会假设一个数量值为1. 这个便利构造器方便 RecipeIngredient 实例快速创建, 并且当创建一些单个数量的实例时可以避免代码重复。这个便利构造器简单调用了指定构造器, 传入数量值 1.

init(name: String) 便利构造器跟指定构造器 init(name: String) 接受一样的参数。因为这个便利构造器重写了超类的指定构造器, 它必须标记 override 修饰符。

尽管 RecipeIngredient 提供了 init(name: String) 作为便利构造器, RecipeIngredient 也提供了所有超类的指定构造器的实现。因此, RecipeIngredient 也就自动继承了超类的所有便利构造器。

在这个例子里, RecipeIngredient 的超类是 Food, 它只有一个便利构造器 init(). 这个构造器会被 RecipeIngredient 继承。继承 init() 函数版本和 Food 版本是一样的。除了它是调用e RecipeIngredient 版本的 init(name: String) 而不是 Food 版本之外。

三个构造器都快要用来创建新的 RecipeIngredient 实例:

let oneMysteryItem = RecipeIngredient()

let oneBacon = RecipeIngredient(name: "Bacon")

let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)

继承关系里的最后一个类ShoppingListItem也是 RecipeIngredient 子类。ShoppingListItem 模拟了一个配方成分。

购物清单中的每一项都以“unpurchased”开始。为了说明这个事实, ShoppingListItem 引入一个布尔属性 purchased, 带有一个默认值 false. ShoppingListItem 同时添加了一个计算属性description, 它提供了ShoppingListItem 实例的文本描述:

class ShoppingListItem: RecipeIngredient {

var purchased = false

var description: String {

var output = "\(quantity) x \(name)"

output += purchased ? " ✔" : " ✘"

return output

  }

}

备注: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 {

print(item.description)

}

// 1 x Orange juice ✔

// 1 x Bacon ✘

// 6 x Eggs ✘

这里, 用数组字面量创建了一个新的数组 breakfastList, 它包含了3个 ShoppingListItem 实例。 数组类型推断为 [ShoppingListItem]. 这个数组创建后, 数组开始项 ShoppingListItem 名字从”[Unnamed]” 变成 “Orange juice” 并且标记为已购买。打印数组中的每一项来表示它们的默认状态按照预期设置了。

可失败构造器

有时候定义可以构造失败的类, 结构体和枚举是很有用的。失败有很多触发的因素, 比如无效构造参数值, 必须的外部资源的缺失, 或者一些阻止构造成功的其他条件。

要处理这些引起失败的构造条件, 可以给类, 结构体或者枚举定义一个或者多个可失败的构造器。方法是在 init 关键字之后放置一个问号 (init?).

备注 :你不能同时使用相同的参数名和参数类型, 定义一个可失败的构造器和不会失败的构造器。

一个可失败的构造器会创建一个可选类型值。 在一个可失败的构造器中写 return nil 表明在这个点构造失败会被触发。

备注: 严格来说, 构造器不返回值。它们的角色是保证 self 在初始化结束时可以充分正确的构造。尽管你写 return nil 会触发一个构造失败, 你不需要使用 return 关键字来表明构造成功。

例如, 为数值类型转换设计的可失败构造器。为了保证数值类型转换的正确, 使用 init(exactly:) 构造器。如果类型转换不能维持这个值, 构造就会失败。

let wholeNumber: Double = 12345.0

let pi = 3.14159

if let valueMaintained = Int(exactly: wholeNumber) {

print("\(wholeNumber) conversion to Int maintains value of \(valueMaintained)")

}

// 打印 "12345.0 conversion to Int maintains value of 12345"

let valueChanged = Int(exactly: pi)

// valueChanged 是 Int? 类型, 不是 Int

if valueChanged == nil {

print("\(pi) conversion to Int does not maintain value")

}

// 打印 "3.14159 conversion to Int does not maintain value"

下面的例子定义了一个结构体 Animal, 有一个常量 String 属性 species. 这个结构体同时定义了一个可失败构造器, 它只有一个参数 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 是 Animal? 类型, 不是 Animal

if let giraffe = someCreature {

print("An animal was initialized with a species of \(giraffe.species)")

}

// 打印 "An animal was initialized with a species of Giraffe"

如果你把空字符串传给可失败构造器的 species 参数, 就会触发构造失败:

let anonymousCreature = Animal(species: "")

// anonymousCreature 是 Animal? 类型, 不是 Animal

if anonymousCreature == nil {

print("The anonymous creature could not be initialized")

}

// 打印 "The anonymous creature could not be initialized"

备注: 判断一个空字符串值不同于判断 nil. 在上面的例子里, 一个空字符串 (“”) 是一个有效的, 非可选的 String. 不过, 对于动物来说 species 属性值为空不合适。为了模拟这个限制, 如果发现空字符串, 这个可失败构造器就会触发一个构造失败。

枚举的可失败构造器

你可以基于一个或多个参数使用构造器去选择一个合适的枚举分支. 如果提供的参数没有匹配合适的分支,构造就会失败。

下面的例子定义了一个枚举 TemperatureUnit, 有三种可能的状态 (kelvin, celsius, 和 fahrenheit). 可失败构造器根据代表温度符号的字符值来寻找一个合适的枚举分支:

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 {

print("This is a defined temperature unit, so initialization succeeded.")

}

// 打印 "This is a defined temperature unit, so initialization succeeded."

let unknownUnit = TemperatureUnit(symbol: "X")

if unknownUnit == nil {

print("This is not a defined temperature unit, so initialization failed.")

}

// 打印 "This is not a defined temperature unit, so initialization failed."

带原始值枚举的可失败构造器

带有原始值的枚举会自动接收一个可失败构造器, init?(rawValue:), 有一个参数 rawValue, 如果找到就匹配一个枚举分支, 否则触发构造失败。

你用原始值重写上面的 TemperatureUnit 例子,然后使用 init?(rawValue:) 构造器:

enum TemperatureUnit: Character {

case kelvin = "K", celsius = "C", fahrenheit = "F"

      }

let fahrenheitUnit = TemperatureUnit(rawValue: "F")

if fahrenheitUnit != nil {

print("This is a defined temperature unit, so initialization succeeded.")

     }

// 打印 "This is a defined temperature unit, so initialization succeeded."

let unknownUnit = TemperatureUnit(rawValue: "X")

if unknownUnit == nil {

print("This is not a defined temperature unit, so initialization failed.")

}

// 打印 "This is not a defined temperature unit, so initialization failed."

构造可失败的传递

在相同类,结构体或者枚举中可失败构造器可以委托调用另外的可失败构造器。相似的, 子类可失败构造器可以向上调用超类的可失败构造器。

另外一种情况, 如果你委托其他构造器导致构造失败, 那么整个构造过程就会立即失败, 后面的构造代码不会再执行。

备注:一个可失败构造器要可以委托调用一个非失败构造器。如果你需要给一个存在的初始化过程添加一个潜在的失败状态,可以用这个方法。

下面的例子定义了一个 Product 的子类 CartItem. CartItem 模拟了在线购物车的一项。CartItem 引入了一个存储常量属性 quantity 并且保证这个属性的值至少为 1:

class Product {

let name: String

init?(name: String) {

if name.isEmpty { return nil }

self.name = name

     }

    }

class CartItem: Product {

let quantity: Int

init?(name: String, quantity: Int) {

if quantity < 1 { return nil }

self.quantity = quantity

super.init(name: name)

   }

}

CartItem 可失败构造器首先判断 quantity 是否小于1. 如果 quantity 无效, 整个初始化过程立即失败并且后面的初始化代码不会执行。 同样的, Product 的可失败构造器判断 name 的值, 如果name是空字符串这个构造过程也会立即失败。

如果使用非空name和quantity大于等于1的值构造一个 CartItem 实例, 初始化就成功了:

if let twoSocks = CartItem(name: "sock", quantity: 2) {

print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)")

}

// 打印 "Item: sock, quantity: 2"

如果你用quantity 等于0来创建一个 CartItem 实例, CartItem 构造器会引起初始化失败:

if let zeroShirts = CartItem(name: "shirt", quantity: 0) {

print("Item: \(zeroShirts.name), quantity: \(zeroShirts.quantity)")

    } else {

print("Unable to initialize zero shirts")

}

// 打印 "Unable to initialize zero shirts"

相似的, 如果你使用空名字去创建一个 CartItem 实例, 超类 Product 构造器会导致初始化失败:

if let oneUnnamed = CartItem(name: "", quantity: 1) {

print("Item: \(oneUnnamed.name), quantity: \(oneUnnamed.quantity)")

   } else {

print("Unable to initialize one unnamed product")

}

// 打印 "Unable to initialize one unnamed product"

重写可失败构造器

你可以在子类重写超类的可失败构造器, 就像其他构造器一样。 或者, 你可以用子类的非失败构造器重写一个超类的构造器。 这使得你可以定义个子类,它的初始化不会失败。即使超类的构造允许失败。

注意,如果你用子类非失败构造器重写了超类的可失败构造器, 唯一向上调用超类构造器的方式是强制拆包超类可失败构造器的结果。

备注:你可以用一个非失败构造器重写一个可失败构造器,除此之外别无他法。

下面的例子定义了一个类 Document. 这个类模拟了一个文档,这个文档可以用一个非空字符串名字或者nil来初始化, 但是不能用一个空字符串:

class Document {

var name: String?

// this initializer creates a document with a nil name value

init() {}

// this initializer creates a document with a nonempty name value

init?(name: String) {

if name.isEmpty { return nil }

self.name = name

   }

}

下面的例子定义了 Document 的一个子类 AutomaticallyNamedDocument. AutomaticallyNamedDocument 子类重写了两个 Document 类引入的指定构造器。这些重写保证了一个 AutomaticallyNamedDocument 有一个初始值 “[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 处理空字符串的情况和超类不同, 它的构造器不需要失败, 所以它提供了一个非失败的构造器版本。

你可以在构造器里使用强制拆包,来调用超类的可失败构造器。 例如, UntitledDocument 子类总是命名为 “[Untitled]”, 并且在它的初始化过程中,它会使用超类的可失败构造器init(name:)

class UntitledDocument: Document {

override init() {

super.init(name: "[Untitled]")!

    }

}

在这种情况下, 如果超类的 init(name:) 构造器用空名字字符串调用, 强制拆包操作会引发一个运行时错误。不过, 它是用常量字符串调用的, 你可以看到这个构造器不会失败, 所以这种情况不会发生运行时错误。

init! 可失败构造器

通过在init关键之后面加上一个问号,你可以定义一个可失败构造器来创建一个对应类型的可选实例。或者, 你可以定义一个可失败构造器,来创建对应类型隐式拆包可选实例。 这个可以通过在init关键之后面加上感叹号而不是问好来实现。

你可以从init? 调用到 init! ,反之亦然, 而且你可以用init!重写 init? with init! ,反之亦然。你还可以从init 调用到 init!, 尽管这样做会触发一个断言,如果 init! 构造器导致构造失败的话。

必需构造器

在一个类的构造器前写上 required 修饰符来表明这个类的每个子类都要实现这些构造器:

class SomeClass {

required init() {

// initializer implementation goes here

    }

}

你也必需在每一个实现必须构造器的子类构造器前写上 required 修饰符, 来表明在链中构造需求适用更远的子类。当重写一个必须指定构造器时,你不需要写上 override 修饰符:

class SomeSubclass: SomeClass {

required init() {

// subclass implementation of the required initializer goes here

   }

}

备注:如果你可以用一个继承来的构造器满足要求,你就不必要提供一个显式的必须构造器。

用闭包或者函数设定默认属性值

如果一个存储属性的默认值需要定制或者设置, 你可以使用一个闭包或者一个全局函数。一旦一个属性所属的新的实例被初始化, 闭包或者函数就会调用, 然后返回值被赋值给属性的默认值。

这些闭包或者函数特别创建和属性相同类型的一个临时值, 返回的临时值用来作为这个属性的默认值。

这里有一个框架, 来展示一个闭包如何用来提供默认属性值:

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 属性, 或是调用任何实例方法。

下面的例子定义了一个结构体 Chessboard, 它为象棋游戏模拟了一个棋盘。象棋是在一个 8 x 8 的去棋盘上玩的, 带着交替的黑白方格。


为了表示这个游戏棋盘, Chessboard 结构体有一个属性 boardColors, 它是一个带有64个布尔值的数组。真值代表黑色方块,假值代表白色方块。第一项代表棋盘的左上方块,最后一项代表棋盘的右下方块。

boardColors 数组使用一个闭包初始化,设定它的颜色值:

struct Chessboard {

let boardColors: [Bool] = {

var temporaryBoard = [Bool]()

var isBlack = false

for i in 1...8 {

     for j in 1...8 {

     temporaryBoard.append(isBlack)

      isBlack = !isBlack

         }

      isBlack = !isBlack

    }

return temporaryBoard

    }()

func squareIsBlackAt(row: Int, column: Int) -> Bool {

return boardColors[(row * 8) + column]

   }

}

一旦新的Chessboard 实例创建, 闭包就会执行, boardColors 默认值会计算和返回。上面例子里的闭包在一个临时数组temporaryBoard 为棋盘上每个方块计算和设置了对应的颜色值, 并且等设置完成就作为闭包返回值返回。返回数组值存储在 boardColors 中,并且可以用squareIsBlackAtRow 功能函数进行查询:

let board = Chessboard()

print(board.squareIsBlackAt(row: 0, column: 1))

// 打印 "true"

print(board.squareIsBlackAt(row: 7, column: 7))

// 打印 "false"

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

推荐阅读更多精彩内容