Advanced-Swift-Sample-Code
1. 介绍
<1>
Swift 和其他语言的相似之处
Swift 是什么样的语言
术语
值,变量,引用,常量 - 只有指向关系被常量化了,而对象本身还是可变的。
<2>
另一件复杂的事情。如果我们的结构体
中包含有引用类型
,在将结构体赋值给一个新变量时所发生的复制行为中,这些引用类型的内容是不会被自动复制一份的,只有引用本身会被复制。这种复制的行为被称作浅复制 (shallow copy)。
举个例子,Foundation 框架中的 Data 结构体实际上是对引用类型 NSData 的一个封装。不过, Data 的作者采取了额外的步骤,来保证当 Data 结构体发生变化的时候对其中的 NSData 对象进行深复制。他们使用一种名为 “写时复制” (copy-on-write) 的技术来保证操作的高效,我们会在结构体和类里详细介绍这种机制。现在我们需要重点知道的是,这种写时复制的特性并不是直接具有的,它需要额外进行实现。
Swift 中,像是数组这样的集合类型也都是对引用类型的封装,它们同样使用了写时复制的方式来在提供值语义的同时保持高效。
<3>
闭包的定义
柯里化函数 定义
静态派发
动态派发 - vtable
重写,重载,多态
<4>
Swift 风格指南 - Swift API 设计准则
2. 内建集合类型
<1> 数组
i. 数组和可变性 - Array 与 NSArray 的不同
Swift 中的数组是值类型(结构体),OC中的是引用类型(对象)
ii. 数组和可选值 - 数组的基本操作
iii. 数组变形
Map
i. 使用函数将行为参数化 - 标准库与自定义方法,使用函数参数。
ii. 可变和带有状态的闭包
iii. 闭包是指那些可以捕获自身作用域之外的变量的函数。
accumulate
Filter
一般来说,你只应该在需要所有结果时才去选择使用 filter。
all(matching)
Reduce
flatMap
使用 forEach 进行迭代
数组类型
切片 - ArraySlice
桥接
<2> 字典
可变性
有用的字典方法 - merge,frequencies,mapValues
Hashable 要求 - 字典其实是哈希表。
<3> Set
i. Set 也是通过哈希表实现的
ii. 如果你需要高效地测试某个元素是否存在于序列中并且元素的顺序不重要时,使用集合是更好的选择 (同样的操作在数组中的复杂度是 O(n))。
iii. 另外,当你需要保证序列中不出现重复元素时,也可以使用集合。
集合代数 - subtracting,intersection,formUnion
IndexSet & CharacterSet : SetAlgebra
IndexSet 表示了一个由正整数组成的集合。当然,你可以用 Set<Int> 来做这件事,但是 IndexSet 更加高效,因为它内部使用了一组范围列表进行实现。(它会存储连续的范围)
CharacterSet 是一个高效的存储 Unicode 码点 (code point) 的集合。它经常被用来检查一个特定字符串是否只包含某个字符子集 (比如字母数字 alphanumerics 或者数字 decimalDigits) 中的字符。
不过,和 IndexSet 有所不同,CharacterSet 并不是一个集合类型。 它的名字,CharacterSet,是从 Objective-C 导入时生成的,在 Swift 中它也并不兼容 Swift 的 Character 类型。可能 UnicodeScalarSet 会是更好的名字
unique()
<4> Range
Range & ClosedRange
不能对 Range 或者 ClosedRange 进行迭代,可以检查某个元素是否存在于范围中
→ 只有半开范围能表达空间隔(也就是下界和上界相等的情况,比如5..<5)。
→ 只有闭合范围能包括其元素类型所能表达的最大值(比如0...Int.max)。而半开范围则要求范围上界是一个比自身所包含的最大值还要大 1 的值。
CountableRange
它的元素类型需要遵守 Strideable 协议 (以整数为步⻓)。
CountableRange & CountableClosedRange
Strideable 的约束使得 CountableRange 和 CountableClosedRange 遵守 RandomAccessCollection,于是我们就能够对它们进行迭代了。
半开范围 | 闭合范围 | |
---|---|---|
元素满足 Comparable | Range | ClosedRange |
元素满足 Strideable | CountableRange | CountableClosedRange |
(以整数为步长) (集合类型) |
partial range
部分范围 (partial range) 指的是将 ... 或 ..< 作为前置或者后置运算符来使用时所构造的范围。
RangeExpression
首先,它允许我们询问某个元素是否被包括在该范围中。
其次,给定一个集合类型,它能够计算出表达式所指定的完整的 Range
范围和按条件遵守协议
半开的 Range 和闭合的 ClosedRange 之间的差异应该会一直存在。没有一种方法将 ClosedRange 转换为 Range。
3. 集合类型协议
<1> Sequence
protocol Sequence {
associatedtype Iterator: IteratorProtocol
func makeIterator() -> Iterator
*// ... *
}
Iterator
protocol IteratorProtocol {
associatedtype Element
mutating func next() -> Element?
}
// for 的实现
var iterator = someSequence.makeIterator()
while let element = iterator.next() {
doSomething(with: element)
}
// 自定义
struct ConstantIterator: IteratorProtocol {
typealias Element = Int
mutating func next() -> Int? {
return 1
}
}
// e.g.
struct FibsIterator: IteratorProtocol {
var state = (0, 1)
mutating func next() -> Int? {
let upcomingNumber = state.0
state = (state.1, state.0 + state.1)
return upcomingNumber
}
}
遵守 Collection 协议
struct PrefixIterator: IteratorProtocol {
let string: String
var offset: String.Index
init(string: String) {
self.string = string
offset = string.startIndex
}
mutating func next() -> Substring? {
guard offset < string.endIndex else {
return nil
}
offset = string.index(after: offset)
return string[..<offset]
}
}
struct PrefixSequence: Sequence {
let string: String
func makeIterator() -> PrefixIterator {
return PrefixIterator(string: string)
}
}
for prefix in PrefixSequence(string: "Hello") {
print(prefix)
}
PrefixSequence(string: "Hello").map { $0.uppercased() }
迭代器和值语义
基于函数的迭代器和序列
// 自定义的结构体具有值语义,而使用 AnyIterator 定义的没有
func fibsIterator() -> AnyIterator<Int> {
var state = (0, 1)
return AnyIterator {
let upcomingNumber = state.0
state = (state.1, state.0 + state.1)
return upcomingNumber
}
}
let fibsSequence = AnySequence(fibsIterator)
Array(fibsSequence.prefix(10)) // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
这两个 sequence 方法非常有用,它们经常用来代替传统的 C ⻛格的循环,特别是当下标的步⻓不遵守线性关系的时候。
sequence(first:next:)
sequence(state:next:)
let fibsSequence2 = sequence(state: (0, 1)) {
// 在这里编译器需要一些类型推断的协助
(state: inout (Int, Int)) -> Int? in
let upcomingNumber = state.0
state = (state.1, state.0 + state.1)
return upcomingNumber
}
Array(fibsSequence2.prefix(10)) // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
无限序列
sequence 对于 next 闭包的使用是被延迟的。
对于序列和集合来说,重要区别就是序列可以是无限的,而集合则不行。
不稳定序列
举一个破坏性的可消耗序列的例子:
let standardIn = AnySequence {
return AnyIterator {
readLine()
}
}
let numberedStdIn = standardIn.enumerated()
// 延迟生成的 enumerate
for (i, line) in numberedStdIn {
print("\(i+1): \(line)")
}
序列和迭代器之间的关系
对于像斐波纳契序列这样的稳定序列来说,它们需要独立的遍历状态,这就是迭代器所提供的 (当然还需要遍历的逻辑,不过这部分是序列的内容)。
makeIterator 方法的目的就是创建这样一个遍历状态。
子序列
protocol Sequence {
associatedtype Element
associatedtype Iterator: IteratorProtocol
where Iterator.Element == Element
associatedtype SubSequence
// ...
}
// → prefix 和 suffix — 获取开头或结尾n个元素
// → prefix(while:) — 从开头开始当满足条件时,
// → dropFirst 和 dropLast — 返回移除掉前n个或后n个元素的子序列
// → drop(while:) — 移除元素,直到条件不再为真,然后返回剩余元素
// → split — 将一个序列在指定的分隔元素时截断,返回子序列的的数组
extension Sequence where Element: Equatable, SubSequence: Sequence, SubSequence.Element == Element {
func headMirrorsTail(_ n: Int) -> Bool {
let head = prefix(n)
let tail = suffix(n).reversed()
return head.elementsEqual(tail)
}
}
[1,2,3,4,2,1].headMirrorsTail(2) // true
链表
/// 一个简单的链表枚举
enum List<Element> {
case end
indirect case node(Element, next: List<Element>)
}
// 在这里使用 indirect 关键字可以告诉编译器这个枚举值 node 应该被看做引用。
// 但是值类型不能循环引用自身,否则编译器就无法计算它的尺寸了。
// indirect 关键字允许一个枚举成员能够被当作引用,这样一来,它就能够持有自己了。
<2> Collection
稳定的序列
i. 在实现 Collection 协议时,最难的部分在于选取一个合适的索引类型
来表达集合类型中的位置。
ii. 除了 Array,Dictionary,Set,String 和它的各种方式以外, 另外还有 CountableRange 和 UnsafeBufferPointer 也是集合类型。Foundation - Data 和 IndexSet
自定义的集合类型
为队列设计协议
遵守 Collection 协议
遵守 ExpressibleByArrayLiteral 协议
关联类型
i. Iterator
集合类型中的默认迭代器类型是 IndexingIterator<Self>,这个类型是一个很简单的结构体,它是对集合的封装,并用集合本身的索引来迭代每个元素。
ii. SubSequence
iii. IndexDistance
一个有符号整数类型,代表了两个索引之间的步数。
iv. Indices
集合的 indices 属性的返回值类型。它代表对于集合的所有有效下标的索引所组成的 集合,并以升序进行排列。
默认类型是 DefaultIndices<Self>。
Indices 的默认类型是 DefaultIndices<Self>。和 Slice 一样,它是对于原来的集合类型的简单封装,并包含起始和结束索引。它需要保持对原集合的引用,这样才能够对索引进行步进。
如果你的索引是整数类型,你可以直接使用 CountableRange<Index>
索引
Index: Comparable
索引必须要有确定的顺序。
字典
的索引是 DictionaryIndex 类型,它是一个指向字典内部存储缓冲区的不透明值。事实上这个类型只是一个 Int 偏移值的封装。
索引失效
索引步进
collection.index(after: someIndex)
当你实现你自己的索引类型时,请记住尽可能地避免持有集合类型的引用。
自定义集合索引
str.split(separator: " ")
为了提高性能,我们要构建一个 Words 集合,它能够让我们不一次性地计算出所有单词,而是可以用延迟加载的方式进行迭代。
切片
list.dropFirst()
let firstDropped2 = words.suffix(from: onePastStart)
let firstDropped3 = words[onePastStart...]
Slice 是基于任意集合类型的一个轻量级封装
切片与原集合共享索引
如果你在通过集合类型的 indices 进行迭代时,修改了集合的内容,那么 indices 所持有的任何对原来集合类型的强引用都会破坏写时复制的性能优化,因为这会造成不必要的复制操作。
如果集合的尺寸很大的话,这会对性能造成很大的影响。
Workaround: 要避免这件事情发生,你可以将 for 循环替换为 while 循环,然后手动在每次迭代的时候增加 索引值,这样你就不会用到 indices 属性。当你这么做的时候,要记住一定要从 collection.startIndex 开始进行循环,而不要把 0 作为开始。
泛型 PrefixIterator
<3> 专门的集合类型
// → BidirectionalCollection — “一个既支持前向又支持后向遍历的集合。”
// → RandomAccessCollection — “一个支持高效随机存取索引遍历的集合。”
// → MutableCollection — “一个支持下标赋值的集合。”
// → RangeReplaceableCollection — “一个支持将任意子范围的元素用别的集合中的元素进行替换的集合。”
BidirectionalCollection
index(before:)
RandomAccessCollection
(a) 以任意距离移动一个索引
(b) 测量任意两个索引之间的距离,两者都需要是 O(1) 时间常数的操作。
MutableCollection
单个元素的下标访问方法 subscript 现在必须提供一个 setter
RangeReplaceableCollection
(a) 一个空的初始化方法 — 在泛型函数中这很有用,因为它允许一个函数创建相同类型的新的空集合。
(b) replaceSubrange(_:with:)方法 — 它接受一个要替换的范围以及一个用来进行替换的集合。
// → append(:) 和 append(contentsOf:) — 将 endIndex..<endIndex(也就是说末尾的空范围)替换为单个或多个新的元素。
// → remove(at:) 和 removeSubrange(:) — 将 i...i 或者 subrange 替换为空集合。
// → insert(at:) 和 insert(contentsOf:at:) — 将 i..<i (或者说在数组中某个位置的空范围)替换为单个或多个新的元素。
// → removeAll — 将 startIndex..<endIndex 替换为空集合。
想要适应严格的类型系统,需要大量的练习
4. 可选值
哨岗值
函数都返回了一个 “魔法” 数来表示函数并没有返回真实的值
通过枚举解决魔法数的问题
enum Optional<Wrapped> {
case none
case some(Wrapped)
}
获取关联值的唯一方法是通过使用 switch 或者 if case 这样的语句来进行模式匹配。
可选值概览
if let
while let
// 一旦你理解 for...in 其实是 while let
不过 do 代码块自身还有一个作用,就是引入一个新的作用域。
双重可选值
因为 case 匹配可以通过重载 ~= 运算符来进行扩展
if var && while var
解包后可选值的作用域
提前退出可以帮助我们在这个函数稍后的部分避免嵌套或者重复的检查。
guard else
唯一的限制是你必须在 else 中离开当前的作用域,也就是说,在代码块的最后你必须写 return 或 者调用 fatalError (或者其他被声明为返回 Never 的方法)。如果你是在循环中使用 guard 的话, 那么最后也可以是 break 或者 continue。
public enum Never { }
public typealias Void = ()
Swift 对 “东西不存在”(nil),“存在且为空”(Void) 以及“不可能发生” (Never) 这几个概念进行了仔细的区分。
最后 Swift 编译器还会检查你是否确实在 guard 块中退出了当前作用域,如果没有的话,你会得到一个编译错误。因为可以得到编译器帮助,所以我们建议尽量选择使用 guard,即便 if 也可以正常工作。
Optional chaining
可选链是一个 “展平” 操作。
可选链对下标和函数调用也同样适用
var optionalLisa: Person? = Person(name: "Lisa Simpson", age: 8)
optionalLisa?.age += 1
因为 Person 是一个结构体,它是一个值类型,绑定后的值只是原来的值的局部作用域的复制,对这个复制进行变更,并不会影响原来的值
如果 Person 是类的话,这么做是可行的。
var a: Int? = 5
a? = 10
a //Optional(10)
var b: Int? = nil
b? = 10
b //nil
??
在字符串插值中使用可选值
// 有时候你确实会想要在字符串插值中使用可选值,比如想要在调试的时候将它的值打印出来,在这种情况下,警告就很烦人了。
// 编译器为我们提供了几种修正这个警告的方式:
// <1> 显式地用 as Any 进行转换,
// <2> 使用 ! 对值进行强制解包 (如果你能确定该值不为 nil 时),
// <3> 使用 String(describing: ...) 对它进行包装,<4> 或者用 nil 合并运算符提供一个默认值。
Optional map
extension Optional {
func map<U>(transform: (Wrapped) -> U) -> U? {
if let value = self {
return transform(value)
}
return nil
}
}
Optional flatMap
如果你对一个可选值调用 map,但是你的转换函数本身也返回可选值结果的话,最终结果将是一个双重嵌套的可选值。
flatMap 可以把结果展平为单个可选值。
这说明 flatMap 和 if let 非常相似,可以相互重写。
可选链也和 flatMap 很相似: i?.advance(by: 1) 实际上和 i.flatMap { $0.advance(by: 1) } 是等价的。
extension Optional {
func flatMap<U>(transform: (Wrapped) -> U?) -> U? {
if let value = self, let transformed = transform(value) {
return transformed
}
return nil
}
}
使用 flatMap 过滤 nil
numbers.flatMap { Int($0) }.reduce(0, +) // 6
我们之前已经看过两个 flatMap 了: 一个作用在数组上展平一个序列,另一个作用在可选值上展平可选值。
这里的 flatMap 是两者的混合: 它将把一个映射为可选值的序列进行展平。
func flatten<S: Sequence, T>(source: S) -> [T] where S.Element == T? {
let filtered = source.lazy.filter { $0 != nil }
return filtered.map { $0! }
}
extension Sequence {
func flatMap<U>(transform: (Element) -> U?) -> [U] {
return flatten(source: self.lazy.map(transform))
}
}
可选值判等
// 编译器会帮助我们将值在需要时转变为可选值。
if regex.first == "^" {
// 只匹配字符串开头
}
func ==<T: Equatable>(lhs: T?, rhs: T?) -> Bool {
switch (lhs, rhs) {
case (nil, nil): return true
case let (x?, y?): return x == y
case (_?, nil), (nil, _?): return false
}
}
Equatable 和 ==
func ==<T: Equatable>(lhs: [T?], rhs: [T?]) -> Bool {
return lhs.elementsEqual(rhs) { $0 == $1 }
}
可选值比较 如果你想要在可选值之间进行除了相等之外的关系比较的话,现在你需要先对它们进行解包, 然后明确地指出 nil 要如何处理
强制解包的时机
infix operator !!
改进强制解包的错误信息
infix operator !?
在调试版本中进行断言
// 想要挂起一个操作我们有三种方式。
// <1> 首先,fatalError 将接受一条信息,并且无条件地停止操作。
// <2> 第二种选择,使用 assert 来检查条件,当条件结果为 false 时,停止执行并输出信息。在发布版本中,assert 会被移除掉,条件不会被检测,操作也永远不会挂起。
// <3> 第三种方式是使用 precondition,它和 assert 比较类型,但是在发布版本中它不会被移除,也就是说,只要条件被判定为 false,执行就会被停止。
多灾多难的隐式解包可选值
原因 1: 暂时来说,你可能还需要到 Objective-C 里去调用那些没有检查返回是否存在的代码
原因 2: 因为一个值只是很短暂地为 nil,在一段时间后,它就再也不会是 nil - outlet
隐式可选值行为
5. 结构体和类
结构体和类的主要不同点:
// → 结构体(和枚举)是值类型,而类是引用类型。在设计结构体时,我们可以要求编译器保证不可变性。而对于类来说,我们就得自己来确保这件事情。
// → 内存的管理方式有所不同。结构体可以被直接持有及访问,但是类的实例只能通过引用来间接地访问。结构体不会被引用,但是会被复制。也就是说,结构体的持有者是唯一的,但是类的实例却能有很多个持有者。
// → 使用类,我们可以通过继承来共享代码。而结构体(以及枚举)是不能被继承的。想要在不同的结构体或者枚举之间共享代码,我们需要使用不同的技术,比如像是组合、泛型以及协议扩展等。
值类型
<1> 不可变性也让代码天然地具有线程安全的特性,因为不能改变的东西是可以在线程之间安全地共享的。
<2> 改变一个结构体变量的属性,在概念上来说,和 为整个变量赋值一个全新的结构体是等价的。
<3> 结构体只有一个持有者。比如,当我们将结构体变量传递给一个函数时,函数将接收到结构体的复制,它也只能改变它自己的这份复制。这叫做值语义 (value semantics),有时候也被叫做复制语义。而对于对象来说,它们是通过传递引用来工作的,因此类对象会拥有很多持有者, 这被叫做引用语义 (reference semantics)。
<4> 编译器所做的对于值类型的复制优化和值语义类型的写时复制行为并不是一回事儿。写时复制必须由开发者来实现,想要实现写时复制,你需要检测所包含的类是否有共享的引用。
<5> 和自动移除不必要的值类型复制不同,写时复制是需要自己实现的。不过编译器会移除那些不必要的 “无效” 浅复制,以及像是 Array 这样的类型中的代码会执行 “智能的” 写时复制,两者互为补充,都是对值类型的优化。
可变性
class BinaryScanner 在 GCD 中访问引发竞态条件
结构体
通过在扩展中定义自定义方法,我们就可以同时保留原来的初始化方法
可变语义
对结构体进行改变,在语义上来说,与重新为它进行赋值是相同的。
如果 Rectangle 是类的话,didSet 就不会被触发了,因为在那种情况下,数组存储的引用不会发生改变,只是引用指向的对象发生了改变。
可变方法
mutating
mutating 同时也是 willSet 和 didSet “知道” 合适进行调用的依据: 任何 mutating 方法的调用或者隐式的可变 setter 都会触发这两个事件。
在很多情况下,一个方法会同时有可变和不可变版本。比如数组有 sort() 方法 (这是个 mutating 方法,将在原地排序) 以及 sorted() 方法 (返回一个新的数组)。我们也可以为我们的 translate(by:_) 提供一个非 mutating 的版本。
extension Rectangle {
mutating func translate(by offset: Point) {
origin = origin + offset
}
}
screen.translate(by: Point(x: 10, y: 10))
screen // (10, 10, 320, 480)
extension Rectangle {
func translated(by offset: Point) -> Rectangle {
var copy = self
copy.translate(by: offset)
return copy
}
}
screen.translated(by: Point(x: 20, y: 20)) *// (30, 30, 320, 480) *
唯一会出现问题的地方是 你在不同的线程中引用了同一个全局或者被捕获的的结构体变量 (默认情况下,闭包将被引用所捕获)。
mutating 是如何工作的: inout 参数
<1> mutating 关键字做的正是此事。它可以将隐式的 self 参数变为可变的。
<2> 在全局函数中,我们可以将一个或多个参数标记为 inout 来达到相同的效果。就和一个普通的参数一样,值被复制并作为参数被传到函数内。不过,我们可以改变这个复制 (就好像它是被 var 定义的一样)。然后当函数返回时,Swift 会将这个 (可能改变过的) 值进行复制并将其返回 给调用者,同时将原来的值覆盖掉。
那些像是 += 这样,可以对左侧值进行变更的运算符,需要其参数为 inout。
使用值类型避免并行 bug
比如说,我们保持 BinaryScanner 是一个结构体,但是我们将 scanRemainingBytes 方法的内容内联使用的话, 我们就会和上面一样面临竞态条件的问题。
写时复制
<1> 这种行为就被称为写时复制。它的工作方式是,每当数组被改变,它首先检查它对存储缓冲区的引用是否是唯一的,或者说,检查数组本身是不是这块缓冲区的唯一拥有者。
<2> 如果是,那么缓冲区可以进行原地变更; 也不会有复制被进行。
不过,如果缓冲区有一个以上的持有者 (如本例中),那么数组就需要先进行复制,然后对复制的值进行变化,而保持其他的持有者不受影响。
实现写时复制
写时复制 (昂贵方式)
写时复制 (高效方式)
<1> 在 Swift 中,我们可以使用 isKnownUniquelyReferenced 函数来检查某个引用只有一个持有者。
<2> 不过,对于 Objective-C 的类,它会直接返回 false。所以,直接对 NSMutableData 使用这个函数的话没什么意义。我们可以创建一个简单的 Swift 类,来将任意的 Objective-C 对象 (或者其他任意值) 封装到 Swift 对象中
<3> 这项技术让你能够在创建保留值语义的结构体的同时,保持像对象和指针那样的高效操作。
写时复制的陷阱
<0>
var array = [COWStruct()]
array[0].change() // No copy
var dict = ["key": COWStruct()]
dict["key"]?.change() // Optional("Copy")
<1> 如果在你将一个写时复制的结构体放到字典
中,又想要避免这种复制的话,你可以将值用类封装起来,这将为值赋予引用语义。
<2> 当你在使用自己的结构体时,也需要将这一点牢记于心。比如,我们可以创建一个储存某个值的简单地容器类型,通过直接访问存储的属性,或者间接地使用下标,都可以访问到这个值。当我们直接访问它的时候,我们可以获取写时复制的优化,但是当我们用下标间接访问的时候,复制会发生
<3> Array 通过使用地址器 (addressors) 的方式实现下标。地址器允许对内存进行直接访问。数组的下标并不是返回元素,而是返回一个元素的地址器。这样一来,元素的内存可以被原地改变,而不需要再进行不必要的复制。你可以在你自己的代码中使用地址器,但是因为它们没有被官方文档化,所以也许会发生改变。要了解更多信息,可以看看 Swift 仓库中关于 Accessors 的文档。
闭包和可变性
如果我们传递这些闭包和函数,它们会以引用的方式存在,并共享同样的状态。
func uniqueIntegerProvider() -> AnyIterator<Int> {
var i = 0
return AnyIterator {
i += 1
return i
}
}
Swift 的结构体一般被存储在栈上,而非堆上。不过对于可变结构体,这其实是一种优化:默认情况下结构体是存储在堆上的,但是在绝大多数时候,这个优化会生效,并将结构体存储到栈上。编译器这么做是因为那些被逃逸闭包捕获的变量需要在栈帧之外依然存在。当编译器侦测到结构体变量被一个函数闭合的时候,优化将不再生效,此时这个结构体将存储在堆上。这样一来,在我们的例子里,就算 uniqueIntegerProvider 退出了作用域,i 也将继续存在。
内存
struct Person {
let name: String
var parents: [Person]
}
var john = Person(name: "John", parents: [])
john.parents = [john]
john // John, parents:[John, parents:[]]
因为值类型的特点,当你把 john 加到数组中的时候,其实它被复制了。更精确地说的话,应该是 “你把 john 的值加到了数组中。” 要是 Person 是一个类的话,我们就会引入一个循环引用。 但是在这里的结构体版本中,john 只有一个持有者,那就是原来的变量值 john。
引用循环
weak 引用
unowned 引用
<1> 对每个 unowned 的引用,Swift 运行时将为这个对象维护另外一个引用计数。当所有的 strong 引用消失时,对象将把它的资源 (比如对其他对象的引用) 释放掉。不过,这个对象本身的内存将继续存在,直到所有的 unowned 引用也都消失。这部分内存将被标记为无效 (有时候我们也把它叫做僵尸 (zombie) 内存),当我们试图访问这样的 unowned 引用时,就会发生运行时错误。
在 unowned 和 weak 之间进行选择
<1> 从根本上来说,这个问题取决于相关对象的生命周期。如果这些对象的生命周期互不相关,也就是说,你不能保证哪一个对象存在的时间会比另一个⻓,那么弱引用就是唯一的选择。
<2> 另一种情况下,如果你可以保证非强引用对象拥有和强引用对象同样或者更⻓的生命周期的话, unowned 引用通常会更方便一些。
这是因为我们可以不需要处理可选值,而且变量将可以被 let 声明,而与之相对,弱引用必须被声明为可选的 var。
<3> 主对象通过强引用控制子对象,子对象对主对象的逆向引用就可以是 unowned 引用。
<4> unowned 引用要比 weak 引用少一些性能损耗,因此访问一个 unowned 引用的属性或者调用它上面的方法都会稍微快一些; 不过,这个因素应该只在性能非常重要的代码路径上才需要被考虑。
闭包和内存
在 Swift 中,除了类以外,函数 (包括闭包) 也是引用类型。我们在闭包和可变性的部分已经看到过,闭包可以捕获变量。如果这些变量自身是引用类型的话,闭包将持有对它们的强引用。
引用循环
我们可以通过使用捕获列表 (capturelist) 来让闭包不去引用视图。
捕获列表
捕获列表也可以用来初始化新的变量。