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 最佳实践之 Generics

如上,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