深入了解Swift:通用实现

通用代码使您可以编写灵活,可重用的函数和类型,这些函数和类型可以根据您定义的要求与任何类型一起使用。 您可以编写避免重复的代码,并以清晰抽象的方式表达其意图。 -Swift文档

在Swift上写的每个人都使用泛型。 ArrayDictionary ,Set-使用标准库中泛型的最基本选项。 它们如何在内部表示? 让我们看看苹果工程师如何实现该语言的基本功能。


泛型参数可以受协议限制,也可以不受限制,尽管从根本上说,泛型与协议结合使用,协议描述了方法参数或类型字段可以精确执行的操作。


为了实现泛型,Swift使用两种方法:


  1. 运行时-通用代码是包装器(装箱)。
  2. 编译时-通用代码被转换为特定类型的代码以进行优化(Specialization)。

装箱


考虑一个带有无限协议通用参数的简单方法:


 func test<T>(value: T) -> T { let copy = value print(copy) return copy } 

迅捷的编译器创建了一个代码块,该代码块将被调用以与任何<T> 。 也就是说,无论我们编写test(value: 1)还是test(value: "Hello") ,都将调用相同的代码,并且有关<T>类型的信息(包含所有必要的信息)将被转移到方法中。


这种无限制的协议参数几乎无济于事,但已经实现了此方法,您需要知道如何复制参数,需要知道其大小才能在运行时为其分配内存,需要知道如何在参数离开字段时销毁它能见度。 Value Witness TableVWT )用于存储此信息。 VWT是在编译阶段为所有类型创建的,编译器保证在运行时只有这样的对象布局。 让我提醒您,Swift中的结构是按值传递的,而类则是按引用传递的,因此,对于let copy = value以及T == MyClassT == MyStruct进行的操作将有所不同。


价值见证表

也就是说,通过传递已声明的结构来调用test方法最终将看起来像这样:


 //  ,  metadata   let myStruct = MyStruct() test(value: myStruct, metadata: MyStruct.metadata) 

MyStruct本身是通用结构并且采用MyStruct<T>的形式时,事情会变得更加复杂。 根据MyStruct内部的<T> ,对于MyStruct<Int>MyStruct<Bool>类型,元数据和VWT将有所不同。 这是运行时的两种不同类型。 但是,为MyStructT每种可能组合创建元数据效率极低,因此Swift采取了另一种方式,在这种情况下,可以在运行时在运行时构造元数据。 编译器为通用结构创建一个元数据模式,该元数据模式可以与特定类型组合,从而在运行时以正确的VWT接收完整的类型信息。


 //   ,  metadata   func test<T>(value: MyStruct<T>, tMetadata: T.Type) { //       let myStructMetadata = get_generic_metadata(MyStruct.metadataPattern, tMetadata) ... } let myStruct = MyStruct<Int>() test(value: myStruct) //   test(value: myStruct, tMetadata: Int.metadata) //      

当我们组合信息时,我们将获得可以使用的元数据(复制,移动,销毁)。


将协议限制添加到泛型时,它仍然有些复杂。 例如,我们将<T>限制为Equatable协议。 让它成为比较传递的两个参数的非常简单的方法。 结果只是比较方法的包装。


 func isEquals<T: Equatable>(first: T, second: T) -> Bool { return first == second } 

为了使程序正常工作,您必须具有一个指向比较方法static func ==(lhs:T, rhs:T)的指针。 如何获得? 显然, VWT传输VWT不够的,它不包含此信息。 为了解决此问题,有一个Protocol Witness TablePWT 。 该VWT类似于VWT ,是在协议的编译阶段创建的,并描述了这些协议。


 isEquals(first: 1, second: 2) //   //     isEquals(first: 1, // 1 second: 2, metadata: Int.metadata, // 2 intIsEquatable: Equatable.witnessTable) // 3 

  1. 传递了两个论点
  2. 传递Int元数据,以便您可以复制/移动/销毁对象
  3. 我们传递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 LanguageSIL编译.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分别是firstsecond参数
  • 在第24行,我们获得类型信息并将其传递给%4
  • 在第25行,我们从类型信息中获得了一个指向比较方法的指针
  • 在第26行上,我们通过指针调用该方法,并同时传递了参数和类型信息
  • 在第27行,我们给出结果。

结果,我们看到:为了在通用方法的实现中执行必要的动作,我们需要在程序执行期间从类型<T>的描述中获取信息。


我们直接进行专业化。


在已编译的SIL文件中,在常规isEquals方法的声明之后,紧随其后的是Int类型专用的声明。



特种SIL

在第39行,不是从运行时从类型信息中获取方法,而是立即调用了比较整数"cmp_eq_Int64"的方法。


为了使该方法“专业化”,必须启用优化 。 您还需要知道


仅当通用声明的定义在当前模块( )中可见时,优化器才能执行专门化

也就是说,该方法不能在不同的Swift模块之间专用(例如,来自Cocoapods库的通用方法)。 标准Swift库是一个例外,其中包含ArraySetDictionary等基本类型。 基础库的所有泛型都专门针对特定类型。


注意:在Swift 4.2中,实现了@inlinable@usableFromInline属性,该属性使优化器可以查看其他模块中的方法主体,并且似乎有机会对其进行专门化,但是我未对此行为进行测试( Source


参考文献


  1. 泛型说明
  2. Swift中的优化
  3. 关于该主题的更详细和深入的介绍。
  4. 英文文章

Source: https://habr.com/ru/post/zh-CN451704/


All Articles