Swift-Tips
1.属性字符串
let string = "Test Attributed Strings"
let attributedString = NSMutableAttributedString(string: string)
let firstAttributes: [String : Any] = [NSForegroundColorAttributeName: UIColor.blue, NSBackgroundColorAttributeName: UIColor.yellow, NSUnderlineStyleAttributeName: 1]
attributedString.addAttributes(firstAttributes, range: NSRange(location: 0, length: 3))
2.Optional 实现
enum OptionalValue<T> {
case none
case some(T)
}
var possibleInteger: OptionalValue<Int> = .none
possibleInteger = .some(100)
3.可选解析
你可以包含多个可选绑定或多个布尔条件在一个 if 语句中,只要使用逗号分开就行。只要有任意一个可选绑定 的值为nil,或者任意一个布尔条件为false,则整个if条件判断为false,这时你就需要使用嵌套 if 条 件语句来处理,如下所示:
if let firstNumber = Int("4"), let secondNumber = Int("42"), firstNumber < secondNumber && secondNumber < 100 {
print("\(firstNumber) < \(secondNumber) < 100")
}
// Prints "4 < 42 < 100"
if let firstNumber = Int("4") {
if let secondNumber = Int("42") {
if firstNumber < secondNumber && secondNumber < 100 {
print("\(firstNumber) < \(secondNumber) < 100")
}
}
}
4.隐式解析可选类型
当可选类型被第一次赋值之后就可以确定之后一直有值的时候,隐式解析可选类型非常有用。
一个隐式解析可选类型其实就是一个普通的可选类型,但是可以被当做非可选类型来使用,并不需要每次都使用 解析来获取可选值。下面的例子展示了可选类型 String 和隐式解析可选类型 String 之间的区别
let possibleString: String? = "An optional string."
let forcedString: String = possibleString! // 需要感叹号来获取值
let assumedString: String! = "An implicitly unwrapped optional string."
let implicitString: String = assumedString // 不需要感叹号
注意:
如果你在隐式解析可选类型没有值的时候尝试取值,会触发运行时错误。和你在没有值的普通可选类型后面加一个惊叹号一样。
你仍然可以把隐式解析可选类型当做普通可选类型来判断它是否包含值:
if assumedString != nil {
print(assumedString)
}
// 输出 "An implicitly unwrapped optional string."
//你也可以在可选绑定中使用隐式解析可选类型来检查并解析它的值:
if let definiteString = assumedString {
print(definiteString)
}
// 输出 "An implicitly unwrapped optional string."
注意:
如果一个变量之后可能变成 nil 的话请不要使用隐式解析可选类型。如果你需要在变量的生命周期中判断是否是nil的话,请使用普通可选类型。
5.错误处理
相对于可选中运用值的存在与缺失来表达函数的成功与失败,错误处理可以推断失败的原因,并传播至程序的其他部分。
当一个函数遇到错误条件,它会抛出一个错误。调用函数的调用者能捕获该错误并进行相应处理。
一个函数可以通过在声明中添加 throws
关键字来抛出错误消息。
当你调用的函数能抛出错误时,应该在表达式中前置 try
关键字
func canThrowAnError() throws { // 这个函数有可能抛出错误
}
do {
try canThrowAnError() // 没有错误消息抛出
} catch {
// 有一个错误消息抛出
}
6.空和运算符号 ??
空合运算符 ( a ?? b )
将对 可选类型 a
进行空判断,如果 a
包含一个值就进行解封,否则就返回一个默认
值 b
。表达式 a
必须是 Optional 类型。默认值 b
的类型必须要和 a
存储值的类型保持一致。
空合运算符是对以下代码的简短表达方法:
a != nil ? a! : b
注意: 如果 a 为非空值( non-nil ),那么值 b 将不会被计算。这也就是所谓的短路求值
7.字符串
Swift 的 String
是值类型 如果您创建了一个新的字符串,那么当其进行常量、变量赋值操作,或在函数/ 方法中传递时,会进行值拷贝。 任何情况下,都会对已有字符串值创建新副本,并对该新副本进行传递或赋值操作。
Swift 默认字符串拷贝的方式保证了在函数/方法中传递的是字符串的值。 很明显无论该值来自于哪里,都是您独自拥有的。 您可以确保传递的字符串不会被修改,除非你自己去修改它。
在实际编译时,Swift 编译器会优化字符串的使用,使实际的复制只发生在绝对必要的情况下 ,这意味着在将字符串作为值类型的同时可以获得极高的性能。
遍历字符串:
您可通过 for-in
循环来遍历字符串中的 characters
属性 来获取每一个字符的值:
//Swif3 中通过 `.characters`属性来遍历
for character in "Dog!🐶". characters {
print(character)
}
// D
// o
// g
// !
// 🐶
//Swift4 中直接通过 string 进行遍历
for character in "Dog!🐶" {
print(character)
}
// D
// o
// g
// !
// 🐶
计算字符数量:
// Swift3
let unusualMenagerie = "Koala 🐨, Snail 🐌, Penguin 🐧, Dromedary 🐪" print("unusualMenagerie has \(unusualMenagerie.characters.count) characters") // 打印输出 "unusualMenagerie has 40 characters"
//Swift4
let unusualMenagerie = "Koala 🐨, Snail 🐌, Penguin 🐧, Dromedary 🐪"
print("unusualMenagerie has \(unusualMenagerie.count) characters")
// Prints "unusualMenagerie has 40 characters"
使用 characters 属性的 indices 属性会创建一个包含全部索引的范围(Range),用来在一个字符串中访问单 个字符。
let greeting = "Guten Tag!"
// Swift3
for index in greeting.characters.indices {
print("\(greeting[index]) ", terminator: "")
}
// 打印输出 "G u t e n T a g ! "
// Swift4
for index in greeting.indices {
print("\(greeting[index]) ", terminator: "")
}
// Prints "G u t e n T a g ! "
8.多行字符串 Swift4
如果需要跨多行的字符串,请使用多行字符串文字。多行字符串文字是由三个双引号包围的字符序列:
let quotation = """
The White Rabbit put on his spectacles. "Where shall I begin,
please your Majesty?" he asked.
"Begin at the beginning," the King said gravely, "and go on
till you come to the end; then stop."
"""
因为多行格式使用三个双引号而不是仅一个,所以可以在多行字符串文字中包含一个双引号( "
),如上面的示例所示。要 在多行字符串中包含 """
文字,必须使用反斜杠( \
)来转义至少一个引号。例如:
let threeDoubleQuotes = """
Escaping the first quote \"""
Escaping all three quotes \"\"\"
"""
在其多行字符串中,字符串文字包括其开头和结尾引号之间的所有行。该字符串在开头的引号( """
)之后的第一行开始,并在结束引号( """
)之前的行结束,这意味着quotation不会以换行符开头或结尾。以下两个字符串相同:
let singleLineString = "These are the same."
let multilineString = """
These are the same.
"""
要使用换行符开头或结尾的多行字符串文字,请将空行写为第一行或最后一行。例如:
"""
This string starts with a line feed.
It also ends with a line feed.
"""
9. 遍历数组
如果我们同时需要每个数据项的值和索引值,可以使用 enumerated()
方法来进行数组遍历。 enumerated()
返回 一个由每一个数据项索引值和数据值组成的元组。我们可以把这个元组分解成临时常量或者变量来进行遍历:
var list = ["Six eggs", "Milk", "Flour", "Baking Powder", "Bananas"]
for item in list {
print(item)
}
// 同时遍历每个数据项的索引和值
for (index, value) in list.enumerated() {
print("Item \(index + 1): \(value)")
}
10.集合
合类型的哈希值
一个类型为了存储在集合中,该类型必须是 可哈希化 的--也就是说,该类型必须提供一个方法来计算它的哈希值。一个哈希值是 Int
类型的,相等的对象哈希值必须相同,比如 a==b
,因此必须 a.hashValue
== b.hashValue
。
Swift 的所有基本类型(比如 String
, Int
, Double
和 Bool
)默认都是可哈希化的,可以作为集合的值的类型或者字典的键的类型。没有 关联值
的枚举成员值默认也是可哈希化的。
注意: 你可以使用你自定义的类型作为集合的值的类型或者是字典的键的类型,但你需要使你的自定义类型符合 Swift 标准库中的
Hashable
协议。符合Hashable
协议的类型需要提供一个类型为Int
的可读属性hashValue
。由类型的hashValue
属性返回的值不需要在同一程序的不同执行周期或者不同程序之间保持相同。
因为Hashable
协议符合Equatable
协议,所以遵循该协议的类型也必须提供一个"是否相等"运算符(==
)的实 现。这个 Equatable 协议要求任何符合==
实现的实例间都是一种相等的关系。也就是说,对于 a,b,c 三个值来 说, == 的实现必须满足下面三种情况:
•a == a
(自反性)
•a == b
意味着b == a
(对称性)
•a == b && b == c
意味着a == c
(传递性)
e.g:
没有遵循 Hashable
的 AClass
无法放到集合中
class AClass {
var name: String = "a"
}
let c1 = AClass()
let c2 = AClass()
var sets: Set = [c1, c2]
// error cannot convert value of type '[AClass]' to specified type 'Set'
遵循了 Hashable
的 BClass
可放到集合中
class BClass {
var name: String = "a"
}
extension BClass: Hashable {
var hashValue: Int {
return name.hashValue
}
static func ==(lhs: BClass, rhs: BClass) -> Bool {
return lhs.name == rhs.name
}
}
let b1 = BClass()
let b2 = BClass()
var sets: Set = [b1, b2]
11.Switch
不存在隐式的贯穿
与 C 和 Objective-C 中的 switch
语句不同,在 Swift 中,当匹配的 case
分支中的代码执行完毕后,程序会终止 switch
语句,而不会继续执行下一个 case
分支。这也就是说,不需要在 case
分支中显式地使用 break
语 句。这使得 switch
语句更安全、更易用,也避免了因忘记写 break
语句而产生的错误。
每一个 case
分支都必须至少包含一条语句,下面这样的代码是错误的,因为第一个 case
分支是空的
let anotherCharacter: Character = "a" switch anotherCharacter {
case "a": // 无效,这个分支下面没有语句 case "A":
print("The letter A")
default:
print("Not the letter A")
}
// 这段代码会报编译错误
为了让单个 case
同时匹配 a
和 A
,可以将这两个值组合成一个 复合匹配
,并用逗号隔开:
let anotherCharacter: Character = "a"
switch anotherCharacter {
case "a", "A":
print("The letter A")
default:
print("Not the letter A")
}
// 输出 "The letter A
区间匹配
case
分支的模式也可以是一个值的区间。下面的例子展示了如何使用区间匹配来输出任意数字对应的自然语言格式:
let approximateCount = 62
let countedThings = "moons orbiting Saturn"
var naturalCount: String
switch approximateCount {
case 0:
naturalCount = "no"
case 1..<5:
naturalCount = "a few"
case 5..<12:
naturalCount = "several"
case 12..<100:
naturalCount = "dozens of"
case 100..<1000:
naturalCount = "hundreds of"
default:
naturalCount = "many"
}
print("There are \(naturalCount) \(countedThings).") // 输出 "There are dozens of moons orbiting Saturn."
元组
我们可以使用元组在同一个 switch
语句中测试多个值。元组中的元素可以是值,也可以是区间。另外,使用下划 线( _ )来匹配所有可能的值。
let somePoint = (1, 1)
switch somePoint {
case (0, 0):
print("\(somePoint) is at the origin")
case (_, 0):
print("\(somePoint) is on the x-axis")
case (0, _):
print("\(somePoint) is on the y-axis")
case (-2...2, -2...2):
print("\(somePoint) is inside the box")
default:
print("\(somePoint) is outside of the box")
}
// Prints "(1, 1) is inside the box”
值绑定
case 分支允许将匹配的值绑定到一个临时的常量或变量,并且在case分支体内使用 —— 这种行为被称为值绑定(value binding),因为匹配的值在case分支体内,与临时的常量或变量绑定
let anotherPoint = (2, 0)
switch anotherPoint {
case (let x, 0):
print("on the x-axis with an x value of \(x)")
case (0, let y):
print("on the y-axis with a y value of \(y)")
case let (x, y):
print("somewhere else at (\(x), \(y))")
}
// 输出 "on the x-axis with an x value of 2"
Where
case 分支的模式可以使用 where
语句来判断额外的条件
let yetAnotherPoint = (1, -1)
switch yetAnotherPoint {
case let (x, y) where x == y:
print("(\(x), \(y)) is on the line x == y")
case let (x, y) where x == -y:
print("(\(x), \(y)) is on the line x == -y")
case let (x, y):
print("(\(x), \(y)) is just some arbitrary point")
}
// 输出 "(1, -1) is on the line x == -y"
复合匹配
当多个条件可以使用同一种方法来处理时,可以将这几种可能放在同一个 case
后面,并且用逗号隔开。当 case
后面的任意一种模式匹配的时候,这条分支就会被匹配。并且,如果匹配列表过长,还可以分行书写:
let someCharacter: Character = "e"
switch someCharacter {
case "a", "e", "i", "o", "u":
print("\(someCharacter) is a vowel")
case "b", "c", "d", "f", "g", "h", "j", "k", "l", "m",
"n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z":
print("\(someCharacter) is a consonant")
default:
print("\(someCharacter) is not a vowel or a consonant")
}
// 输出 "e is a vowel"
let stillAnotherPoint = (9, 0)
switch stillAnotherPoint {
case (let distance, 0), (0, let distance):
print("On an axis, \(distance) from the origin")
default:
print("Not on an axis")
}
// 输出 "On an axis, 9 from the origin"
12. fallthrough 贯穿
fallthrough 关键字不会检查它下一个将会落入执行的 case 中的匹配条件。 fallthrough 简单地使代 码继续连接到下一个 case 中的代码,这和 C 语言标准中的 switch 语句特性是一样的。
let character = "a"
switch character {
case "a":
print("One")
fallthrough
case "b":
print("Two ")
default:
print("default")
}
//One
//Two
13. 带标签的语句
在 Swift 中,你可以在循环体和条件语句中嵌套循环体和条件语句来创造复杂的控制流结构。并且,循环体和条件语句都可以使用 break
语句来提前结束整个代码块。因此,显式地指明 break
语句想要终止的是哪个循环体或者条件语句会很有用。类似地,如果你有许多嵌套的循环体,显式指明 continue
语句想要影响哪一个循环体也会非常有用。
为了实现这个目的,你可以使用标签 (statement label)
来标记一个循环体或者条件语句,对于一个条件语 句,你可以使用 break
加标签的方式来结束这个被标记的语句。对于一个循环语句,你可以使用 break
或者 continue
加标签,来结束或者继续这条被标记语句的执行。
声明一个带标签的语句是通过在该语句的关键词的同一行前面放置一个标签,作为这个语句的前导关键字 (introd ucor keyword)
,并且该标签后面跟随一个冒号。下面是一个针对 while
循环体的标签语法,同样的规则适用于所有的循环体和条件语句。
label name
: whilecondition
{statements
}
let loop1 = 0...5
let loop2 = 0...5
loop1: for l1 in loop1 {
print("loop1")
loop2: for l2 in loop2 {
print("loop2")
if l2 == 3 {
break loop1 //显式的结束loop1循环
}
}
}
/* 输出
loop1
loop2
loop2
loop2
loop2
*/
14. 枚举
关联值
你可以定义 Swift 枚举来存储任意类型的关联值,如果需要的话,每个枚举成员的关联值类型可以各不相同。枚举的这种特性跟其他语言中的 可识别联合(discriminated unions)
,标签联合(tagged unions)
,或者 变体(variants)
相似。
e.g.
定义一个名为 Barcode
的枚举类型,它的一个成员值是具有 (Int,Int,Int,Int)
类型关联值的 upc
,另一个 成员值是具有 String
类型关联值的 qrCode
。
enum Barcode {
case upc(Int, Int, Int, Int)
case qrCode(String)
}
switch 中提取关联值:
你可以在 switch
的 case
分支代码中提取每个关联值作为一个常量(用 let
前缀)或者 作为一个变量(用 var
前缀)来使用:
var productBarcode = Barcode.upc(8, 85909, 51226, 3)
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")
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).")
}
// 打印 "QR code: ABCDEFGHIJKLMNOP."
如果一个枚举成员的所有关联值都被提取为常量,或者都被提取为变量,为了简洁,你可以只在成员名称前标注一个 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).")
}
// 输出 "QR code: ABCDEFGHIJKLMNOP."
原始值
作为关联值的替代选择,枚举成员可以被默认值(称为原始值 raw values
)预填充,这些原始值的类型必须相同。
Note: 要设置原始值,枚举必须继承自一个类型:
原始值可以是字符串,字符,或者任意整型值或浮点型值。每个原始值在枚举声明中必须是唯一的。
// 这样会编译错误
enum ASCIIControlCharacter {
case tab = "\t"
case lineFeed = "\n"
case carriageReturn = "\r"
}
// 需要继承自一个类型,如 Character 才可以设置原始值
enum ASCIIControlCharacter: Character {
case tab = "\t"
case lineFeed = "\n"
case carriageReturn = "\r"
}
注意
原始值和关联值是不同的。原始值是在定义枚举时被预先填充的值,像上述三个 ASCII 码。对于一个特定的枚举成员,它的原始值始终不变。关联值是创建一个基于枚举成员的常量或变量时才设置的值,枚举成员的关联值可以变化。
原始值的隐式赋值
在使用原始值为整数或者字符串类型的枚举时,不需要显式地为每一个枚举成员设置原始值,Swift 将会自动为你赋值。
例如,当使用整数作为原始值时,隐式赋值的值依次递增 1 。如果第一个枚举成员没有设置原始值,其原始值将为0。
enum Planet: Int {
case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}
let p = Planet.mercury
p.rawValue
// p.rawValue = 0
当使用字符串作为枚举类型的原始值时,每个枚举成员的隐式原始值为该枚举成员的名称。
enum CompassPoint: String {
case north, south, east, west
}
let earthsOrder = Planet.earth.rawValue // earthsOrder 值为 3
let sunsetDirection = CompassPoint.west.rawValue // sunsetDirection 值为 "west"
使用原始值初始化枚举实例
如果在定义枚举类型的时候使用了原始值,那么将会自动获得一个初始化方法,这个方法接收一个叫做 rawValue
的参数,参数类型即为原始值类型,返回值则是枚举成员或 nil
。你可以使用这个初始化方法来创建一个新的枚举实例。
let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet 类型为 Planet? 值为 Planet.uranus
然而,并非所有 Int
值都可以找到一个匹配的行星。因此,原始值构造器总是返回一个 可选 的枚举成员。在上面的例子中, possiblePlanet
是 Planet?
类型,或者说“可选的 Planet
”。
注意
原始值构造器是一个可失败构造器,因为并不是每一个原始值都有与之对应的枚举成员。
递归枚举
递归枚举是一种枚举类型,它有一个或多个枚举成员使用该枚举类型的实例作为关联值。使用递归枚举时,编译器会插入一个间接层。你可以在枚举成员前加上 indirect
来表示该成员可递归。
例如,下面的例子中,枚举类型存储了简单的算术表达式:
enum ArithmeticExpression {
case number(Int)
indirect case addition(ArithmeticExpression, ArithmeticExpression)
indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}
你也可以在枚举类型开头加上 indirect
关键字来表明它的所有成员都是可递归的:
indirect enum ArithmeticExpression {
case number(Int)
case addition(ArithmeticExpression, ArithmeticExpression)
case multiplication(ArithmeticExpression, ArithmeticExpression)
}
/**
上面定义的枚举类型可以存储三种算术表达式:纯数字、两个表达式相加、两个表达式相乘。
枚举成员 addition 和 multiplication 的关联值也是算术表达式——这些关联值使得嵌套表达式成为可能。
例如,表达式 (5 + 4) * 2 ,乘号右边是一个数字,左边则是另一个表达式。
因为数据是嵌套的,因而用来存储数据的枚举类型也需要支持这种嵌套——这意味着枚举类型需要支持递归。
下面的代码展示了使用 ArithmeticExpression 这个递归枚举创 建表达式 (5 + 4) * 2
*/
let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))
// 要操作具有递归性质的数据结构,使用递归函数是一种直截了当的方式。例如,下面是一个对算术表达式求值的函数:
func evaluate(_ expression: ArithmeticExpression) -> Int {
switch expression {
case let .number(value):
return value
case let .addition(left, right):
return evaluate(left) + evaluate(right)
case let .multiplication(left, right):
return evaluate(left) * evaluate(right)
}
}
print(evaluate(product)) // 打印 "18"
15. 属性
延迟存储属性
延迟存储属性是指当第一次被调用的时候才会计算其初始值的属性。在属性声明前使用 lazy
来标示一个延迟存储属性。
注意
必须将延迟存储属性声明成变量(使用var
关键字),因为属性的初始值可能在实例构造完成之后才会得到。而常量属性在构造过程完成之前必须要有初始值,因此无法声明成延迟属性。
延迟属性很有用,当属性的值依赖于在实例的构造过程结束后才会知道影响值的外部因素时,或者当获得属性的初始值需要复杂或大量计算时,可以只在需要的时候计算它。
class DataImporter {
/*
DataImporter 是一个负责将外部文件中的数据导入的类。 这个类的初始化会消耗不少时间。
*/
var fileName = "data.txt"
// 这里会提供数据导入功能 }
class DataManager {
lazy var importer = DataImporter() var data = [String]()
// 这里会提供数据管理功能
}
let manager = DataManager() manager.data.append("Some data") manager.data.append("Some more data")
// DataImporter 实例的 importer 属性还没有被创建
print(manager.importer.fileName)
// DataImporter 实例的 importer 属性现在被创建了 // 输出 "data.txt”
注意
如果一个被标记为lazy
的属性在没有初始化时就同时被多个线程访问,则无法保证该属性只会被初始化一 次。
存储属性和实例变量 Stored Properties and Instance Variable
Swift 中没有实例变量这个概念
Objective-C 为类实例存储值和引用提供两种方法。除了 属性 之外,还可以使用 实例变量 作为属性值的后端存储。
Swift 编程语言中把这些理论统一用属性来实现。Swift 中的属性没有对应的实例变量,属性的后端存储也无法直接访问。这就避免了不同场景下访问方式的困扰,同时也将属性的定义简化成一个语句。属性的全部信息——包括命名、类型和内存管理特征——都在唯一一个地方(类型定义中)定义。
计算属性
除存储属性外,类、结构体和枚举可以定义计算属性。计算属性不直接存储值,而是提供一个 getter 和一个可 选的 setter,来间接获取和设置其他属性或变量的值。
struct SomeStruct {
var width = 0.0, height = 0.0
var length: Double {
get {
return width + height
}
set(newLength) {
width = newLength * 0.5
height = newLength * 0.5
}
}
}
简化 setter 声明
如果计算属性的 setter
没有定义表示新值的参数名,则可以使用默认名称 newValue
。下面是使用了简化 setter
声明的结构体代码:
struct SomeStruct {
var width = 0.0, height = 0.0
var length: Double {
get {
return width + height
}
set {
width = newValue * 0.5
height = newValue * 0.5
}
}
}
只读计算属性
只有 getter
没有 setter
的计算属性就是只读计算属性。只读计算属性总是返回一个值,可以通过点运算符访问,但不能设置新的值。
只读计算属性的声明可以去掉 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
观察器将 新的属性值 作为常量参数传入,可以为该参数指定一个名称,如果不指定参数名,则使用默认参数名 newValue
表示
didSet
观察器将 旧的属性值 作为参数传入,也可以为该参数指定一个参数名,不指定则使用默认参数名 oldValue
表示。 如果 在 didSet
方法中再次对该属性赋值,那么新值会覆盖旧的值。
注意:
父类的属性在子类的构造器中被赋值时,它在父类中的观察器会被调用,随后才会调用子类的观察器。在父类初始化方法调用之前,子类给属性赋值时,观察器不会被调用。
class StepCounter {
var totalSteps: Int = 0 {
willSet {
print("About to set totalSteps to \(newValue)")
}
didSet {
if totalSteps > oldValue {
print("Did set totalSteps: \(totalSteps) oldValue:\(oldValue)")
} else {
// 可以再次对totalSteps 进行赋值,并且不会触发属性观察器
totalSteps = 0
}
}
}
}
16.在实例方法中修改值类型
结构体和枚举是 值类型 ,默认情况下,值类型的属性不能再它的实例方法中被修改。
但是,如果确实需要在某个特定的方法中修改结构体或者枚举的属性,可以为这个方法选择 可变(mytating)
行为,然后就可以在该方法内部改变它的属性。并且这个方法的任何改变都会在方法执行结束时写回到原始的结构体中。 方法还可以给它隐含的 self
属性赋值一个全新的实例,这个新实例在方法结束时会替换现存实例。
要使用 可变
方法,将关键字 mutating
放到方法的 func
关键字之前就可以了:
也可以在可变方法中给 self
赋值,可变方法能够赋给隐含属性 self
一个全新的实例。
struct Point {
var x = 0.0, y = 0.0
mutating func moveBy(deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
mutating func reChangeSelf() {
self = Point(x: 0.0, y: 0.0)
}
}
var p = Point(x: 1.0, y: 1.0)
p.moveBy(deltaX: 2.0, y: 3.0)
p.reChangeSelf()
17.类型方法
定义在类型本身上调用的方法叫做 类型方法 。
在方法的 func
关键字之前加上关键字 static
来指定类型方法。类还要用关键字 class
来允许自雷重写父类的方法实现。
在 Swift 中,可以为所有的 类、结构体和枚举定义类型方法。每一个类型方法都能被他所支持的类型显式调用。
class SomeClass {
// class 修饰的类型方法可在子类中重写
class func someTypeMethod() {
print("Called someTypeMethod")
}
// static 修饰的类型方法不可在子类中重写
static func someTypeMethod2() {
print("Called someTypeMethod2")
}
}
SomeClass.someTypeMethod()
SomeClass.someTypeMethod2()
class SomeChildClass :SomeClass {
override class func someTypeMethod() {
print("Override Child someTypeMethod")
}
}
18.下标
下标可以定义在类、结构体和枚举中,是访问集合,列表或序列中元素的快捷方式。可以使用下标的索引设置和获取值,而不需要再调用对应的存取方法。如 通过下标访问一个 Array
实例中的元素可以写作 someArray[index]
, 访问 Dictionary
中的实例元素可以写作 someDictionary[key]
。
一个类型可以定义多个下标,通过不同索引类型进行重载。 下标不限于一维,可以定义具有多个参数的下标满足自定义类型的需求。
下标语法
下标允许通过在实例名称后面的方括号中传入一个或者多个索引值来对实例进行存取。
下标语法
与实例方法类似,定义下标使用 subscript
关键字,指定一个或多个输入参数和返回类型;下标可以设定为读写或只读, 这种行为由 getter
和 setter
实现,类似于计算属性。
subscript(index: Int) -> Int {
get {
// return an appropriate subscript value here
}
set(newValue) {
// perform a suitable setting action here
}
}
newValue
的类型和下标返回的类型相同。如同计算属性,可以不指定 setter
的参数( newValue
), 如果不指定参数, setter
提供一个名为 newValue
的默认参数.
如同只读计算属性,可以省略只读下标的 get
关键字:
subscript(index: Int) -> Int {
// 返回一个适当的 Int 类型的值
}
下面代码演示了只读下标的实现,这里定义了一个 TimesTable
结构体,用来表示传入整数的乘法表:
struct TimesTable {
let multiplier: Int
subscript(index: Int) -> Int {
return multiplier * index
}
}
let threeTimesTable = TimesTable(multiplier: 3)
print("six times three is \(threeTimesTable[6])")
//six times three is 18
19.继承
基类: 不继承于其他的类,称为基类。
Swift 中的类并不是从一个通用的基类继承而来。如果不为定义的类指定一个超类的话,这个类就自动成为基类
访问超类的方法、属性及下标
在合适的地方,可以通过使用 super
前缀来访问超类版本的方法,属性或下标:
- 在方法
someMethod()
的重写实现中,可以通过super.someMethod()
来调用超类版本的someMethod()
方法 - 在属性
someProperty
的getter
或setter
的重写实现中,可以通过super.someProperty
来访问超类版本的someProperty
属性 - 在下标的重写中,可以通过
super[someIndex]
来访问超类版本的相同下标
重写属性
可以重写属性的 实例属性 或 类型属性 ,提供自己定制的 getter
和 setter
,或添加属性观察器使重写的属性可以观察属性值什么时候发生改变。
重写属性的 Getters 和 Setters
可以提供定制的 getter
或 setter
来重写任意继承来的属性,无论继承来的属性是存储属性还是计算型属性。 子类并不知道继承来的属性是存储型还是计算型的,它只知道继承来的属性会有一个名字和类型。 在重写一个属性时,必须将它的名称和类型都写出来,这样才能是编译器去检查你重写的属性是与超类中同名同类型的属性相匹配的。
可以将一个继承来的只读属性重写为可读写属性。只需要在子类中提供 setter
和 getter
的实现即可。但是,你不能将一个继承来的读写属性重写为一个只读属性。
class SomeClass {
var name : String {
get {
return "SomeClass"
}
}
}
class SomeSubClass : SomeClass {
/// 可以将继承来的只读属性重写为一个读写属性
override var name: String {
get {
return name
}
set {
name = newValue
}
}
}
当尝试将继承来的读写属性重写为只读属性时,编译器会报错
class SomeClass {
var name : String = "SomeClass"
}
class SomeSubClass : SomeClass {
/// 不能将继承来的读写属性重写为一个只读属性
//error: cannot override mutable property with read-only property 'name'
override var name: String {
get {
return name
}
}
}
注意:
如果你在重写属性中提供了setter
,那么你也一定要提供getter
。如果你不想在重写版本中的getter
里修改继承来的属性值,你可以直接通过super.someProperty
来返回继承来的值,其中someProperty
是你要重写的属性的名字。
重写属性观察器
可以通过重写属性为一个继承来的属性添加属性观察器。这样一来,当继承来的属性值发生改变时,就会被通知到。
class SomeClass {
var name : String = "SomeClass"
}
class SomeSubClass : SomeClass {
override var name: String {
didSet {
print("Did Set")
}
willSet {
print("Will Set")
}
}
}
注意
你不可以为继承来的常量存储型属性或继承来的只读计算型属性添加属性观察器。这些属性的值是不可以被设置的,所以,为它们提供willSet
或didSet
实现是不恰当的。
此外还要注意,你不可以同时提供重写的setter
和重写的属性观察器。如果你想观察属性值的变化,并且你已经为那个属性提供了定制的setter
,那么你在setter
中就可以观察到任何值变化了。
- 子类中只重写了
getter
也不能同时重写属性观察器
编译报错:error: willSet variable may not also have a get specifier
class SomeClass {
var name : String = "SomeClass"
}
class SomeSubClass : SomeClass {
override var name: String {
//只重写 get 则该属性重写为只读属性,不会调用 set 方法, 所以 重写属性观察器的话会编译错误
get {
return super.name + "in subclass"
}
willSet {
}
}
}
- 不能同时提供重写的
setter
和重写的属性观察器:
编译报错:error: willSet variable may not also have a set specifier
class SomeClass {
var name : String = "SomeClass"
}
class SomeSubClass : SomeClass {
override var name: String {
get {
return super.name + "in subclass"
}
set {
name = newValue + "in subclass"
}
willSet {
print("will set")
}
}
}
正确的做法是,可以通过定制的 setter
观察属性的值变化
class SomeClass {
var name : String = "SomeClass"
}
class SomeSubClass : SomeClass {
override var name: String {
get {
return super.name + "in subclass"
}
set {
print("副作用...")
name = newValue + "in subclass"
}
}
}
防止重写
通过把方法,属性或下标标记为 final
来防止它们被重写,只需要在声明关键字前加上 final
修饰符即 可(例如: final var
, final func
, final class func
, 以及 final subscript
)。
如果你重写了带有 final
标记的方法,属性或下标,在编译时会报错。在类扩展中的方法,属性或下标也可以在 扩展的定义里标记为 final
的。
你可以通过在关键字 class
前添加 final
修饰符( final class
)来将整个类标记为 final
的。这样的类是不可 被继承的,试图继承这样的类会导致编译报错。
20.构造过程
构造过程 是使用类、结构体或枚举类型的实例之前的准备过程。在新实例可用之前必须执行的过程,具体操作包括:
- 设置实例中每个存储属性的初始值
- 执行其他必须的设置或者初始化工作
通过定义 构造器 来实现构造过程,这些构造器可用看做是用来创建特定类型新实例的特殊方法。 Swift 的构造器无需返回值,它们的主要任务是确保新实例在第一次使用之前完成正确的初始化
存储属性初始赋值
类和结构体 在创建实例时,必须为所有存储属性设置合适的初始值。存储属性的值不能处于一个位置的状态。
// 直接设置默认属性值
class SomeClass {
var name: String = "haha"
}
// 类SomeClass没有设置默认属性值,编译报错 error: class 'SomeStruct' has no initializers
class SomeClass {
var name: String
}
// 可以在构造器中设置默认属性值
class SomeClass {
var name: String
init() {
name = "initialName"
}
}
// 直接设置默认属性值
struct SomeStruct {
var name: String = "haha"
}
// 或通过默认构造器设置
struct SomeStruct {
var name: String
}
// 结构体SomeStruct 没有设置默认属性值,但是编译器会自动生成一个逐一成员构造器
let someStruct = SomeStruct(name: "name")
//也可以构造器中设置默认属性值
struct SomeStruct {
var name: String
init() {
name = "haha"
}
}
let someStruct = SomeStruct()
注意
在为存储属性设置默认值或者在构造器中为其赋值时,它们的值是直接被设置的,不会触发任何属性观察器
值类型的构造器代理
构造器可以通过调用其它构造器来完成实例的部分构造过程。这一过程称为构造器代理,它能减少多个构造器间
的代码重复。
构造器代理的实现规则和形式在值类型和类类型中有所不同。值类型(结构体和枚举类型)不支持继承,所以构造器代理的过程相对简单,因为它们只能代理给自己的其它构造器。类则不同,它可以继承自其它类这意味着类有责任保证其所有继承的存储型属性在构造时也能正确的初始化。
对于值类型,你可以使用 self.init
在自定义的构造器中引用相同类型中的其它构造器。并且你只能在构造器内部调用 self.init
。
如果你为某个值类型定义了一个自定义的构造器,你将无法访问到默认构造器(如果是结构体,还将无法访问逐一成员构造器)。这种限制可以防止你为值类型增加了一个额外的且十分复杂的构造器之后,仍然有人错误的使用自动生成的构造器。
struct Size {
var width = 0.0, height = 0.0
}
struct Point {
var x = 0.0, y = 0.0
}
struct Rect {
var origin = Point()
var size = Size()
// 使用默认属性来初始化
init() {}
// 使用指定的 origin 和 size 来初始化
init(origin: Point, size: Size) {
self.origin = origin
self.size = size
}
// 使用指定的 center 和 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)
}
}
注意
假如你希望默认构造器、逐一成员构造器以及你自己的自定义构造器都能用来创建实例,可以将自定义的构造器写到扩展(extension
)中,而不是写在值类型的原始定义中。
struct Rect {
var origin = Point(x: 0.0, y: 0.0)
var size = Size(width: 0.0, height: 0.0)
}
extension Rect {
// 使用指定的 center 和 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)
}
}
// 默认构造器
let r0 = Rect()
// 自定义构造器
let r1 = Rect(center: Point(x: 1.0, y: 2.0), size: Size(width: 3.0, height: 4.0))
// 逐一构造器
let r2 = Rect(origin: Point(x: 1.0, y: 2.0), size: Size(width: 3.0, height: 4.0))
21.类的继承和构造过程
类里面的所有存储型属性——包括所有继承自父类的属性——都必须在构造过程中设置初始值。
指定构造器
指定构造器是类中最主要的构造器,一个指定构造器将初始化类中提供的所有属性,并根据父类链往上调用父类的构造器来实现父类的初始化。
每一个类都必须拥有至少一个指定构造器。
便利构造器
便利构造器是类中比较次要的、辅助型的构造器。你可以定义便利构造器来调用同一个类中的指定构造器,并未其参数提供默认值。也可以定义便利构造器来创建一个特殊用途或特定输入值的实例。
类的指定构造器和便利构造器语法
指定构造器语法:
init(parameters) {
statements
}
便利构造器语法: convenience
convenience init(parameters) {
statements
}
22.通过闭包或函数设置属性的默认值
如果某个 存储属性 的默认值需要一些定制或设置,可以使用闭包或全局函数为其提供定制的默认值,每当这个属性所在的类型的新实例被创建时,对应的闭包或函数会被调用,而他们的返回值会被当做默认值赋值给这个属性。
这种类型的闭包或函数通常会创建一个跟属性类型相同的临时变量,然后修改它的值以满足预期的初始黄台,最后返回这个临时变量,作为属性的默认值。
class SomeClass {
var name: String
init(name: String) {
self.name = name
}
}
class SomeSub: SomeClass {
var age: String = {
let age = "11"
print("age = \(age)")
return age
}()
}
let sub = SomeSub(name: "haha")
//age = 11
注意闭包结尾的大括号后面接了一对空的小括号。这用来告诉 Swift 立即执行此闭包。如果你忽略了这对括号,相当于将闭包本身作为值赋值给了属性,而不是将闭包的返回值赋值给属性。
23.解决实例之间的强引用循环
类实例之间的强引用循环:
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
}
var tenant: Person?
deinit {
print("(Apartment \(unit) is being deinitialized )")
}
}
var tom: Person?
var unit4A: Apartment?
tom = Person(name: "Tom")
unit4A = Apartment(unit: "4A")
tom?.apartment = unit4A
unit4A?.tenant = tom
tom = nil
unit4A = nil
//当吧tom 和 unit4A 设置为nil 时,析构函数均不会调用,强引用循环会一直阻止 Person 和 Apartment 类实例的销毁,从而导致内存泄漏
弱引用和无主引用
Swift 提供两种方法来解决在使用类的属性时所遇到的强引用循环问题:弱引用 (weak reference
)和 无主引用 (unowned reference
)
弱引用和无主引用允许引用循环中的一个实例引用另一个实例而不保持强引用。 这样实例之间能相互引用而不产生强引用循环。
当其他的实例有更短的生命周期时,使用弱引用,即当其他实例先析构时使用弱引用。 在上面的例子中, 在一个公寓的声明周期内,会存在某段时间没有主人的情况,所以在 Apartment
类中使用弱引用来打破循环引用。 相反的,当其他实例拥有相同或者更长的声明周期的情况下,应该使用无主引用。
弱引用
弱引用指不会对其引用的实例保持强引用,因此不会阻止 ARC 销毁被引用的实例。这个特性阻止了引用变为强引用循环的一部分。在声明属性或变量时,在前面加上 weak
关键字表明这是一个弱引用。
由于弱引用并不会保持所引用的实例,即使引用存在,实例也有可能被销毁。因此,ARC 会在引用的实例被销毁后,自动将其赋值为 nil
。并且,因为弱引用允许他们的值在运行时被赋值为 nil
, 所以他们会被定义为可选类型变量而不是常量。
注意
当 ARC 设置弱引用为nil
时,属性观察器不会被触发
将 Apartment
的 tenant
属性声明成弱引用后, Person
实例依然保持对 Apartment
实例的强引用,但是 Apartment
实例只持有对 Person
实例的弱引用。这意味着当断开 tom
变量所持有的强引用时,再也没有指向 Person
实例的强引用了, 由于再也没有指向 Person
实例的强引用,该实例 (tom
) 会被销毁
tom = nil
// Tom is being deinitialized
唯一剩下执行 Apartment
实例的强引用来自 unit4A
,如果断开这个强引用,再也没有指向 Apartment
实例的强引用了, 该实例 (unit4A
)也会被销毁
unit4A = nil
//Apartment 4A is being deinitialized
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")
}
}
var tom: Person?
var unit4A: Apartment?
tom = Person(name: "Tom")
unit4A = Apartment(unit: "4A")
tom?.apartment = unit4A
unit4A?.tenant = tom
tom = nil
// Tom is being deinitialized
unit4A = nil
//Apartment 4A is being deinitialized
无主引用
和弱引用类似,无主引用保持对一个实例的强引用。和弱引用不同的是无主引用在其他实例有相同或者更长的生命周期时使用。你可以在声明属性或者变量时,在前面加上关键字 unowned
表示这是一个无主引用。
无主引用通常被期望拥有值。不过 ARC 无法再实例被销毁后将无主引用设为 nil
,因为非可选类型的变量不允许被赋值为 nil
。
重要
使用无主引用,你必须确保引用始终指向一个未被销毁的实例。
如果你视图在实例被销毁后,访问该实例的无主引用,会触发运行时错误。
下例定义了两个类 Customer
和 CreditCard
, 模拟了银行客户和信用卡。 这两个类中,每一个都将和另外一个类的实例作为自身的属性。这种关系可能会造成强引用循环。
Customer
和 CreditCard
之间的关系与之前弱引用例子中的 Apartment
和 Person
不同。 一个客户可能有或者没有信用卡,但是一张信用卡总是关联值一个客户。 为了表示这种关系, Customer
类有一个可选类型的 card
属性,但是 CreditCard
类有一个非可选类型的 customer
属性。
此外,只能通过将一个 number
值和 customer
实例传递给 CreditCard
的构造函数的方式来创建 CreditCard
实例。 这样可以确保当创建 CreditCard
实例时总是有一个 customer
实例与之关联。
由于信用卡总是关联着一个客户,因此将 customer
属性定义为无主引用,可以避免引用循环
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit {
print("\(name) is being deinitialized")
}
}
class CreditCard {
let number: Int
unowned let customer: Customer
init(number: Int, customer: Customer) {
self.number = number
self.customer = customer
}
deinit {
print("Card #\(number) is being deinitialized")
}
}
var tom: Customer?
tom = Customer(name: "Tom")
tom?.card = CreditCard(number: 1234_5678_9012_3456, customer: tom!)
tom = nil
//Tom is being deinitialized
//Card #1234567890123456 is being deinitialized
在关联两个实例后, Customer
实例持有对 CreditCard
实例的强引用,而 CreditCard
实例持有对 Customer
实例的无主引用。
由于 customer
的无主医用,当断开 tom
变量持有的强引用时,再也没有指向 Customer
实例的强引用。由于再也没有指向 Customer
实例的强引用,该实例会被销毁。其后,再也没有指向 CreditCard
实例的强引用,该实例也被随之销毁了。
tom = nil
//Tom is being deinitialized
//Card #1234567890123456 is being deinitialized
注意
上面的例子展示了如何使用 安全的无主引用 。对于需要禁用运行时的安全检查情况(例如,出于性能方面的原因), Swift 还提供了 不安全的无主引用 。与所有不安全的操作一样,你需要负责检查代码以确保其安全性。 可用通过unowned(unsafe)
来声明不安全无主引用。 如果你视图在实例被销毁后,访问该实例的不安全无主引用,你的程序会尝试访问该实例之前所在的内存地址,这是一个不安全的操作。
无主引用以及隐式解析可选属性
上面两个例子:
-
Person
和Apartment
的例子展示了两个属性都允许为nil
,并会潜在的产生强引用循环,这种场景适合用弱引用来解决 -
Customer
和CreditCard
的例子展示了一个属性的值允许为nil
, 而另一个属性的值不允许为nil
,这也可能会产生强引用循环,这种场景适合通过无主引用来解决。
然而,存在着第三种场景,在这种场景中,两个属性都必须有值,并且初始化后永远不会为 nil
。在这种场景中,需要一个类使用无主类型,而另一个类使用隐式解析可选属性。
这个两个属性在初始化完成后能被直接访问 (不需要展开) ,同时避免了引用循环。
例如:
定义两个类, Country
和 City
,每个类都将另一个类的实例保存为属性。在这个模型中,每个国家都必须有首都,每个城市都必须属于一个国家。为了实现这种关系, Country
类拥有一个 capitalCity
属性,而 City
类拥有一个 country
属性:
class Country {
let name: String
var capitalCity: City!
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
deinit {
print("city \(name) is being deinitialized")
}
}
class City {
let name: String
let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
deinit {
print("county: \(name) is being deinitialized")
}
}
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”
为了建立两个类的依赖关系, City
的构造含函数接受一个Country
实例作为参数,并且将实例保存到 country
属性。
Country
的构造函数调用了 City
的构造函数。然而,只有 Country
的实例完全初始化后, Country
的构造函数才能把 self
传给 City
的构造函数。 (在两段式构造过程中有具体描述。)
// 将 capitalCity 属性改为非隐式可选类型的话
class Country {
let name: String
var capitalCity: City
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
deinit {
print("city \(name) is being deinitialized")
}
}
// 编译器报错
//'self' used before all stored properties are initialized
// self.capitalCity = City(name: capitalName, country: self) 此时 self 没有完成初始化
为了满足这种需求,通过在类型结尾处加上感叹号 City!
的方式,将 Country
的 capitalCity
属性声明为隐式解析可选类型的属性。这意味像其他可选类型一样, capitalCity
属性的默认值为 nil
,单是不需要展开它的值就能访问它。(在隐式解析可选类型中有描述。)
由于 capitalCity
默认值为 nil
,一旦 Country
的实例在构造函数中给 name
属性赋值后,整个初始化过程就完成了。这意味着一旦 name
属性被赋值后, Country
的构造函数就能引用并传递隐式的 self
。 Country
的构造函数在赋值 capitalCity
时就能将 self
作为参数传递给 City
的构造函数。
// 如果不先初始化 name 则会报错
class Country {
let name: String
var capitalCity: City!
init(name: String, capitalName: String) {
self.capitalCity = City(name: capitalName, country: self)
self.name = name
}
deinit {
print("city \(name) is being deinitialized")
}
}
//error: 'self' used before all stored properties are initialized
//由于没有完成初始化,不能讲self 作为参数传递给 City的构造函数 self.capitalCity = City(name: capitalName, country: self)
闭包引起的强引用循环
强引用循环还会发生在当你讲一个闭包赋值给类实例的某个属性,并且这个闭包中又捕获(使用)了这个类实例时。这个闭包中可能访问了实例的某个属性,例如 self.somProperty
,或者闭包中调用了实例的某个方法,例如 self.someMethod()
。这两种情况都导致了闭包捕获 self
,从而产生了强引用循环。
强引用循环的产生,是因为闭包和类相似,都是引用类型。当你把一个闭包赋值给某个属性时,你是讲这个闭包的引用赋值给了属性。实质上,这和之前的问题是一样的 -- 两个强引用彼此一直有效。但是,和两个类实例不同,这次是一个实例,另一个是闭包。
闭包引起的强引用循环示例:
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name)/>"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
paragraph = nil
// 无输出
实例的 paragraph
的 asHTML
属性持有闭包的强引用,但是,闭包在其内部引用了 self
(引用了 self.name
和 self.text
),因此闭包捕获了 self
,这意味着闭包反过来持有了 paragraph
的强引用。 这样两个对象就产生了强引用循环。
注意:
虽然闭包多次使用了self
,它只捕获HTMLElement
实例的一个强引用。
如果设置 paragraph
变量为 nil
, 打破它持有的 HTMLElement
实例的强引用, 由于强引用循环,HTMLElement
实例和它的闭包都不会销毁。
解决闭包引起的强引用循环
Swift 提供了一种优雅的方式来解决闭包引起的强引用循环问题,称为 闭包捕获列表
(closure capture list)
在定义闭包的同时,定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的强引用循环。捕获列表定义了闭包体内捕获一个或者多个引用类型的规则。和类实例之间的强引用循环一样,声明每个捕获的引用为弱引用或无主引用来替代强引用。根据代码关系来决定使用弱引用还是无主引用。
注意
Swift 要求只要在闭包内使用了self
的成员,就要用self.someProperty
或者self.someMethod()
(而不只是someProperty
或someMethod()
)。这提醒你可能一不小心就捕获了self
。
定义捕获列表
驳货列表的每一项都由一对元素组成,一个元素是 weak
或 unowned
关键字,另一个元素是类实例的引用(如 self
) 或初始化过的变量 (如 delegate = self.delegate!
)。这些项在方括号中用逗号分开。
如果闭包有参数列表和返回类型,把捕获列表放在它们前面:
lazy var someClousure: (Int, String) -> String = {
[unowned self, weak delegate = self.delegate!] (index: Int, stringToProress: String) -> String in
// 这里是闭包的函数体
}
如果闭包没有知名参数或返回类型,即它们会通过上下文推断,那么可以把捕获列表和关键字 in
放在闭包最开始的地方:
lazy var someClosure: Void -> String = {
[unowned self, weak delegate = self.delegate!] in // 这里是闭包的函数体
}
弱引用和无主引用
在闭包和捕获的实例总是相互引用并且总是同时销毁时,将闭包内的捕获值定义为 无主引用
相反的,在被捕获的引用可能会变成 nil
时,将闭包内的捕获定义为 弱引用
。 弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为 nil
。这使我们可以在闭包内检查它们是否存在。
注意
如果被捕获的引用绝对不会变为nil
,应该用无主引用,而不是弱引用。
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name)/>"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
paragraph = nil
//p is being deinitialized
在 asHTML
闭包中通过添加捕获列表 [unowned self]
来表示将 self
捕获为无主引用而不是强引用,这样闭包以无主引用的方式捕获 self
,并不会持有 HTMLElement
实例的强引用。 将 paragraph
置为 nil
后, HTMLElement
实例将会被销毁。
Any 和 AnyObject 的类型转换
Swift 为不确定类型提供了两种特殊的类型别名:
- Any 可以表示任何类型,包括函数类型。
- AnyObject 可以表示任何类类型的实例。
重载运算符使用static
修饰:
Swift提供了类似于C++中的重载运算符方法,可以对自定义类型实现运算符操作。
在定义的时候,运算符函数都是static修饰的,因为运算符函数是类方法(或者结构,枚举)。