1、隐式解析可选类型
有时候在程序架构中,第一次被赋值之后,可以确定一个可选类型总会有值,这时候每次使用时都判断和解析可选值是非常低效的,因为可以确定它总会有值。
把想要用作可选的类型的后面的问号(String?)改成感叹号(String!)来声明一个隐式解析可选类型。
一个隐式解析可选类型就是一个普通的可选类型,但是可以当做非可选类型使用,不需要每次都是用解析来获取可选值。
如下例子展示了可选类型String和隐式解析可选类型String的区别:
let possibleString: String? = "An optional string."
let forcedString: String! = possibleString! // 需要感叹号来获取值
let assumedString: String! = "An implicity unwrapped optional string."
let implicitString: String = assumedString // 不需要感叹号
可以把隐式解析可选类型当做普通可选类型来判断它是否包含值:
if assumedString != nil {
print(assumedString!) // 输出:"An implicity unwrapped optional string."
}
也可以在可选绑定中使用隐式解析可选类型来检查并解析它的值:
if let definiteString = assumedString {
print(definiteString) // 输出:"An implicity unwrapped optional string."
}
2、输入输出参数
定义一个输入输出参数时,在参数定义前加 inout 关键字。
一个输入输出参数有传入函数的值,这个值被函数修改,然后被传出函数,替换原来的值。
当传入的参数作为输入输出参数时,需要在参数名前加 & 符,表示这个值可以被函数修改。
注意:输入输出参数不能有默认值,而且可变参数不能用 inout 标记
例
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let tempA = a
a = b
b = tempA
}
var someInt = 3, anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
3、闭包表达式语法的一般形式
{ (parameters) -> return type in
statements
}
4、尾随闭包
如果需要将一个很长的闭包表达式作为最后一个参数传递给函数,将这个闭包替换为尾随闭包的形式很有用。尾随闭包是一个书写在函数圆括号之后的闭包表达式,函数支持将其作为最后一个参数调用。使用尾随闭包时,不用写出它的参数标签:
func someFunctionThatTakesAClosure(closure: () -> Void) {
// 函数体部分
}
// 以下是不使用尾随闭包进行函数调用
someFunctionThatTakesAClosure(closure: {
// 闭包主体部分
})
// 以下是使用尾随闭包进行函数调用
someFunctionThatTakesAClosure() {
// 闭包主体部分
}
如果闭包表达式是函数或方法的唯一参数,当使用尾随闭包时,可以把 () 省略:
reversedNames = names.sorted { $0 > $1 }
5、逃逸闭包
当一个闭包作为参数传到一个函数中,且这个碧波啊在函数返回之后才被执行,称该闭包从函数中逃逸。
当定义接收闭包作为参数的函数时,需在参数名执勤啊标注 @escaping,用来指明这个闭包是允许“逃逸”出这个函数的。
例
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
someFunctionWithEscapingClosure(_:)函数接受一个闭包作为参数,该闭包被添加到一个函数外定义的数组中。如果不将这个参数标记为 @escaping,就会得到一个编译错误。
6、枚举
枚举的语法
enum SomeEnumeration {
// 枚举定义放在这里
}
// 例
enum CompassPoint {
case north, south
case east
case west
}
与C和Objective-C不同,Swift的枚举成员在被创建时不会被赋予一个默认的整型值。
上面的CompassPoint例中,north、south、east、west不会被隐式地赋值为0,1,2,3。这些枚举成员本身就是完备的值,这些值的类型是CompassPoint类型。
7、枚举成员的遍历
令枚举遵循 CaseIterable 协议,Swift会生成一个 allCases 属性,表示一个包含枚举所有成员的集合。例
enunm Beverage: CaseIterable {
case coffee, tea, juice
}
let numberOfChoices = Beverage.allCases.count
print("\(numberOfChoices) beverages available")
// 打印“3 beverages available”
8、枚举关联值
可以为枚举的一个成员(如Beverage.tea)设置一个常量或者变量,并在赋值之后查看这个值,这个额外的信息成为关联值。
每次使用枚举成员时,可以修改这个关联值。
如果需要的话,每个枚举成员的关联值类型可以更不相同。
例
enum Barcode {
case upc(Int, Int, Int, Int)
case qrCode(String)
}
以上代码可以这么理解:
定义一个名为 Barcode 的枚举类型,它的一个成员值是具有 (Int, Int, Int, Int) 类型关联值的 upc,另一个成员值是具有 String 类型关联值的 qrCode。
这个定义不提供任何 Int 或 String 类型的关联值,只是定义了当 Barcode 常量和变量等于 Barcode.upc 或 Barcode.qrCode 时,可以存储的关联值的类型。
然后可以使用任意一种条形码类型创建新的条形码,如
var productBarcode = Barcode.upc(8, 85909, 51226, 3)
上例创建了一个名为 productBarcode 的变量,并将 Barcode.upc 赋值给它,关联的元组值为 (8, 85909, 51226, 3)。
枚举关联值与Switch结合使用:
可以在 switch 的 case 分支代码中提取每个关联值作为一个常量(用 let 前缀)或者作为一个变量(用 var 前缀)来使用:
switch productBarcode {
case .upc(let numberSystem, let manufacturer, let product, let check):
print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case .qrCode(let productCode):
print("QR code: \(productCode).")
}
如果一个枚举成员的所有关联值都被提取为常量,或者都被提取为变量,为了简洁,你可以只在成员名称前标注一个 let 或者 var:
switch productBarcode {
case let .upc(numberSystem, manufacturer, product, check):
print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case let .qrCode(productCode):
print("QR code: \(productCode).")
}
9、枚举原始值
作为关联值的替代选择,枚举成员可以被默认值(成为原始值)预填充,原始值类型必须相同。
原始值可以是字符串、字符,或者任意整型值或浮点型值。每个原始值在枚举声明中必须是唯一的。
例
enum ASCIIControlCharacter: Character {
case tab = "\t"
case lineFeed = "\n"
case carriageReturn = "\r"
}
注意
原始值和关联值是不同的。原始值是在定义枚举时被预先填充的值,像上述三个 ASCII 码。对于一个特定的枚举成员,它的原始值始终不变。关联值是创建一个基于枚举成员的常量或变量时才设置的值,枚举成员的关联值可以变化。
枚举原始值的隐式赋值
在使用原始值为整数或者字符串类型的枚举时,不需要显式地为每一个枚举成员设置原始值,Swift 将会自动为你赋值。
例如,当使用整数作为原始值时,隐式赋值的值依次递增 1。如果第一个枚举成员没有设置原始值,其原始值将为 0。
当使用字符串作为枚举类型的原始值时,每个枚举成员的隐式原始值为该枚举成员的名称。
使用枚举成员的 rawValue 属性可以访问该枚举成员的原始值。
枚举原始值初始化枚举实例
如果在定义枚举类型的时候使用了原始值,那么将会自动获得一个初始化方法,这个方法接收一个叫做 rawValue 的参数,参数类型即为原始值类型,返回值则是枚举成员或 nil。你可以使用这个初始化方法来创建一个新的枚举实例。
例,利用整型原始值表示每个行星在太阳系中的顺序:
enum Planet: Int {
case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}
// Plant.mercury 的显式原始值为 1,Planet.venus 的隐式原始值为 2,依次类推。
这个列子使用原始值7创建了枚举成员 Uranus:
let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet 类型为 Planet? 值为 Planet.uranus
注意
原始值构造器是一个可失败构造器,因为并不是每一个原始值都有与之对应的枚举成员。
10、递归枚举
递归枚举是一种枚举类型,它有一个或多个枚举成员使用该枚举类型的实例作为关联值。使用递归枚举时,编译器会插入一个间接层。你可以在枚举成员前加上 indirect 来表示该成员可递归,也可以在枚举类型开头加上 indirect 关键字来表明它的所有成员都是可递归的。
11、结构体和类
结构体和类的共同点
定义属性用于存储值
定义方法用于提供功能
定义下标操作用于通过下标语法访问它们的值
定义构造器用于设置初始值
通过扩展以增加默认实现之外的功能
遵循协议以提供某种标准功能
与结构体相比,类还有如下的附加功能:
继承允许一个类继承另一个类的特征
类型转换允许在运行时检查和解释一个类实例的类型
析构器允许一个类实例释放任何其所被分配的资源
引用计数允许对一个类的多次引用
优先使用结构体,因为它更容易理解。
结构体和类--类型定义的语法
struct SomeStructure {
// 在这里定义结构体
}
class SomeClass {
// 在这里定义类
}
Swift 中请使用驼峰命名法,其中类型首字母大写,属性和方法首字母小写。
定义结构体和定义类的示例:
struct Resolution {
var width = 0
var height = 0
}
class VideoMode {
var resolution = Resolution()
var interlaced = false
var frameRate = 0.0
var name: String?
}
结构体类型的成员逐一构造器
结构体有一个自动生成的成员逐一构造器,用于初始化新结构体实例中成员的属性。新实例中各个属性的初始值可以通过属性的名称传递到成员逐一构造器之中:
let vga = Resolution(width: 640, height: 480)
类实例没有默认的成员逐一构造器。
结构体和枚举是值类型
值类型:当它被赋值给一个变量、常量或者被传递给一个函数的时候,其值会被拷贝。
Swift 中所有的基本类型:整数(integer)、浮点数(floating-point number)、布尔值(boolean)、字符串(string)、数组(array)和字典(dictionary),都是值类型,其底层也是使用结构体实现的。
Swift 中所有的结构体和枚举类型都是值类型,它们的实例,以及实例中所包含的任何值类型的属性,在代码中传递的时候都会被复制。
注意
标准库定义的集合,例如数组,字典和字符串,都对复制进行了优化以降低性能成本。新集合不会立即复制,而是跟原集合共享同一份内存,共享同样的元素。在集合的某个副本要被修改前,才会复制它的元素。而你在代码中看起来就像是立即发生了复制。
类是引用类型
与值类型不同,引用类型在被赋予到一个变量、常量或者被传递到一个函数时,其值不会被拷贝。因此,使用的是已存在实例的引用,而不是其拷贝。
恒等运算符
相同(===)
不相同(!==)
可以使用这两个运算符检测两个常量或者变量是否引用了同一个实例。
注意,“相同”(用三个等号表示,===)与“等于”(用两个等号表示,==)的不同。“相同”表示两个类类型(class type)的常量或者变量引用同一个类实例。“等于”表示两个实例的值“相等”或“等价”,判定时要遵照设计者定义的评判标准。
Choosing Between Structures and Classes(在结构和类之间进行选择)
考虑结构体是值类型,类是引用类型。
12、属性
属性将值与特定的类、结构体或枚举关联。
存储属性会将常量和变量存储为实例的一部分,而计算属性则是直接计算(而不是存储)值。
计算属性可以用于类、结构体和枚举,而存储属性只能用于类和结构体。
属性也可以直接与类型本身关联,这种属性称为类型属性。
存储属性
存储属性就是存储在特定类或结构体实例里的一个常量或变量。存储属性可以是变量存储属性(用关键字 var 定义),也可以是常量存储属性(用关键字 let 定义)。
可以在定义存储属性的时候指定默认值,请参考 默认构造器 。也可以在构造过程中设置或修改存储属性的值,甚至修改常量存储属性的值,请参考 构造过程中常量属性的修改 。
常量结构体实例的存储属性
如果创建了一个结构体实例并将其赋值给一个常量,则无法修改该实例的任何属性,即使被声明为可变属性也不行。
因为结构体属于值类型。当值类型的实例被声明为常量的时候,它的所有属性也就成了常量。
属于引用类型的类则不一样。把一个引用类型的实例赋给一个常量后,依然可以修改该实例的可变属性。
延时加载存储属性
在属性声明前使用 lazy 来标示一个延时加载存储属性。
注意
必须将延时加载属性声明成变量(使用 var 关键字),因为属性的初始值可能在实例构造完成之后才会得到。而常量属性在构造过程完成之前必须要有初始值,因此无法声明成延时加载。
计算属性
类、结构体和枚举可以定义计算属性。计算属性不直接存储值,而是提供一个 getter 和一个可选的 setter,来间接获取和设置其他属性或变量的值。
例
struct Point {
var x = 0.0, y = 0.0
}
struct Size {
var width = 0.0, height = 0.0
}
struct Rect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set(newCenter) {
origin.x = newCenter.x - (size.width / 2)
origin.y = newCenter.y - (size.height / 2)
}
}
}
var square = Rect(origin: Point(x: 0.0, y: 0.0),
size: Size(width: 10.0, height: 10.0))
let initialSquareCenter = square.center
square.center = Point(x: 15.0, y: 15.0)
print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// 打印“square.origin is now at (10.0, 10.0)”
简化 Setter 声明
如果计算属性的 setter 没有定义表示新值的参数名,则可以使用默认名称 newValue。
例
struct AlternativeRect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set {
origin.x = newValue.x - (size.width / 2)
origin.y = newValue.y - (size.height / 2)
}
}
}
简化 Getter 声明
如果整个 getter 是单一表达式,getter 会隐式地返回这个表达式结果。
例
struct CompactRect {
var origin = Point()
var size = Size()
var center: Point {
get {
Point(x: origin.x + (size.width / 2),
y: origin.y + (size.height / 2))
}
set {
origin.x = newValue.x - (size.width / 2)
origin.y = newValue.y - (size.height / 2)
}
}
}
在 getter 中忽略 return
与在函数中忽略 return
的规则相同,参考 隐式返回的函数。
只读计算属性
只有 getter 没有 setter 的计算属性叫只读计算属性。只读计算属性总是返回一个值,可以通过点运算符访问,但不能设置新的值。
注意
必须使用 var 关键字定义计算属性,包括只读计算属性,因为它们的值不是固定的。let 关键字只用来声明常量属性,表示初始化后再也无法修改的值。
只读计算属性的声明可以去掉 get 关键字和花括号:
struct Cuboid {
var width = 0.0, height = 0.0, depth = 0.0
var volume: Double {
return width * height * depth
}
}
let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")
// 打印“the volume of fourByFiveByTwo is 40.0”
属性观察器
属性观察器监控和响应属性值的变化,每次属性被设置值的时候都会调用属性观察器,即使新值和当前值相同的时候也不例外。
可以在以下位置添加属性观察器:
- 自定义的存储属性
- 继承的存储属性
- 继承的计算属性
对于继承的属性,你可以在子类中通过重写属性的方式为它添加属性观察器。对于自定义的计算属性来说,使用它的 setter 监控和响应值的变化。
可以为属性添加其中一个或两个观察器: - willSet 在新的值被设置之前调用
- didSet 在新的值被设置之后调用
willSet 观察期会将新的属性值作为常量参数传入,在 willSet 的实现代码中可以为这个参数指定一个名称,如果不指定则参数仍然可用,这时使用默认名称 newValue 表示。
同样,didSet 观察器会将旧的属性值作为参数传入,可以为该参数指定一个名称或者使用默认参数名 oldValue。如果在 didSet 方法中再次对该属性赋值,那么新值会覆盖旧的值。
注意:在父类初始化方法调用之后,在子类构造器中给父类的属性赋值时,会调用父类属性的 willSet 和 didSet 观察器。而在父类初始化方法调用之前,给子类的属性赋值时不会调用子类属性的观察器。
例
class StepCounter {
var totalSteps: Int = 0 {
willSet(newTotalSteps) {
print("将 totalSteps 的值设置为 \(newTotalSteps)")
}
didSet {
if totalSteps > oldValue {
print("增加了 \(totalSteps - oldValue) 步")
}
}
}
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// 将 totalSteps 的值设置为 200
// 增加了 200 步
stepCounter.totalSteps = 360
// 将 totalSteps 的值设置为 360
// 增加了 160 步
stepCounter.totalSteps = 896
// 将 totalSteps 的值设置为 896
// 增加了 536 步
属性包装器
属性包装器这个暂时没有理解好!
全局变量和局部变量
全局变量是在函数、方法、闭包或任何类型之外定义的变量。局部变量是在函数、方法或闭包内部定义的变量。
注意:全局的常量或变量都是延迟计算的,跟 延时加载存储属性相似,不同的地方在于,全局的常量或变量不需要标记
lazy
修饰符。
局部范围的常量和变量从不延迟计算。
类型属性
实例属性属于一个特定类型的实例,每创建一个实例,实例都拥有属于自己的一套属性值,实例之间的属性相互独立。
可以为类型本身定义属性,无论创建了多少个该类型的实例,这些属性都只有唯一一份。这种属性就是类型属性。
类型属性用于定义某个类型所有实例共享的数据。
存储型类型属性可以是变量或常量,计算型类型属性跟实例的计算型属性一样只能定义成变量属性。
注意:跟实例的存储型属性不同,必须给存储型类型属性指定默认值,因为类型本身没有构造器,也就无法在初始化过程中使用构造器给类型属性赋值。
存储型类型属性是延迟初始化的,它们只有在第一次被访问的时候才会被初始化。即使它们被多个线程同时访问,系统也保证只会对其进行一次初始化,并且不需要对其使用 lazy 修饰符。
类型属性语法
在 C 或 Objective-C 中,与某个类型关联的静态常量和静态变量,是作为 global(全局)静态变量定义的。
Swift 中,类型属性是作为类型定义的一部分写在类型最外层的花括号内,因此它的作用范围也就在类型支持的范围内。
使用关键字 static 来定义类型属性。在为类定义计算型类型属性时,可以改用关键字 class 来支持子类对父类的实现进行重写。
下面的例子演示了存储型和计算型类型属性的语法:
struct SomeStructure {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 1
}
}
enum SomeEnumeration {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 6
}
}
class SomeClass {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 27
}
class var overrideableComputedTypeProperty: Int {
return 107
}
}
获取和设置类型属性的值
跟实例属性一样,类型属性也是通过点运算符来访问。但是,类型属性是通过类型本身来访问,而不是通过实例。比如:
print(SomeStructure.storedTypeProperty)
// 打印“Some value.”
SomeStructure.storedTypeProperty = "Another value."
print(SomeStructure.storedTypeProperty)
// 打印“Another value.”
print(SomeEnumeration.computedTypeProperty)
// 打印“6”
print(SomeClass.computedTypeProperty)
// 打印“27”
13、方法
方法是与某些特定类型相关联的函数。
实例方法
实例方法是属于某个特定类、结构体或者枚举类型实例的方法。实例方法提供访问和修改实例属性的方法或提供与实例目的相关的功能,并以此来支撑实例的功能。实例方法的语法与函数完全一致。
self属性
类型的每一个实例都有一个隐含属性叫做 self,self 完全等同于该实例本身。
不必在你的代码里面经常写 self,只要在一个方法中使用一个已知的属性或者方法名称,如果你没有明确地写 self,Swift 假定你是指当前实例的属性或者方法。
使用这条规则的主要场景是实例方法的某个参数名称与实例的某个属性名称相同的时候。在这种情况下,参数名称享有优先权,并且在引用属性时必须使用一种更严格的方式。这时你可以使用 self 属性来区分参数名称和属性名称。
在实例方法中修改值类型
结构体和枚举是值类型。默认情况下,值类型的属性不能在它的实例方法中被修改。
如果你确实需要在某个特定的方法中修改结构体或者枚举的属性,你可以为这个方法选择 可变(mutating)行为,然后就可以从其方法内部改变它的属性;并且这个方法做的任何改变都会在方法执行结束时写回到原始结构中。
方法还可以给它隐含的 self 属性赋予一个全新的实例,这个新实例在方法结束时会替换现存实例。
要使用 可变方法,将关键字 mutating 放到方法的 func 关键字之前就可以了:
struct Point {
var x = 0.0, y = 0.0
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
}
var somePoint = Point(x: 1.0, y: 1.0)
somePoint.moveBy(x: 2.0, y: 3.0)
print("The point is now at (\(somePoint.x), \(somePoint.y))")
// 打印“The point is now at (3.0, 4.0)”
注意,不能在结构体类型的常量(a constant of structure type)上调用可变方法,因为其属性不能被改变,即使属性是变量属性。
let fixedPoint = Point(x: 3.0, y: 3.0)
fixedPoint.moveBy(x: 2.0, y: 3.0)
// 这里将会报告一个错误
在可变方法中给 self 赋值
可变方法能够赋给隐含属性 self 一个全新的实例。上面 Point 的例子可以用下面的方式改写:
struct Point {
var x = 0.0, y = 0.0
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
self = Point(x: x + deltaX, y: y + deltaY)
}
}
新版的可变方法 moveBy(x:y:) 创建了一个新的结构体实例,它的 x 和 y 的值都被设定为目标值。调用这个版本的方法和调用上个版本的最终结果是一样的。
枚举的可变方法(mutating)
枚举的可变方法可以把 self 设置为同一枚举类型中不同的成员:
enum TriStateSwitch {
case off, low, high
mutating func next() {
switch self {
case .off:
self = .low
case .low:
self = .high
case .high:
self = .off
}
}
}
var ovenLight = TriStateSwitch.low
ovenLight.next()
// ovenLight 现在等于 .high
ovenLight.next()
// ovenLight 现在等于 .off
类型方法
在类型本身上调用的方法,这种方法就叫做类型方法。
在方法的 func 关键字之前加上关键字 static,来指定类型方法。
类还可以用关键字 class 来指定,从而允许子类重写父类该方法的实现。
注意:在 Swift 中,你可以为所有的类、结构体和枚举定义类型方法。每一个类型方法都被它所支持的类型显式包含。
类型方法和实例方法一样用点语法调用。但是,你是在类型上调用这个方法,而不是在实例上调用。
例
class SomeClass {
class func someTypeMethod() {
// 在这里实现类型方法
}
}
SomeClass.someTypeMethod()
在类型方法的方法体(body)中,self 属性指向这个类型本身,而不是类型的某个实例。
这意味着你可以用 self 来消除类型属性和类型方法参数之间的歧义。
14、下标
下标可以定义在类、结构体和枚举中,是访问集合、列表或序列中元素的快捷方式。
可以使用下标的索引,设置和获取值,而不需要再调用对应的存取方法。
举例来说,用下标访问一个 Array 实例中的元素可以写作 someArray[index],访问 Dictionary 实例中的元素可以写作 someDictionary[key]。
下标语法
subscript(index: Int) -> Int {
get {
// 返回一个适当的 Int 类型的值
}
set(newValue) {
// 执行适当的赋值操作
// newValue可以不指定,setter 会提供一个名为 newValue 的默认参数。
}
}
15、继承
一个类可以继承另一个类的方法,属性和其它特性。当一个类继承其它类时,继承类叫子类,被继承类叫超类(或父类)。在 Swift 中,继承是区分「类」与其它类型的一个基本特征。
在 Swift 中,类可以调用和访问超类的方法、属性和下标,并且可以重写这些方法,属性和下标来优化或修改它们的行为。
基类
不继承于其它类的类,称之为基类。
重写
子类可以为继承来的实例方法,类方法,实例属性,类属性,或下标提供自己定制的实现。这种行为叫重写。
如果要重写某个特性,你需要在重写定义的前面加上 override
关键字。
缺少 override
关键字的重写都会在编译时被认定为错误。
防止重写
可以通过把方法,属性或下标标记为 final 来防止它们被重写,只需要在声明关键字前加上 final 修饰符即可(例如:final var、final func、final class func 以及 final subscript)。
可以通过在关键字 class 前添加 final 修饰符(final class)来将整个类标记为 final 。
16、构造过程
构造过程是使用类、结构体或枚举类型的实例之前的准备过程。在新实例使用前这个过程是必须的,它包括设置实例中每个存储属性的初始值和执行其他必须的设置或构造过程。
Swift 的构造器没有返回值。它们的主要任务是保证某种类型的新实例在第一次使用前完成正确的初始化。
存储属性的初始赋值
类和结构体在创建实例时,必须为所有存储型属性设置合适的初始值。存储型属性的值不能处于一个未知的状态。
可以在构造器中为存储型属性设置初始值,也可以在定义属性时分配默认值。
注意:当你为存储型属性分配默认值或者在构造器中设置初始值时,它们的值是被直接设置的,不会触发任何属性观察者。
构造器的更多知识参见--->构造过程
通过闭包或函数设置属性的默认值
如果某个存储型属性的默认值需要一些自定义或设置,你可以使用闭包或全局函数为其提供定制的默认值。每当某个属性所在类型的新实例被构造时,对应的闭包或函数会被调用,而它们的返回值会当做默认值赋值给这个属性。
例
class SomeClass {
let someProperty: SomeType = {
// 在这个闭包中给 someProperty 创建一个默认值
// someValue 必须和 SomeType 类型相同
return someValue
}()
}
我常用的
lazy var tableView: UITableView = {() -> UITableView in
let frame = CGRect(x: 0.0, y: swiftNavHeight, width: swiftScreenWidth, height: swiftScreenHeight - swiftNavHeight)
let tmpV: UITableView = UITableView(frame: frame, style: .plain)
tmpV.dataSource = self
tmpV.delegate = self
tmpV.estimatedRowHeight = CGFloat(TTJoinTeamInputCell.cellHeight)
return tmpV
}()
17、析构过程
析构器只适用于类类型,当一个类的实例被释放之前,析构器会被立即调用。
析构器用关键字 deinit 来标示,类似于构造器要用 init 来标示。
可选链
可选链式调用是一种可以在当前值可能为 nil 的可选值上请求和调用属性、方法及下标的方法。如果可选值有值,那么调用就会成功;如果可选值是 nil,那么调用将返回 nil。多个调用可以连接在一起形成一个调用链,如果其中任何一个节点为 nil,整个调用链都会失败,即返回 nil。
注意:Swift 的可选链式调用和 Objective-C 中向 nil 发送消息有些相像,但是 Swift 的可选链式调用可以应用于任意类型,并且能检查调用是否成功。
可选链式调用的返回结果与原本的返回结果具有相同的类型,但是被包装成了一个可选值。
例如,使用可选链式调用访问属性,当可选链式调用成功时,如果属性原本的返回结果是 Int 类型,则会变为 Int? 类型。
通过可选链式调用来调用方法
可以通过可选链是调用来调用方法,并判断是否调用成功,即使这个方法没有返回值。
没有返回值的方法具有隐式的返回类型Void,该值为一个空元祖,写成()。
下面的例子,通过判断返回值是否为nil,来判断赋值是否成功:
if (john.residence?.address = someAddress) != nil {
print("It was possible to set the address.")
} else {
print("It was not possible to set the address.")
}
// 打印“It was not possible to set the address.”
通过可选链式调用访问下标
通过可选链式调用,我们可以在一个可选值上访问下标,并且判断下标调用是否成功。
通过可选链式调用访问可选值的下标时,应该将问号放在下标方括号的前面而不是后面。可选链式调用的问号一般直接跟在可选表达式的后面。
例
if let firstRoomName = john.residence?[0].name {
print("The first room name is \(firstRoomName).")
} else {
print("Unable to retrieve the first room name.")
}
// 打印“Unable to retrieve the first room name.”
18、错误处理
错误处理(Error handling) 是响应错误以及从错误中恢复的过程。Swift 在运行时提供了抛出、捕获、传递和操作可恢复错误(recoverable errors)的一等支持(first-class support)。
注意:Swift 中的错误处理涉及到错误处理模式,这会用到 Cocoa 和 Objective-C 中的 NSError。
表示与抛出错误
Swift 中,错误用遵循 Error 协议的类型的值来表示。这个空协议表明该类型可以用于错误处理。
Swift 的枚举类型尤为适合构建一组相关的错误状态,枚举的关联值还可以提供错误状态的额外信息。
例如,在游戏中操作自动贩卖机时,你可以这样表示可能会出现的错误状态:
enum VendingMachineError: Error {
case invalidSelection // 选择无效
case insufficientFunds(coinsNeeded: Int) // 金额不足
case outOfStock // 缺货
}
抛出一个错误可以让你表明有意外情况发生,导致正常的执行流程无法继续执行。抛出错误使用 throw 语句。
例如,下面的代码抛出一个错误,提示贩卖机还需要 5 个硬币:
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
处理错误
某个错误被抛出时,附近的某部分代码必须负责处理这个错误。
Swift 中有 4 种处理错误的方式:
1)把函数抛出的错误传递给调用此函数的代码
2)用 do-catch 语句处理错误
3)将错误作为可选类型处理
4)或者断言此错误根本不会发生。
用 throwing 函数传递错误
为了表示一个函数、方法或构造器可以抛出错误,在函数声明的参数之后加上 throws 关键字。
一个标有 throws 关键字的函数被称作 throwing 函数。
如果这个函数指明了返回值类型,throws 关键词需要写在返回箭头(->)的前面。
func canThrowErrors() throws -> String
func cannotThrowErrors() -> String
一个 throwing 函数可以在其内部抛出错误,并将错误传递到函数被调用时的作用域。
在调用抛出错误的方法的地方,必须要么直接处理这些错误--使用 do-catch 语句,try? 或 try!;要么继续将这些错误传递下去。
注意:只有 throwing 函数可以传递错误,非 throwing 函数内部抛出的错误只能在函数内部处理。
用 Do-Catch 处理错误
可以用一个 do-catch 语句运行一段闭包代码来处理错误。
如果在 do 子句中的代码抛出了一个错误,这个错误会与 catch 子句匹配,从而决定哪条子句处理它。
do-catch 语句的一般形式:
do {
try expression
statements
} catch pattern 1 {
statements
} catch pattern 2 where condition {
statements
} catch pattern 3, pattern 4 where condition {
statements
} catch {
statements
}
在 catch 后面写一个匹配模式来表明这个子句能处理什么样的错误。
如果一条 catch 子句没有指定匹配模式,那么这条子句可以匹配任何错误,并且把错误绑定到一个名字为 error 的局部常量。
将错误转换成可选值
可以使用 try? 通过将错误转换成一个可选值来处理错误。如果是在计算 try? 表达式时抛出错误,该表达式的结果就为 nil。
例,在下面的代码中,x 和 y 有着相同的数值和等价的含义:
func someThrowingFunction() throws -> Int {
// ...
}
let x = try? someThrowingFunction()
let y: Int?
do {
y = try someThrowingFunction()
} catch {
y = nil
}
如果 someThrowingFunction() 抛出一个错误,x 和 y 的值是 nil。否则 x 和 y 的值就是该函数的返回值。
无论 someThrowingFunction() 的返回值类型是什么类型,x 和 y 都是这个类型的可选类型。
禁用错误传递
有时你知道某个 throwing 函数实际上在运行时是不会抛出错误的,可以在表达式前面写 try! 来禁用错误传递,这会把调用包装在一个不会有错误抛出的运行时断言中。如果真的抛出了错误,你会得到一个运行时错误。
例
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
指定清理操作
defer:可以使用 defer 语句在即将离开当前代码块时执行一系列语句。
defer 语句将代码的执行延迟到当前的作用域退出之前。
该语句由 defer 关键字和要被延迟执行的语句组成。
延迟执行的语句不能包含任何控制转移语句,例如 break、return 语句,或是抛出一个错误。延迟执行的操作会按照它们声明的顺序从后往前执行——也就是说,第一条 defer 语句中的代码最后才执行,第二条 defer 语句中的代码倒数第二个执行,以此类推。
func processFile(filename: String) throws {
if exists(filename) {
let file = open(filename)
defer {
close(file)
}
while let line = try file.readline() {
// 处理文件
}
// close(file)会在这里被调用,即作用域的最后。
}
}
注意:即使没有涉及到错误处理的代码,你也可以使用 defer 语句。
19、类型转换
类型转换可以判断实例的类型,也可以将实例看做是其父类或者子类的实例。
类型转换在 Swift 中使用 is 和 as 操作符实现。这两个操作符分别提供了一种简单达意的方式去检查值的类型或者转换它的类型。
检查类型
用类型检查操作符(is)来检查一个实例是否属于特定子类型。若实例属于那个子类型,类型检查操作符返回 true,否则返回 false。
向下转型
当一个常量或变量可能在幕后属于一个子类,可以尝试用类型转换操作符(as? 或 as!)向下转到它的子类型。
向下转型可能会失败,类型转型操作符带有两种不同形式。
条件形式 as? 返回一个你试图向下转成的类型的可选值。
强制形式 as! 把试图向下转型和强制解包转换结果结合为一个操作。
Any 和 AnyObject 的类型转换
Swift 为不确定类型提供了两种特殊的类型别名:
- Any 可以表示任何类型,包括函数类型。
- AnyObject 可以表示任何类类型的实例。
注意:Any 类型可以表示所有类型的值,包括可选类型。Swift 会在你用 Any 类型来表示一个可选值的时候,给你一个警告。如果你确实想使用 Any 类型来承载可选值,你可以使用 as 操作符显式转换为 Any,如下所示:
let optionalNumber: Int? = 3
things.append(optionalNumber) // 警告
things.append(optionalNumber as Any) // 没有警告
20、嵌套类型
要在一个类型中嵌套另一个类型,将嵌套类型的定义写在其外部类型的 {} 内,而且可以根据需要定义多级嵌套。
嵌套类型实践
struct BlackjackCard {
// 嵌套的 Suit 枚举
enum Suit: Character {
case spades = "♠", hearts = "♡", diamonds = "♢", clubs = "♣"
}
// 嵌套的 Rank 枚举
enum Rank: Int {
case two = 2, three, four, five, six, seven, eight, nine, ten
case jack, queen, king, ace
struct Values {
let first: Int, second: Int?
}
var values: Values {
switch self {
case .ace:
return Values(first: 1, second: 11)
case .jack, .queen, .king:
return Values(first: 10, second: nil)
default:
return Values(first: self.rawValue, second: nil)
}
}
}
// BlackjackCard 的属性和方法
let rank: Rank, suit: Suit
var description: String {
var output = "suit is \(suit.rawValue),"
output += " value is \(rank.values.first)"
if let second = rank.values.second {
output += " or \(second)"
}
return output
}
}
引用嵌套类型
外部引用嵌套类型时,在嵌套类型的类型名前加上其外部类型的类型名作为前缀:
let heartsSymbol = BlackjackCard.Suit.hearts.rawValue
21、扩展
扩展可以给一个现有的类,结构体,枚举,还有协议添加新的功能。
它还拥有不需要访问被扩展类型源代码就能完成扩展的能力(即逆向建模)。
扩展和 Objective-C 的分类很相似。(与 Objective-C 分类不同的是,Swift 扩展是没有名字的。)
Swift 中的扩展可以:
- 添加计算型实例属性和计算型类属性
- 定义实例方法和类方法
- 提供新的构造器
- 定义下标
- 定义和使用新的嵌套类型
- 使已经存在的类型遵循(conform)一个协议
在 Swift 中,你甚至可以扩展协议以提供其需要的实现,或者添加额外功能给遵循的类型所使用。
注意:扩展可以给一个类型添加新的功能,但是不能重写已经存在的功能。
扩展的语法
使用 extension 关键字声明扩展:
extension SomeType {
// 在这里给 SomeType 添加新的功能
}
扩展可以扩充一个现有的类型,给它添加一个或多个协议。
协议名称的写法和类或者结构体一样:
extension SomeType: SomeProtocol, AnotherProtocol {
// 协议所需要的实现写在这里
}
扩展--计算型属性
扩展可以给现有类型添加计算型实例属性和计算型类属性。
例:给 Swift 内建的 Double 类型添加了五个计算型实例属性,从而提供与距离单位相关工作的基本支持:
extension Double {
var km: Double { return self * 1_000.0 }
var m: Double { return self }
var cm: Double { return self / 100.0 }
var mm: Double { return self / 1_000.0 }
var ft: Double { return self / 3.28084 }
}
let oneInch = 25.4.mm
print("One inch is \(oneInch) meters")
// 打印“One inch is 0.0254 meters”
let threeFeet = 3.ft
print("Three feet is \(threeFeet) meters")
// 打印“Three feet is 0.914399970739201 meters”
注意:扩展可以添加新的计算属性,但是它们不能添加存储属性,或向现有的属性添加属性观察者。
扩展--构造器
扩展可以给现有的类型添加新的构造器。
它使你可以把自定义类型作为参数来供其他类型的构造器使用,或者在类型的原始实现上添加额外的构造选项。
扩展--方法
扩展可以给现有类型添加新的实例方法和类方法。
例 给 Int 类型添加了一个新的实例方法叫做 repetitions:
extension Int {
func repetitions(task: () -> Void) {
for _ in 0..<self {
task()
}
}
}
扩展--可变实例方法
通过扩展添加的实例方法同样也可以修改(或 mutating(改变))实例本身。
结构体和枚举的方法,若是可以修改 self 或者它自己的属性,则必须将这个实例方法标记为 mutating,就像是改变了方法的原始实现。
例:对 Swift 的 Int 类型添加了一个新的 mutating 方法,叫做 square,它将原始值求平方:
extension Int {
mutating func square() {
self = self * self
}
}
var someInt = 3
someInt.square()
// someInt 现在是 9
扩展--下标
扩展可以给现有的类型添加新的下标。
下面的例子中,对 Swift 的 Int 类型添加了一个整数类型的下标。下标 [n] 从数字右侧开始,返回小数点后的第 n 位:
- 123456789[0] 返回 9
- 123456789[1] 返回 8
……以此类推:
extension Int {
subscript(digitIndex: Int) -> Int {
var decimalBase = 1
for _ in 0..<digitIndex {
decimalBase *= 10
}
return (self / decimalBase) % 10
}
}
746381295[0]
// 返回 5
746381295[1]
// 返回 9
746381295[2]
// 返回 2
746381295[8]
// 返回 7
如果操作的 Int 值没有足够的位数满足所请求的下标,那么下标的现实将返回 0,将好像在数字的左边补上了 0:
746381295[9]
// 返回 0,就好像你进行了这个请求:
0746381295[9]
扩展--嵌套类型
扩展可以给现有的类,结构体,还有枚举添加新的嵌套类型:
extension Int {
enum Kind {
case negative, zero, positive
}
var kind: Kind {
switch self {
case 0:
return .zero
case let x where x > 0:
return .positive
default:
return .negative
}
}
}
22、协议
协议 定义了一个蓝图,规定了用来实现某一特定任务或者功能的方法、属性,以及其他需要的东西。类、结构体或枚举都可以遵循协议,并为协议定义的这些要求提供具体实现。某个类型能够满足某个协议的要求,就可以说该类型遵循这个协议。
除了遵循协议的类型必须实现的要求外,还可以对协议进行扩展,通过扩展来实现一部分要求或者实现一些附加功能,这样遵循协议的类型就能够使用这些功能。
协议语法
协议的定义方式与类、结构体和枚举的定义非常相似:
protocol SomeProtocol {
// 这里是协议的定义部分
}
要让自定义类型遵循某个协议,在定义类型时,需要在类型名称后加上协议名称,中间以冒号(:)分隔。遵循多个协议时,各协议之间用逗号(,)分隔:
struct SomeStructure: FirstProtocol, AnotherProtocol {
// 这里是结构体的定义部分
}
若是一个类拥有父类,应该将父类放在遵循的协议名之前,以逗号分隔:
class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
// 这里是类的定义部分
}
协议--属性要求
协议可以要求遵循协议的类型提供特定名称和类型的实例属性或类型属性。
协议不指定属性是存储属性还是计算属性,它只指定属性的名称和类型。
协议还指定属性是可读的还是可读可写的。
如果协议要求属性是可读可写的,那么该属性不能是常量属性或只读的计算型属性。
如果协议只要求属性是可读的,那么该属性不仅可以是可读的,如果代码需要的话,还可以是可写的。
协议总是用 var 关键字来声明变量属性,在类型声明后加上 { set get } 来表示属性是可读可写的,可读属性则用 { get } 来表示:
protocol SomeProtocol {
var mustBeSettable: Int { get set }
var doesNotNeedToBeSettable: Int { get }
}
在协议中定义类型属性时,总是使用 static 关键字作为前缀。
当类类型遵循协议时,除了 static 关键字,还可以使用 class 关键字来声明类型属性:
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}
例 协议与遵循协议的结构体和类
protocol FullyNamed {
var fullName: String { get }
}
这是一个只有一个实例属性要求的协议,任何遵循 FullyNamed 的类型,都必须有一个可读的 String 类型的实例属性 fullName。
struct Person: FullyNamed {
var fullName: String
}
let join = Person(fullName: "John Appleseed")
class Starship: FullyNamed {
var prefix: String?
var name: String
init(name: String, prefix: String? = nil) {
self.name = name
self.prefix = prefix
}
var fullName: String {
return (prefix != nil ? prefix! + " " : "") + name
}
}
var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
协议--方法要求
协议可以要求遵循协议的类型实现某些指定的实例方法或类方法。
这些方法作为协议的一部分,像普通方法一样放在协议的定义中,但是不需要大括号和方法体。
可以在协议中定义具有可变参数的方法,和普通方法的定义方式相同。但是,不支持为协议中的方法提供默认参数。
在协议中定义类方法的时候,总是使用 static 关键字作为前缀。即使在类实现时,类方法要求使用 class 或 static 作为关键字前缀,前面的规则仍然适用:
protocol SomeProtocol {
static func someTypeMethod()
}
例
protocol RandomNumberGenerator {
func random() -> Double
}
RandomNumberGenerator 协议要求遵循协议的类型必须拥有一个名为 random, 返回值类型为 Double 的实例方法。
例:下面是一个遵循并符合 RandomNumberGenerator 协议的类。实现了一个叫做 线性同余生成器(linear congruential generator) 的伪随机数算法。
class LinearCongruentialGenerator: RandomNumberGenerator {
var lastRandom = 42.0
let m = 139968.0
let a = 3877.0
let c = 29573.0
func random() -> Double {
lastRandom = ((lastRandom * a + c).truncatingRemainder(dividingBy:m))
return lastRandom / m
}
}
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// 打印 “Here's a random number: 0.37464991998171”
print("And another one: \(generator.random())")
// 打印 “And another one: 0.729023776863283”
协议--异变方法要求
如果你在协议中定义了一个实例方法,该方法会改变遵循该协议的类型的实例,那么在定义协议时需要在方法前加 mutating 关键字。这使得结构体和枚举能够遵循此协议并满足此方法要求。
注意:实现协议中的 mutating 方法时,若是类类型,则不用写 mutating 关键字。而对于结构体和枚举,则必须写 mutating 关键字。
例:
protocol Togglable {
mutating func toggle()
}
Togglable 协议只定义了一个名为 toggle 的实例方法。
toggle() 方法将改变实例属性,从而切换遵循该协议类型的实例的状态。
enum OnOffSwitch: Togglable {
case off, on
mutating func toggle() {
switch self {
case .off:
self = .on
case .on:
self = .off
}
}
}
var lightSwitch = OnOffSwitch.off
lightSwitch.toggle()
// lightSwitch 现在的值为 .on
协议--构造器要求
协议可以要求遵循协议的类型实现指定的构造器。
你可以像编写普通构造器那样,在协议的定义里写下构造器的声明,但不需要写花括号和构造器的实体:
protocol SomeProtocol {
init(someParameter: Int)
}
协议构造器要求的类实现
可以在遵循协议的类中实现构造器,无论是作为指定构造器,还是作为便利构造器。
无论哪种情况,你都必须为构造器实现标上 required 修饰符:
class SomeClass: SomeProtocol {
required init(someParameter: Int) {
// 这里是构造器的实现部分
}
}
使用 required 修饰符可以确保所有子类也必须提供此构造器实现,从而也能遵循协议。
注意:如果类已经被标记为
final
,那么不需要在协议构造器的实现中使用required
修饰符,因为final
类不能有子类。
如果一个子类重写了父类的指定构造器,并且该构造器满足了某个协议的要求,那么该构造器的实现需要同时标注 required 和 override 修饰符:
protocol SomeProtocol {
init()
}
class SomeSuperClass {
init() {
// 这里是构造器的实现部分
}
}
class SomeSubClass: SomeSuperClass, SomeProtocol {
// 因为遵循协议,需要加上 required
// 因为继承自父类,需要加上 override
required override init() {
// 这里是构造器的实现部分
}
}
协议作为类型
尽管协议本身并未实现任何功能,但是协议可以被当做一个功能完备的类型来使用。
协议作为类型使用,有时被称作「存在类型」,这个名词来自「存在着一个类型 T,该类型遵循协议 T」。
协议可以像其他普通类型一样使用,使用场景如下:
- 作为函数、方法或构造器中的参数类型或返回值类型
- 作为常量、变量或属性的类型
- 作为数组、字典或其他容器中的元素类型
下面是将协议作为类型使用的例子:
class Dice {
let sides: Int
let generator: RandomNumberGenerator
init(sides: Int, generator: RandomNumberGenerator) {
self.sides = sides
self.generator = generator
}
func roll() -> Int {
return Int(generator.random() * Double(sides)) + 1
}
}
var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
for _ in 1...5 {
print("Random dice roll is \(d6.roll())")
}
// Random dice roll is 3
// Random dice roll is 5
// Random dice roll is 4
// Random dice roll is 5
// Random dice roll is 4
协议--委托
委托是一种设计模式,它允许类或结构体将一些需要它们负责的功能委托给其他类型的实例。
委托模式的实现很简单:定义协议来封装那些需要被委托的功能,这样就能确保遵循协议的类型能提供这些功能。
委托模式可以用来响应特定的动作,或者接收外部数据源提供的数据,而无需关心外部数据源的类型。
在扩展里添加协议遵循
在不修改源代码的情况下,可以通过扩展令已有类型遵循并符合协议。
扩展可以为已有类型添加属性、方法、下标以及构造器,因此可以符合协议中的相应要求。
有条件地遵循协议
泛型类型可能只在某些情况下满足一个协议的要求,比如当类型的泛型形式参数遵循对应协议时。
你可以通过在扩展类型时列出限制让泛型类型有条件地遵循某协议。在你采纳协议的名字后面写泛型 where 分句。
下面的扩展让 Array 类型只要在存储遵循 TextRepresentable 协议的元素时就遵循 TextRepresentable 协议。
extension Array: TextRepresentable where Element: TextRepresentable {
var textualDescription: String {
let itemsAsText = self.map {
$0.textualDescription
}
return "[" + itemsAsText.joined(separator: ", ") + "]"
}
}
let myDice = [d6, d12]
print(myDice.textualDescription)
// 打印 "[A 6-sided dice, A 12-sided dice]"
在扩展里声明采纳协议
当一个类型已经遵循了某个协议中的所有要求,却还没有声明采纳该协议时,可以通过空的扩展来让它采纳该协议:
struct Hamster {
var name: String
var textualDescription: String {
return "A hamster named \(name)"
}
}
extension Hamster: TextRepresentable {}
Hamster 的实例可以作为 TextRepresentable 类型使用:
let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// 打印 “A hamster named Simon”
使用合成实现来采纳协议
协议类型的集合
协议类型可以在数组或者字典这样的集合中使用。
协议的继承
协议能够继承一个或多个其他协议,可以在继承的协议的基础上增加新的要求。
协议的继承语法与类的继承相似,多个被继承的协议间用逗号分隔:
protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
// 这里是协议的定义部分
}
类专属的协议
通过添加 AnyObject 关键字到协议的继承列表,就可以限制协议只能被类类型采纳(以及非结构体或者非枚举的类型)。
protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
// 这里是类专属协议的定义部分
}
协议合成
要求一个类型同时遵循多个协议是很有用的。
可以使用协议组合来复合多个协议到一个要求里。
协议组合行为就和你定义的临时局部协议一样拥有构成中所有协议的需求。
协议组合不定义任何新的协议类型。
协议组合使用 SomeProtocol & AnotherProtocol 的形式。
你可以列举任意数量的协议,用和符号(&)分开。
除了协议列表,协议组合也能包含类类型,这允许你标明一个需要的父类。
下面的例子中,将 Named 和 Aged 两个协议按照上述语法组合成一个协议,作为函数参数的类型:
protocol Named {
var name: String { get }
}
protocol Aged {
var age: Int { get }
}
struct Person: Named, Aged {
var name: String
var age: Int
}
func wishHappyBirthday(to celebrator: Named & Aged) {
print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")
}
let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(to: birthdayPerson)
检查协议一致性
可以使用 类型转换 中描述的 is
和 as
操作符来检查协议一致性,即是否遵循某协议,并且可以转换到指定的协议类型。
检查和转换协议的语法与检查和转换类型是完全一样的:
- is 用来检查实例是否遵循某个协议,若遵循则返回 true,否则返回 false;
- as? 返回一个可选值,当实例遵循某个协议时,返回类型为协议类型的可选值,否则返回 nil
- 将实例强制向下转换到某个协议类型,如果强转失败,将触发运行时错误。
可选的协议要求
协议可以定义可选要求,遵循协议的类型可以选择是否实现这些要求。
在协议中使用 optional 关键字作为前缀来定义可选要求。可选要求用在你需要和 Objective-C 打交道的代码中。
协议和可选要求都必须带上 @objc 属性。标记 @objc 特性的协议只能被继承自 Objective-C 类的类或者 @objc 类遵循,其他类以及结构体和枚举均不能遵循这种协议。
使用可选要求时(例如,可选的方法或者属性),它们的类型会自动变成可选的。比如,一个类型为 (Int) -> String 的方法会变成 ((Int) -> String)?。需要注意的是整个函数类型是可选的,而不是函数的返回值。
协议扩展
协议可以通过扩展来为遵循协议的类型提供属性、方法以及下标的实现。
通过这种方式,你可以基于协议本身来实现这些功能,而无需在每个遵循协议的类型中都重复同样的实现,也无需使用全局函数。
例 扩展 RandomNumberGenerator 协议来提供 randomBool() 方法。该方法使用协议中定义的 random() 方法来返回一个随机的 Bool 值:
extension RandomNumberGenerator {
func randomBool() -> Bool {
return random() > 0.5
}
}
通过协议扩展,所有遵循协议的类型,都能自动获得这个扩展所增加的方法实现而无需任何额外修改:
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// 打印 “Here's a random number: 0.37464991998171”
print("And here's a random Boolean: \(generator.randomBool())")
// 打印 “And here's a random Boolean: true”
协议扩展可以为遵循协议的类型增加实现,但不能声明该协议继承自另一个协议。协议的继承只能在协议声明处进行指定。
协议扩展--提供默认实现
可以通过协议扩展来为协议要求的方法、计算属性提供默认的实现。
如果遵循协议的类型为这些要求提供了自己的实现,那么这些自定义实现将会替代扩展中的默认实现被使用。
注意:通过协议扩展为协议要求提供的默认实现和可选的协议要求不同。虽然在这两种情况下,遵循协议的类型都无需自己实现这些要求,但是通过扩展提供的默认实现可以直接调用,而无需使用可选链式调用。
为协议扩展添加限制条件
在扩展协议的时候,可以指定一些限制条件,只有遵循协议的类型满足这些限制条件时,才能获得协议扩展提供的默认实现。
这些限制条件写在协议名之后,使用 where 子句来描述。
23、泛型
泛型代码让你能根据自定义的需求,编写出适用于任意类型的、灵活可复用的函数及类型。
泛型函数
func swapTwoValues<T>(_ a: inout T, _ b: T) {
let temporaryA = a
a = b
b = temporaryA
}
泛型版本的函数使用占位符类型名(这里叫做 T ),而不是 实际类型名(例如 Int、String 或 Double),占位符类型名并不关心 T 具体的类型,但它要求 a 和b 必须是相同的类型,T 的实际类型由每次调用 swapTwoValues(::) 来决定。
T 是占位类型名。
泛型--类型参数
上面 swapTwoValues(::) 例子中,占位类型 T 是一个类型参数的例子,类型参数指定并命名一个占位类型,并且紧随在函数名后面,使用一对尖括号括起来(例如 <T>)。
可提供多个类型参数,将它们都写在尖括号中,用逗号分开。
泛型类型
类型约束
类型约束指定类型参数必须继承自指定类、遵循特定的协议或协议组合。
类型约束语法
在一个类型参数名后面放置一个类名或者协议名,并用冒号进行分隔,来定义类型约束。
下面将展示泛型函数约束的基本语法(与泛型类型的语法相同):
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// 这里是泛型函数的函数体部分
}
泛型--关联类型
关联类型为协议中的某个类型提供了一个占位符名称,其代表的实际类型在协议被遵循时才会被指定。
关联类型通过 associatedtype 关键字来指定。
关联类型实践
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
Container 协议定义了三个任何遵循该协议的类型(即容器)必须提供的功能:
- 必须可以通过 append(_:) 方法添加一个新元素到容器里。
- 必须可以通过 count 属性获取容器中元素的数量,并返回一个 Int 值。
- 必须可以通过索引值类型为 Int 的下标检索到容器中的每一个元素。
泛型 Where 语句
可以通过定义一个泛型 where 子句来实现。通过泛型 where 子句让关联类型遵从某个特定的协议,以及某个特定的类型参数和关联类型必须类型相同。
可以通过将 where 关键字紧跟在类型参数列表后面来定义 where 子句,where 子句后跟一个或者多个针对关联类型的约束,以及一个或多个类型参数和关联类型间的相等关系。
可以在函数体或者类型的大括号之前添加 where 子句。
24、不透明类型
参考不透明类型
25、自动引用计数
注意:引用计数仅仅应用于类的实例。结构体和枚举类型是值类型,不是引用类型,也不是通过引用的方式存储和传递。
类实例之间的循环强引用
如果两个类实例互相持有对方的强引用,因而每个实例都让对方一直存在,就是这种情况。这就是所谓的循环强引用。
解决实例之间的循环强引用
弱引用(weak reference)和无主引用(unowned reference)
弱引用和无主引用允许循环引用中的一个实例引用另一个实例而不保持强引用。这样实例能够互相引用而不产生循环强引用。
使用场景:
当其他的实例有更短的生命周期时,使用弱引用(weak),否则使用无主引用(unowned)。
弱引用
弱引用不会对其引用的实例保持强引用,不会阻止 ARC 销毁被引用的实例。这个特性阻止了引用变为循环强引用。
声明属性或者变量时,在前面加上 weak 关键字表明这是一个弱引用。
弱引用不会保持所引用的实例,即使引用存在,实例也有可能被销毁。因此,ARC 会在引用的实例被销毁后自动将其弱引用赋值为 nil。并且因为弱引用需要在运行时允许被赋值为 nil,所以它们会被定义为可选类型变量,而不是常量。
注意:当 ARC 设置弱引用为 nil 时,属性观察不会被触发。
使用实例:
class Person {
let name: String
init(name: String) {
self.name = name
}
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
weak var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
无主引用
和弱引用类似,无主引用不会牢牢保持住引用的实例。
和弱引用不同的是,无主引用在其他实例有相同或者更长的生命周期时使用。
你可以在声明属性或者变量时,在前面加上关键字 unowned 表示这是一个无主引用。
无主引用通常都被期望拥有值。不过 ARC 无法在实例被销毁后将无主引用设为 nil,因为非可选类型的变量不允许被赋值为 nil。
重点:使用无主引用,你必须确保引用始终指向一个未销毁的实例。
如果你试图在实例被销毁后,访问该实例的无主引用,会触发运行时错误。
无主引用和隐式解包可选值属性
下面的例子,是第三种场景,两个属性都必须有值,且初始化后永不会为nil,这种场景中,需要一个类使用无主属性,另一个使用隐式解包可选值属性。
这使两个属性初始化完成后能被直接访问(不需要可选解包),同时避免了循环引用。
下面的模型中,
class Country {
let name: String
var capitalCity: City!
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}
class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}
通过一条语句同时创建 Country 和 City 的实例,而不产生循环强引用,且 capitalCity 属性能被直接访问,不需要通过感叹号来解包它的可选值:
var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
// 打印“Canada's capital city is called Ottawa”
闭包的循环强引用
循环强引用是在两个类实例属性互相保持对方的强引用时产生的。
循环强引用还会发生在当你将一个闭包赋值给类实例的某个属性,且这个闭包体中又使用了这个类实例时。
闭包和类相似,都是引用类型,所以会产生循环引用。
Swift 提供了一种优雅的方法来解决这个问题,称之为 闭包捕获列表(closure capture list)。
解决闭包的循环强引用
在定义闭包时同时定义捕获列表作为闭包的一部分,通过这种方式解决闭包和类实例之间的循环强引用。
捕获列表定义了闭包体内捕获一个或多个引用类型的规则。声明每个捕获的引用为弱引用或无主引用,而不是强引用。
注意:只要在闭包内使用 self 的成员,就要用 self.someProperty 或者 self.someMethod()(而不只是 someProperty 或 someMethod())。这提醒你可能会一不小心就捕获了 self。
定义捕获列表
捕获列表中的每一项都由一对元素组成,一个元素是 weak 或 unowned 关键字,另一个元素是类实例的引用(例如 self)或初始化过的变量(如 delegate = self.delegate)。这些项在方括号中用逗号分开。
如果闭包有参数列表和返回类型,把捕获列表放在它们前面:
lazy var someClosure = {
[unowned self, weak delegate = self.delegate]
(index: Int, stringToProcess: String) -> String in
// 这里是闭包的函数体
}
如果闭包没有指明参数列表或者返回类型,它们会通过上下文推断,那么可以把捕获列表和关键字 in 放在闭包最开始的地方:
lazy var someClosure = {
[unowned self, weak delegate = self.delegate] in
// 这里是闭包的函数体
}
弱引用和无主引用
在闭包和捕获的实例总是互相引用并且总是同时销毁时,将比包内的捕获定义为无主引用。
在被捕获的引用可能变为 nil 时,将闭包内的捕获定义为弱引用。
弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动设置为nil。
注意:如果被捕获的引用绝对不会变为 nil,应该用无主引用,而不是弱引用。
26、内存安全
瞬时访问:一个访问不可能在其访问期间被其它代码访问,这就是一个瞬时访问。正常情况下,两个瞬时访问是不可能同时发生的。大多数内存访问都是瞬时的。
长期访问:在别的代码执行时持续访问,这种就是长期访问。
瞬时访问和长期访问的区别在于别的代码有没有可能在访问期间同时访问,也就是在时间线上的重叠。一个长期访问可以被别的长期访问或瞬时访问重叠。
重叠的访问主要出现在使用 in-out 参数的函数和方法或者结构体的 mutating 方法里。
In-Out 参数的访问冲突
一个函数会对它所有的 in-out 参数进行长期写访问。in-out 参数的写访问会在所有非 in-out 参数处理完之后开始,直到函数执行完毕为止。如果有多个 in-out 参数,则写访问开始的顺序与参数的顺序一致。
长期访问的存在会造成一个结果,你不能在访问以 in-out 形式传入后的原变量,即使作用域原则和访问权限允许——任何访问原变量的行为都会造成冲突。例如:
var stepSize = 1
func increment(_ number: inout Int) {
number += stepSize
}
increment(&stepSize)
// 错误:stepSize 访问冲突
stepSize 的读访问和写访问重叠了。
解决这个冲突的一种方式,是显示拷贝一份 stepSize :
// 显式拷贝
var copyOfStepSize = stepSize
increment(©OfStepSize)
// 更新原来的值
stepSize = copyOfStepSize
// stepSize 现在的值是 2
长期写访问的存在还会造成另一种结果,往同一个函数的多个 in-out 参数里传入同一个变量也会产生冲突,例如:
func balance(_ x: inout Int, _ y: inout Int) {
let sum = x + y
x = sum / 2
y = sum - x
}
var playerOneScore = 42
var playerTwoScore = 30
balance(&playerOneScore, &playerTwoScore) // 正常
balance(&playerOneScore, &playerOneScore)
// 错误:playerOneScore 访问冲突
方法里 self 的访问冲突
一个结构体的 mutating 方法会在调用期间对 self 进行写访问。此时方法内其它代码就不能对self对应的实例属性发起重叠访问。
属性的访问冲突
如结构体,元组和枚举的类型都是由多个独立的值组成的,例如结构体的属性或元组的元素。
因为它们都是值类型,修改值的任何一部分都是对于整个值的修改,意味着其中一个属性的读或写访问都需要访问整一个值。
27、访问控制
访问控制可以限定其它源文件或模块对你的代码的访问。
可以明确地给单个类型(类、结构体、枚举)设置访问级别,也可以给这些类型的属性、方法、构造器、下标等设置访问级别。协议也可以被限定在一定访问级别的范围内使用,包括协议里的全局常量、变量和函数。
模块和源文件
Swift 中的访问控制模型基于模块和源文件这两个概念。
模块指的是独立的代码单元,框架或应用程序会作为一个独立的模块来构建和发布。在 Swift 中,一个模块可以使用 import 关键字导入另外一个模块。
在 Swift 中,Xcode 的每个 target(例如框架或应用程序)都被当作独立的模块处理。如果你是为了实现某个通用的功能,或者是为了封装一些常用方法而将代码打包成独立的框架,这个框架就是 Swift 中的一个模块。当它被导入到某个应用程序或者其他框架时,框架的内容都将属于这个独立的模块。
源文件 就是 Swift 模块中的源代码文件(实际上,源文件属于一个应用程序或框架)。尽管我们一般会将不同的类型分别定义在不同的源文件中,但是同一个源文件也可以包含多个类型、函数等的定义。
访问级别
Swift 为代码中的实体提供了五种不同的访问级别。
- open 和 public 级别可以让实体被同一模块源文件中的所有实体访问,在模块外也可以通过导入该模块来访问源文件里的所有实体。通常情况下,你会使用 open 或 public 级别来指定框架的外部接口。
open 和 public 的区别:open 只能作用于类和类的成员。open 限定的类和成员能够在模块外被继承和重写。将类的访问级别显示指定为 open 表明你已经设计好了类的代码,且充分考虑过这个类在其他模块中作用于父类时的影响。 - internal 级别让实体被同一模块源文件中的任何实体访问,但是不能被模块外的实体访问。通常情况下,如果某个接口只在应用程序或框架内部使用,就可以将其设置为 internal 级别。
fileprivate 限制实体只能在其定义的文件内部访问。如果功能的部分实现细节只需要在文件内使用时,可以使用 fileprivate 来将其隐藏。 - private 限制实体只能在其定义的作用域,以及同一文件内的 extension 访问。如果功能的部分细节只需要在当前作用域内使用时,可以使用 private 来将其隐藏。
open 为最高访问级别(限制最少),private 为最低访问级别(限制最多)。
访问级别基本原则
Swift 中的访问级别遵循一个基本原则:实体不能定义在具有更低访问级别(更严格)的实体中。
例:
- 一个 public 的变量,其类型的访问级别不能是 internal,fileprivate 或是 private。因为无法保证变量的类型在使用变量的地方也具有访问权限。
- 函数的访问级别不能高于它的参数类型和返回类型的访问级别。因为这样就会出现函数可以在任何地方被访问,但是它的参数类型和返回类型却不可以的情况。
默认访问级别
如果你不显式的指定它们的访问级别,那么它们将都有一个 internal 的默认访问级别。
子类访问级别
子类的访问级别不得高于父类的访问级别。
常量、变量、属性、下标
常量、变量、属性、下标不能拥有比它们的类型更高的访问级别。
Getter 和 Setter
常量、变量、属性、下标的 Getters 和 Setters 的访问级别和它们所属类型的访问级别相同。
Setter 的访问级别可以低于对应的 Getter 的访问级别,这样就可以控制变量、属性或下标的读写权限。在 var 或 subscript 关键字之前,你可以通过 fileprivate(set),private(set) 或 internal(set) 为它们的写入权限指定更低的访问级别。
类型别名
你定义的任何类型别名都会被当作不同的类型,以便于进行访问控制。类型别名的访问级别不可高于其表示的类型的访问级别
网友评论