通用代码使您可以编写灵活,可重用的函数和类型,这些函数和类型可以根据您定义的要求与任何类型一起使用。 您可以编写避免重复的代码,并以清晰抽象的方式表达其意图。 -Swift文档
在Swift上写的每个人都使用泛型。 Array
, Dictionary
,Set-使用标准库中泛型的最基本选项。 它们如何在内部表示? 让我们看看苹果工程师如何实现该语言的基本功能。
泛型参数可以受协议限制,也可以不受限制,尽管从根本上说,泛型与协议结合使用,协议描述了方法参数或类型字段可以精确执行的操作。
为了实现泛型,Swift使用两种方法:
- 运行时-通用代码是包装器(装箱)。
- 编译时-通用代码被转换为特定类型的代码以进行优化(Specialization)。
装箱
考虑一个带有无限协议通用参数的简单方法:
func test<T>(value: T) -> T { let copy = value print(copy) return copy }
迅捷的编译器创建了一个代码块,该代码块将被调用以与任何<T>
。 也就是说,无论我们编写test(value: 1)
还是test(value: "Hello")
,都将调用相同的代码,并且有关<T>
类型的信息(包含所有必要的信息)将被转移到方法中。
这种无限制的协议参数几乎无济于事,但已经实现了此方法,您需要知道如何复制参数,需要知道其大小才能在运行时为其分配内存,需要知道如何在参数离开字段时销毁它能见度。 Value Witness Table
( VWT
)用于存储此信息。 VWT
是在编译阶段为所有类型创建的,编译器保证在运行时只有这样的对象布局。 让我提醒您,Swift中的结构是按值传递的,而类则是按引用传递的,因此,对于let copy = value
以及T == MyClass
和T == MyStruct
进行的操作将有所不同。
也就是说,通过传递已声明的结构来调用test
方法最终将看起来像这样:
当MyStruct
本身是通用结构并且采用MyStruct<T>
的形式时,事情会变得更加复杂。 根据MyStruct
内部的<T>
,对于MyStruct<Int>
和MyStruct<Bool>
类型,元数据和VWT
将有所不同。 这是运行时的两种不同类型。 但是,为MyStruct
和T
每种可能组合创建元数据效率极低,因此Swift采取了另一种方式,在这种情况下,可以在运行时在运行时构造元数据。 编译器为通用结构创建一个元数据模式,该元数据模式可以与特定类型组合,从而在运行时以正确的VWT
接收完整的类型信息。
当我们组合信息时,我们将获得可以使用的元数据(复制,移动,销毁)。
将协议限制添加到泛型时,它仍然有些复杂。 例如,我们将<T>
限制为Equatable
协议。 让它成为比较传递的两个参数的非常简单的方法。 结果只是比较方法的包装。
func isEquals<T: Equatable>(first: T, second: T) -> Bool { return first == second }
为了使程序正常工作,您必须具有一个指向比较方法static func ==(lhs:T, rhs:T)
的指针。 如何获得? 显然, VWT
传输VWT
不够的,它不包含此信息。 为了解决此问题,有一个Protocol Witness Table
或PWT
。 该VWT
类似于VWT
,是在协议的编译阶段创建的,并描述了这些协议。
isEquals(first: 1, second: 2)
- 传递了两个论点
- 传递
Int
元数据,以便您可以复制/移动/销毁对象 - 我们传递
Int
实现Equatable
的信息。
如果该限制要求实现另一个协议,例如T: Equatable & MyProtocol
,则将使用以下参数添加有关MyProtocol
信息:
isEquals(..., intIsEquatable: Equatable.witnessTable, intIsMyProtocol: MyProtocol.witnessTable)
使用包装器来实现泛型可以使您灵活地实现所有必需的功能,但是开销可以优化。
通用专业化
为了消除在程序执行期间获取信息的不必要需求,使用了所谓的通用专业化方法。 它允许您使用具有特定实现的特定类型替换通用包装。 例如,对于两次调用isEquals(first: 1, second: 2)
和isEquals(first: "Hello", second: "world")
调用,除了主要的“包装器”实现外,还有另外两个版本完全不同的Int
和对于String
。
源代码
首先,创建一个generic.swift文件并编写一个我们将考虑的小的通用函数。
func isEquals<T: Equatable>(first: T, second: T) -> Bool { return first == second } isEquals(first: 10, second: 11)
现在,您需要了解它最终变成编译器的原因。
通过使用Swift Intermediate Language或SIL
编译.swift文件,可以清楚地看到这一点。
关于SIL和编译过程的一些知识
SIL
是快速编译的几个阶段之一的结果。
源代码.swift被传递给Lexer,后者创建该语言的抽象语法树( AST
),并基于该树执行代码的类型检查和语义分析。 SilGen将AST
转换为SIL
,称为raw SIL
,在此基础上对代码进行了优化canonical SIL
获得了优化的canonical SIL
,并将其传递给IRGen
转换为IR
- LLVM
理解为一种特殊格式,该格式将转换为 , .
.o , .
, .
SIL` , .
。
再次泛型
从我们的源代码创建SIL
文件。
swiftc generic.swift -O -emit-sil -o generic-sil.s
我们得到一个扩展名为*.s
的新文件。 从内部看,我们将看到比原始代码更少的可读性代码,但是仍然相对清晰。
找到带有注释的行// isEquals<A>(first:second:)
。 这是我们方法描述的开始。 它以注释// end sil function '$s4main8isEquals5first6secondSbx_xtSQRzlF'
。 您的名字可能略有不同。 让我们分析一下方法描述。
- 第21行的
%0
和%1
分别是first
和second
参数 - 在第24行,我们获得类型信息并将其传递给
%4
- 在第25行,我们从类型信息中获得了一个指向比较方法的指针
- 在第26行上,我们通过指针调用该方法,并同时传递了参数和类型信息
- 在第27行,我们给出结果。
结果,我们看到:为了在通用方法的实现中执行必要的动作,我们需要在程序执行期间从类型<T>
的描述中获取信息。
我们直接进行专业化。
在已编译的SIL
文件中,在常规isEquals
方法的声明之后,紧随其后的是Int
类型专用的声明。
在第39行,不是从运行时从类型信息中获取方法,而是立即调用了比较整数"cmp_eq_Int64"
的方法。
为了使该方法“专业化”,必须启用优化 。 您还需要知道
仅当通用声明的定义在当前模块( 源 )中可见时,优化器才能执行专门化
也就是说,该方法不能在不同的Swift模块之间专用(例如,来自Cocoapods库的通用方法)。 标准Swift库是一个例外,其中包含Array
, Set
和Dictionary
等基本类型。 基础库的所有泛型都专门针对特定类型。
注意:在Swift 4.2中,实现了@inlinable
和@usableFromInline
属性,该属性使优化器可以查看其他模块中的方法主体,并且似乎有机会对其进行专门化,但是我未对此行为进行测试( Source )
参考文献
- 泛型说明
- Swift中的优化
- 关于该主题的更详细和深入的介绍。
- 英文文章