本文为私人学习笔记,仅仅做为记录使用,详情内容请查阅 中文官方文档。
泛型
先看一段代码。
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
上述代码的作用是交换两个 Int
的值。那么,当我们现在需要交换两个 String
的值时,我们需要重写编写类似的交换方法。那如果还需要交换其他类型的值,又该如何呢?
泛型能让你根据自定义的需求,编写出适用于任意类型的、灵活可复用的函数及类型。你可以避免编写重复的代码,而是使用一种清晰抽象的方式来表达代码的意图。
泛型是 Swift 最强大的特性之一,很多 Swfit 标准库是基于泛型代码构建的。例如,Swift 的 Array
和 Dictionary
都是泛型集合。
泛型函数
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
泛型函数适用于任意类型,它使用 占位符
类型名(这里叫做 T
),而不是实际类型。泛型函数的函数名后面跟着占位类型名 T
,并使用尖括号包裹起来,这个将括号告诉 Swift 那个 T
是函数定义内的一个占位类型名,因此 Swift 不会去查找名为 T
的实例类型。
T
类型参数由传入的值的类型推断出来,这点和 Any
类型有着很大的差别。
类型参数的命名
字典 Dictionary<Key, Value>
中的 Key
和 Value
及数组 Array<Element>
中的 Element
,这能告诉阅读代码的人这些参数类型与泛型类型或函数之间的关系。然而,当它们之间没有有意义的关系时,通常使用单个字符来表示,例如 T
、U
、V
。
泛型类型
除了泛型函数,Swift 的标准库中还有很多泛型类型,例如 Dictionary
和 Array
。Swift 还允许你自定义泛型类型,这些自定义的类、结构体和枚举可以适用于任意类型。
struct Stack<Element> {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
自定义结构体 Stack
使用了占位类型 Element
,这个类型参数包裹在紧随结构体的一对尖括号里。
类型约束
类型约束指定类型参数必须继承自指定类或者遵循特定的协议。例如,Swift 的 Dictionary
类型对字典的键的类型做了限制,字典的 key
必须是可哈希的(hashable),Swift 的基本类型默认都是可哈希的。可哈希的目的是为了便于检查字典中是否已经包含某个特定键的值。如果没有这个约束,那么字典将无法判断是否可以插入或者替换某个指定键的值,也不能查找到已经存储在字典中的指定键的值。
约束语法:
在一个类型参数后面放置一个类名或者协议名,并用冒号进行分割,来定义类型约束。
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
协议定义了一个关联类型 Item
,该类型并没有指定实际类型,这个类型的确定留给了遵循该协议的类型来提供。
struct Stack<Element>: Container {
// Stack<Element> 的原始实现部分
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
// Container 协议的实现部分
mutating func append(_ item: Element) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Element {
return items[i]
}
}
Stack
结构体遵循了 Container
协议,其中占位类型参数 Element
被用作 append(_:)
方法的 Item
参数和下标的返回类型。Swift 可以据此推断出 Element
的类型即是 Item
的类型。
除此之外,如果没有在协议中涉及到关联类型,即 Swift 无法自动推断出关联类型的实际类型,你也可以在遵循协议的类型中手动完成关联类型的实际类型。例如:
typealias Item = Int
扩展现有类型来指定关联类型
我们可以通过扩展来让一个已经存在的类型遵循一个协议,然后就可以将该类型充当这个协议来使用,这里的协议包括了使用了关联类型协议。
Swift 的 Array
类型已经提供了 append(_:)
方法,count
属性,以及使用 Int
索引的下标来检索其元素,这些功能都满足了 Container
协议的要求,因此我们可以通过扩展声明其遵循了 Container
协议。你可以通过一个空扩展来实现这点:
extension Array: Container {}
Array
的 append(_:)
方法和下标确保了 Swift 可以推断出 Item
具体的实际类型,并且定义了这个扩展之后,你可以将任意的 Array
当作 Container
来使用。
给关联类型添加约束
关联类型同样可以添加约束。
associatedtype Item: Equatable
泛型 Where 语句
泛型约束 让你能够为泛型函数、下标、类型的类型参数定义一些强制要求。
对关联类型添加约束通常是非常有用的,你可以通过定义一个泛型 where
子句来实现。通过泛型 where
子句让关联类型遵从某个特定的协议,以及某个特定的类型参数和关联类型必须类型相同。
你可以通过将 where
关键字紧跟在类型参数列表后面来定义 where
子句, where
子句后跟一个或者多个针对关联类型的约束,以及一个或者多个类型参数和关联类型间的相等关系。你可以在函数体或者类型的大括号之前添加 where
子句。
func allItemsMatch<C1: Container, C2: Container>
(_ someContainer: C1, _ anotherContainer: C2) -> Bool
where C1.Item == C2.Item, C1.Item: Equatable {
// 检查两个容器含有相同数量的元素
if someContainer.count != anotherContainer.count {
return false
}
// 检查每一对元素是否相等
for i in 0..<someContainer.count {
if someContainer[i] != anotherContainer[i] {
return false
}
}
// 所有元素都匹配,返回 true
return true
}
具有泛型 Where 子句的扩展
可以使用泛型 where
子句作为扩展的一部分。
extension Stack where Element: Equatable {
func isTop(_ item: Element) -> Bool {
guard let topItem = items.last else {
return false
}
return topItem == item
}
}
上述使用了泛型 where
子句为 Stack
扩展添加了新的条件,只有当 Stack
中的元素符合 Equatable
协议时,扩展才会添加 isTop(_:)
方法。
如果尝试在其元素不符合 Equatable
协议的 Stack
对象上调用该方法则会收到编译错误。
再比如,你可以扩展 Container
协议。
extension Container where Item: Equatable {
func startsWith(_ item: Item) -> Bool {
return count >= 1 && self[0] == item
}
}
新的扩展方法会确保容器至少有一个元素,然后检查容器中的第一个元素是否与给定的元素相等。
具有泛型 Where 子句的关联类型
可以在关联类型后面加上具有泛型 where
的子句。例如,建立一个包含迭代器(Iterator
)的容器。
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
func makeIterator() -> Iterator
}
迭代器(Iterator
)的泛型 where
子句要求:无论迭代器是什么类型,迭代器中的元素类型,必须和容器项目的类型保持一致。makeIterator()
则提供了容器的迭代器的访问接口。
一个协议继承了另一个协议,你通过在协议声明的时候,包含泛型 where
子句,来添加了一个约束到被继承协议的关联类型。
protocol ComparableContainer: Container where Item: Comparable { }
泛型下标
下标可以是泛型,它们能够包含泛型 where
子句。
extension Container {
subscript<Indices: Sequence>(indices: Indices) -> [Item]
where Indices.Iterator.Element == Int {
var result = [Item]()
for index in indices {
result.append(self[index])
}
return result
}
}
这个 Container
协议的扩展添加了一个下标方法,接收一个索引的集合,返回每一个索引所在的值的数组。这个泛型下标的约束如下:
- 在尖括号中的泛型参数
Indices
,必须是符合标准库中的Sequence
协议的类型。 - 下标使用的单一的参数,
indices
,必须是Indices
的实例。 - 泛型
where
子句要求Sequence(Indices)
的迭代器,其所有的元素都是 Int 类型。这样就能确保在序列(Sequence
)中的索引和容器(Container
)里面的索引类型是一致的。
综合一下,这些约束意味着,传入到 indices 下标,是一个整型的序列。
不透明类型
返回不透明类型
不透明类型和泛型相反。泛型允许调用一个方法时,为这个方法的形参和返回值指定一个与实现无关的类型。不透明类型允许函数实现时,选择一个与调用代码无关的返回类型。
// 协议
protocol Shape {
func draw() -> String
}
// 输出图形:正方形
struct Square: Shape {
var size: Int
func draw() -> String {
let line = String(repeating: "*", count: size)
let result = Array<String>(repeating: line, count: size)
return result.joined(separator: "\n")
}
}
// 返回不透明类型
func makeGraphics() -> some Shape {
let square = Square(size: 2)
return square
}
makeSquare()
函数将返回值类型定义为 some Shape
,因此,该函数返回遵循 Shape
协议的给定类型,而不需要指定任何具体类型。换句话说,该函数可以表明它公共接口的基本性质 - 返回值是一个几何图形,而不是由公共接口协议生成的特殊类型。如 Square
。
不透明类型和协议类型的区别
虽然使用不透明类型作为函数返回值,看起来和返回协议类型非常的相似,但是这两者有一个重要的却别:是否需要保证类型一致性。
// 反转形状
struct FlippedShape<T: Shape>: Shape {
var shape: T
func draw() -> String {
let lines = shape.draw().split(separator: "\n")
return lines.reversed().joined(separator: "\n")
}
}
func invalidFlip<T: Shape>(_ shape: T) -> some Shape {
if shape is Square {
return shape // 错误:返回类型不一致
}
return FlippedShape(shape: shape) // 错误:返回类型不一致
}
由于 invalidFlip(_:)
方法返回值可能存在两种,所以该方法是不正确的,为了修正该方法,我们可以将针对 Square
的特殊处理移入到 FlippedShape
中,以确保函数的返回值唯一。
struct FlippedShape<T: Shape>: Shape {
var shape: T
func draw() -> String {
if shape is Square {
return shape.draw()
}
let lines = shape.draw().split(separator: "\n")
return lines.reversed().joined(separator: "\n")
}
}
func invalidFlip<T: Shape>(_ shape: T) -> some Shape {
return FlippedShape(shape: shape)
}
一个不透明类型只能对应一个具体的类型,即便函数调用者并不能知道是哪一种类型;协议类型则可以同时对应多个类型,只要它们都遵循同一协议。总的来说,协议类型更具灵活性,底层类型可以存储更多的值,而不透明类型对这些底层类型有着更强的限制。
不透明类型的作用
具有不透明返回类型的函数或方法会隐藏返回值的类型信息。函数不再提供具体的类型作为返回类型,而是根据它支持的协议来描述返回值。
在处理模块和调用代码之间的关系时,隐藏类型信息非常重要,因为返回的底层数据类型仍然可以保持私有。而且不同于返回协议类型,不透明类型能保证类型一致性,即:编译器能获取到类型信息,同时模块使用者却不能获取到。
闭包的循环强引用
在定义闭包的同时定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的循环引用。
捕获列表定义类闭包体内捕获的一个或者多个引用类型的规则。根据具体的应用场景来使用弱引用还是无主引用。
捕获列表
捕获列表中的每一项都是由一对元素组成,一个元素是 weak
或 unowner
关键字,另一个元素是类实例的引用。
如果闭包有参数列表和返回类型,那么把捕获列表放在它们的前面。
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
// 这里是闭包的函数体
}
弱引用 vs 无主引用
在闭包和捕获列表的实例总是相互引用并且总是同时销毁时,将闭包内的捕获定义为 无主引用
。相反的,如果被捕获的引用具有更短的生命周期,可能随时变为 nil
,那么将闭包内的捕获定义为 弱引用
。
弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为 nil
。