Swift 作为现代、高效、安全的编程言语,其背面有许多高档特性为之支撑。
『 Swift 最佳实践 』系列对常用的言语特性逐个进行介绍,助力写出更简练、更优雅的 Swift 代码,快速完成从 OC 到 Swift 的转变。
该系列内容首要包括:
- Optional
- Enum
- Closure
- Protocol
- Generics
- Property Wrapper
- Structured Concurrent
- Result builder
- Error Handle
- Advanced Collections (Asyncsequeue/OptionSet/Lazy)
- Expressible by Literal
- Pattern Matching
- Metatypes(.self/.Type/.Protocol)
ps. 本系列不是入门级语法教程,需求有必定的 Swift 根底
本文是系列文章的第五篇,介绍 Generics,经过泛型能够写出更灵敏、通用性更好的代码。
Write code that works for multiple types and specify requirements for those types. — Swift Docs Generics
Swift 经过 Type Constraints 赋以 Generics 更强大的才能,能够愈加灵敏的操控 Generics 具有的才能和运用场景 。
于此一同,由于 Generics 需求 Boxing 以及办法调用都是动态派发有必定的功用损耗。
为此,Swift 在编译时会做特化处理 (Specialization) 以优化 Generics 的功用。
Phantom Types 在 Swift 现有类型安全根底之上还能够进一步强化类型。
邂逅 Generics
在 Swift 中,能够界说泛型类型 (Generic class/struce/enum),也能够界说泛型办法。
下面咱们经过一个造作的典型的例子来逐步介绍 Swift Generics 的特性。
Generic Types
完成一个自界说的 Array (BetterArray
):
public struct BetterArray {
var storages: [Any] = [] //
mutating func append(_ newEelement: Any) {
stroages.append(newEelement)
}
}
看起来还不错?
but,元素类型怎样是 Any
❓
很不 “Swift”!
元素类型又不能写死,那该怎样办?
这时就轮到泛型上台了:
//
public struct BetterArray<T> {
var storages: [T] = []
mutating func append(_ newElement: T) {
storages.append(newElement)
}
}
如上,为 BetterArray
添加了泛型 (T
)
对泛型名 T
不是很满足,能够给它起个更有含义的姓名:
//
public struct BetterArray<Element> {
var storages: [Element] = []
mutating func append(_ newElement: Element) {
storages.append(newElement)
}
}
初始化 BetterArray
时需指定泛型的详细类型,如:
//
var betterArray = BetterArray<Int>()
目前 BetterArray
的功用有点简略,给它添加一个 index(of:)
的才能,即检索某个元素的 index:
func index(of element: Element) -> Int? {
storages.firstIndex(of: element)
}
很遗憾,编译报错 :
Referencing instance method 'firstIndex(of:)' on 'Collection' requires that 'Element' conform to 'Equatable'
简略讲,便是要求 BetterArray
中的元素完成 Equatable
协议。
问题不大,Generics 能够添加类型束缚 (Type Constraints)。
Type Constraints
// 也能够用 where clause:
// public struct BetterArray<Element> where Element: Equatable
//
public struct BetterArray<Element: Equatable> {
var storages: [Element] = []
mutating func append(_ newElement: Element) {
storages.append(newElement)
}
func index(of element: Element) -> Int? {
storages.firstIndex(of: element)
}
}
完美!
but,可能会接到投诉 ,「 我仅仅要用 BetterArray
做些存储,并不需求调用 index(of:)
办法,凭啥要完成 Equatable
?!」
问题大不,Type Constraints 不只能够加在 Generics 类型界说时,也能够经过「 where clause 」加在详细办法上:
public struct BetterArray<Element> {
// ...
//
func index(of element: Element) -> Int? where Element: Equatable {
storages.firstIndex(of: element)
}
}
这时,只需不调用 index(of:)
办法,任何类型都能够用 BetterArray
:
struct DemoElement {}
var betterArray = BetterArray<DemoElement>() // ✅
betterArray.append(DemoElement()) // ✅
// ❌ Instance method 'index(of:)' requires that 'DemoElement' conform to 'Equatable'
betterArray.index(of: DemoElement())
BetterArray
还需求个 remove
功用 :
public struct BetterArray<Element> {
// ...
//
func index(of element: Element) -> Int? where Element: Equatable {
storages.firstIndex(of: element)
}
//
mutating func remove(_ nouseElement: Element) where Element: Equatable {
storages.removeAll { $0 == nouseElement }
}
}
如上,index(of)
、remove
两个办法都要求 Element
完成 Equatable
, 此时能够为 BetterArray
增加一个分类,并将 Type Constraints 一致放在分类上:
public struct BetterArray<Element> { /* ... */ }
//
extension BetterArray where Element: Equatable {
func index(of element: Element) -> Int? {
storages.firstIndex(of: element)
}
mutating func remove(_ nouseElement: Element) {
storages.removeAll { $0 == nouseElement }
}
}
关于 Generic Type Constraints,有三种状况:
-
Protocol Constraints:如上所示,要求类型完成某个协议 (
where Element: Equatable
); -
Class Constraints:要求类型是某个类的子类 (where Element: UIView),如:
extension BetterArray where Element: UIView { func subviews(at index: Int) -> [UIView]? { guard index < storages.count else { return nil } return storages[index].subviews } }
-
Same-type Constraints:要求类型是某个详细的类型值 (
where Element == String
),如:extension BetterArray where Element == String { func splice() -> Element { storages.reduce("") { partialResult, element in partialResult + element } } }
Same-type Constraints 一般只出现在 extension 或详细某个办法上,若出现在类型界说上就没有含义了,如:
// ⚠️ Same-type requirement makes generic parameter 'T' non-generic; this is an error in Swift 6 struct BadArray<T> where T == String {}
Protocol associatedtype Constraints 也是上面 3 种状况。
总归,Type Constraints 赋以 Generics 更大的操作空间。
不加 Type Constraints 的泛型除了存储,其他基本上什么也做不了!
连实例化都做不了,由于没有
init
办法!
Generic Functions
BetterArray
怎样能少了「函数式」的才能呢,加个 map
:
public struct BetterArray<Element> {
//
func map<T>(_ transform: (Element) throws -> T) rethrows -> [T] {
try storages.map(transform)
}
}
如上,不只能够给类型 (Class、Struct、Enum) 加上 Generics,还能够给办法添加 Generics。
泛型办法的调用不需求显式指定对应的详细类型:
// 经过 Inferring Type,可知详细类型为 String,不需求手动指定
//
let result = betterArray.map { _ in "" }
正如在 Swift 最佳实践之 Protocol 中介绍的,从 Swift 5.7 起,关于有 Protocol Constraints 的泛型办法能够用
some
关键字改写,更简练:
func someDemo<P: Equatable>(_ other: P) -> Bool { // ... } // Equivalent to func someDeom(_ other: some Equatable) -> Bool { // ... }
“深化” Generics
编译器是怎么处理 Generics 的?
依据 Swift 最佳实践之 Protocol 中相关经验看,应该不简略
总的来说,Swift 对 Generics 的处理分 2 种状况:
- 运行时,对 Generics 做装箱处理 (Boxing)
- 编译时,对 Generics 做特化处理 (Specialization)
Boxing
所谓 Boxing,与用于处理 Protocol 作为类型 (Existential Type) 时的 Existential Container 十分类似。
简略来说,便是要对 Generics 做一次封装转换,Generics 在运用是实在类型可能千差万别,但 Generics 界说是需求有「固定的目标模型」。
所谓目标模型 (Object Model),首要有几个职责:
- 辅导目标实例化时属性怎么存储
- 辅导目标怎么履行 allocate、copy、destroy 等根底内存操作以及获取 size、alignment 等内存信息
- 辅导怎么查找实例办法的进口地址
如上节所述,依据 Generic Type Constraints 的不同,能够分为三种状况:
-
No Constraints,这类泛型能做的事十分少,Boxing 只需关心 allocate、copy、destroy 等基本操作怎么履行即可
-
Class Constraints,有根底类作为束缚,除了 allocate、copy、destroy 以外,还需求经过 VWT (Value Witness Table) 存储束缚类中界说的办法,以便经过 generic-types 能够调用到它们
-
Protocol Constraints,除了 allocate、copy、destroy 以外,还需求经过 PWT (Protocol Witness Table) 存储协议中指定的办法,以便经过 generic-types 能够调用它们
这里讨论的 Protocol 是没有 class constraint 的,关于只能由类完成的协议作为泛型束缚时,其效果同上面讨论的 Class Constraints。
经过 SIL (Swift Intermediate Language) 能够大致了解 Swift 背面的完成原理。
swiftc demo.swift -O -emit-sil -o demo-sil.s
如上,经过 swiftc 命令能够生成 SIL。
其中的
-O
是对生成的 SIL 代码进行编译优化,使 SIL 更简练高效。后面要讲到的泛型特化 (Specialization of Generics) 也只有在
-O
优化下会发生。
总归,Generics 对功用有影响,首要体现在 2 个方面:
- Boxing 处理
- 经过 Generics 调用的办法都是动态派发 (经过 VWT 或 PWT)
Specialization
Generics 带来的功用影响能够经过特化 (Specialization of Generics) 来优化。
所谓特化便是生成泛型的特定版别,将泛型转换为非泛型,如:
@inline(never)
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
var a = 1
var b = 2
swapTwoValues(&a, &b)
如上,经过Int
型参数调用swapTwoValues
时,编译器就会生成该办法的Int
版别:
// specialized swapTwoValues<A>(_:_:)
sil shared [noinline] @$s4main13swapTwoValuesyyxz_xztlFSi_Tg5 : $@convention(thin) (@inout Int, @inout Int) -> () {
// %0 "a" // users: %6, %4, %2
// %1 "b" // users: %7, %5, %3
bb0(%0 : $*Int, %1 : $*Int):
debug_value_addr %0 : $*Int, var, name "a", argno 1 // id: %2
debug_value_addr %1 : $*Int, var, name "b", argno 2 // id: %3
%4 = load %0 : $*Int // user: %7
%5 = load %1 : $*Int // user: %6
store %5 to %0 : $*Int // id: %6
store %4 to %1 : $*Int // id: %7
%8 = tuple () // user: %9
return %8 : $() // id: %9
} // end sil function '$s4main13swapTwoValuesyyxz_xztlFSi_Tg5'
那么,什么时候会进行泛型特化呢?
总的原则是在编译泛型办法时知道有哪些调用方,一同调用方的类型是可推演的。
最简略的状况便是泛型办法与调用方在同一个源文件里,一同进行编译。
别的,在编译时若敞开了 Whole-Module Optimization,同一模块内部的泛型调用也能够被特化。
Phantom Types
Phantom Types 并非 Swift 特有的,归于一种通用编码技巧。
Phantom Types 没有严格的界说,一般表述是:出现在泛型参数中,但没有被真正运用。
如下代码中的 Role
(例子来自 How to use phantom types in Swift),它只出现在泛型参数中,在 Employee
完成中并未运用:
struct Employee<Role>: Equatable {
var name: String
}
Phantom Types 有何用?
用于对类型做进一步的强化。
Employee 可能有不同的人物,如:Sales、Programmer 等,咱们将其界说为空 enum:
enum Sales { }
enum Programmer { }
由于 Employee
完成了 Equatable
,能够在两个实例间进行判等操作。
但判等操作显着只有在同一种人物间进行才有含义:
let john = Employee<Sales>.init(name: "John")
let sea = Employee<Programmer>.init(name: "Sea")
john == sea
正是由于 Phantom Types 在起作用,上述代码中的判等操作编译无法经过:
Cannot convert value of type 'Employee' to expected argument type 'Employee'
将 Phantom Types 界说成空 enum,使其无法被实例化,然后真正满足 Phantom Types 语义。
小问题
下面这段代码在 ~Swift 5.7 上报错,Type 'any FooProtocol' cannot conform to 'FooProtocol'
:
protocol FooProtocol {}
struct Foo: FooProtocol {}
func fooFunc<T: FooProtocol>(_ x: T?) {}
func test() {
let foo: any FooProtocol = Foo()
fooFunc(foo) // ❌ Type 'any FooProtocol' cannot conform to 'FooProtocol'
}
而下面 2 个版别没问题:
-
将
any FooProtocol
–>some FooProtocol
protocol FooProtocol {} struct Foo: FooProtocol {} func fooFunc<T: FooProtocol>(_ x: T?) {} func test() { // let foo: some FooProtocol = Foo() fooFunc(foo) // ✅ }
-
将泛型参数从
optional
–>non-optional
protocol FooProtocol {} struct Foo: FooProtocol {} // func fooFunc<T: FooProtocol>(_ x: T) {} func test() { let foo: any FooProtocol = Foo() fooFunc(foo) // ✅ }
why ❓
咱们先来一个好理解的版别:
protocol FooProtocol {}
struct Foo: FooProtocol {}
func fooFunc<T: FooProtocol>(_ x: T?) {}
func test() {
//
let foo: (any FooProtocol)? = Foo() // ❌ Type 'any FooProtocol' cannot conform to 'FooProtocol'
fooFunc(foo)
}
上面这段代码编译报错,原因类似于:
fooFunc(nil) // ❌ Generic parameter 'T' could not be inferred
参数是 nil
时,泛型类型无法确认!
因而,也不能以 Optional 类型去调用泛型办法,这个要求合情合理。
泛型办法若只有一个参数,不该将其界说为 Optional,如:
func fooFunc<T: FooProtocol>(_ x: T?) {}
原因在于,永久不可能以
nil
或 Optional 变量去调用fooFunc
在有多个参数时,能够,如:
func fooFunc2<T: FooProtocol>(_ x: T?, _ y: T) {} fooFunc2(nil, Foo())
总归,在调用泛型办法时,相关泛型类型需求是清晰的!
关键是,上面是以 non-Optional 类型 (let foo: any FooProtocol
) 调用的泛型办法 (fooFunc
),为何也不可❓
如上,Swift-Evolution 0352-implicit-open-existentials
简略讲,理论上能够,没问题,但 Apple 爸爸挑选不能够!
理由是,看起来很奇怪
好消息是,在 Swift 5.8 (Xcode 14.3) 上能够正确编译了 Swift-Evolution 0375-opening-existential-optional
在 ~Swift 5.7 上能够经过类型擦除 (Type Erasure) 的方法处理:
protocol FooProtocol {
func bar()
}
struct Foo: FooProtocol {
func bar() {}
}
//
struct AnyFoo: FooProtocol {
let anyInstance: any FooProtocol
func bar() {
anyInstance.bar()
}
}
func fooFunc<T: FooProtocol>(_ x: T?) {}
func test() {
let foo: any FooProtocol = Foo()
//
fooFunc(AnyFoo(anyInstance: foo))
}
小结
本文对 Swift Generics 进行了扼要介绍,经过 Generics + Type Constraints 能够写出十分灵敏实用的代码。
Generics 也会带来必定的功用损耗,经过泛型特化 (Specialization) 能够优化 Generics 功用。
Phantom Types 作为一种通用编码技巧,在 Swift 中同样能够用来完成类型增加。
参考资料
Embrace Swift generics – WWDC22 – Videos
Swift Generics (Expanded) – WWDC18 – Videos
Swift Docs Generics
swift/OptimizationTips.rst at main apple/swift GitHub
Whats behind swift generic system?
Swift.org – Whole-Module Optimization in Swift 3
swift/SIL.rst at main apple/swift GitHub
How to use phantom types in Swift
Measurements and Units with Phantom Types
Phantom types in Swift
Building type-safe networking in Swift
Type-Safe File Paths with Phantom Types – Swift Talk