泛型
是Swift
最强大的特性之一,oc转Swift的需要重点学习一下。
① 泛型代码能根据所定义的要求写出可以用于任何类型的灵活的、可复用的函数。可以编写出可复用、意图表达清晰、抽象的代码。
② 泛型是Swift最强大的特性之一,很多Swift标准库是基于泛型代码构建的。如,Swift 的Array
和Dictionary
类型都是泛型集合;你也可以创建一个容纳Int
值的数组,或者容纳String
值的数组,甚至容纳任何 Swift 可以创建的其他类型的数组。同样,可以创建一个存储任何指定类型值的字典,而且类型没有限制。
③ 泛型所解决的问题:代码的复用性和抽象能力
。比如,交换两个值,这里的值可以是Int
、Double
、String
。
//经典例子swap,使用泛型,可以满足不同类型参数的调用
func swap<T>(_ a: inout T, _ b: inout T){
let tmp = a
a = b
b = tmp
}
一、基础语法
主要讲3点:类型约束
、关联类型
、Where语句
。
1.1 类型约束
在一个类型参数后面放置协议或者是类
,例如下面的例子,要求类型参数T遵循Equatable协议
。
func test<T: Equatable>(_ a: T, _ b: T)->Bool{
return a == b
}
1.2 关联类型
在定义协议时,使用
关联类型
给协议中用到的类型起一个占位符名称
。关联类型只能用于协议
,并且是通过关键字associatedtype
指定。
下面这个示例,仿写的一个栈的结构体
struct LLStack {
private var items = [Int]()
mutating func push(_ item: Int){
items.append(item)
}
mutating func pop() -> Int?{
if items.isEmpty {
return nil
}
return items.removeLast()
}
}
该结构体中有个成员Item
,是个只能存储Int
类型的数组,如果想使用其他类型呢? 可以通过协议
来实现 :
protocol LLStackProtocol {
//协议中使用类型的占位符
associatedtype Item
}
struct LLStack: LLStackProtocol{
//在使用时,需要指定具体的类型
typealias Item = Int
private var items = [Item]()
mutating func push(_ item: Item){
items.append(item)
}
mutating func pop() -> Item?{
if items.isEmpty {
return nil
}
return items.removeLast()
}
}
我们在尝试用泛型
实现上面的功能:
struct LLStack<Element> {
private var items = [Element]()
mutating func push(_ item: Element){
items.append(item)
}
mutating func pop() -> Element?{
if items.isEmpty {
return nil
}
return items.removeLast()
}
}
泛型的优势和强大,暴露无疑。
2.3 Where语句
where语句
主要用于表明泛型需要满足的条件
,即限制形式参数的要求
。
protocol LLStackProtocol {
//协议中使用类型的占位符
associatedtype Item
var itemCount: Int {get}
mutating func pop() -> Item?
func index(of index: Int) -> Item
}
struct LLStack: LLStackProtocol{
//在使用时,需要指定具体的类型
typealias Item = Int
private var items = [Item]()
var itemCount: Int{
get{
return items.count
}
}
mutating func push(_ item: Item){
items.append(item)
}
mutating func pop() -> Item?{
if items.isEmpty {
return nil
}
return items.removeLast()
}
func index(of index: Int) -> Item {
return items[index]
}
}
/*
where语句
- T1.Item == T2.Item 表示T1和T2中的类型必须相等
- T1.Item: Equatable 表示T1的类型必须遵循Equatable协议,意味着T2也要遵循Equatable协议
*/
func compare<T1: LLStackProtocol, T2: LLStackProtocol>(_ stack1: T1, _ stack2: T2)
-> Bool where T1.Item == T2.Item, T1.Item: Equatable{
guard stack1.itemCount == stack2.itemCount else {
return false
}
for i in 0..<stack1.itemCount {
if stack1.index(of: i) != stack2.index(of: i){
return false
}
}
return true
}
还可以这么写:
extension LLStackProtocol where Item: Equatable{}
当希望泛型指定类型时拥有特定功能,可以这么写,在上述写法的基础上增加extension
:
extension LLStackProtocol where Item == Int{
func test(){
print("test")
}
}
var s = LGStack()
泛型函数
简单示例:
//简单的泛型函数
func testGenric<T>(_ value: T) -> T{
let tmp = value
return tmp
}
class Teacher {
var age: Int = 18
var name: String = "Kody"
}
//传入Int类型
testGenric(10)
//传入元组
testGenric((10, 20))
//传入实例对象
testGenric(Teacher())
从以上代码可以看出,泛型函数可以接受任何类型
。
问题? 泛型是如何区分不同的参数,来管理不同类型的内存呢?
查看SIL代码,并没有什么内存相关的信息。查看IR代码,从中可以得出VWT
中存放的是 size(大小)
、alignment(对齐方式)
、stride(步长)
、destory
、copy(函数)
。
2.1 VWT
看下VWT
的源码(在Metadata.h
中TargetValueWitnessTable
):
/// A value-witness table. A value witness table is built around
/// the requirements of some specific type. The information in
/// a value-witness table is intended to be sufficient to lay out
/// and manipulate values of an arbitrary type.
template <typename Runtime> struct TargetValueWitnessTable {
// For the meaning of all of these witnesses, consult the comments
// on their associated typedefs, above.
#define WANT_ONLY_REQUIRED_VALUE_WITNESSES
#define VALUE_WITNESS(LOWER_ID, UPPER_ID) \
typename TargetValueWitnessTypes<Runtime>::LOWER_ID LOWER_ID;
#define FUNCTION_VALUE_WITNESS(LOWER_ID, UPPER_ID, RET, PARAMS) \
typename TargetValueWitnessTypes<Runtime>::LOWER_ID LOWER_ID;
#include "swift/ABI/ValueWitness.def"
using StoredSize = typename Runtime::StoredSize;
/// Is the external type layout of this type incomplete?
bool isIncomplete() const {
return flags.isIncomplete();
}
/// Would values of a type with the given layout requirements be
/// allocated inline?
static bool isValueInline(bool isBitwiseTakable, StoredSize size,
StoredSize alignment) {
return (isBitwiseTakable && size <= sizeof(TargetValueBuffer<Runtime>) &&
alignment <= alignof(TargetValueBuffer<Runtime>));
}
/// Are values of this type allocated inline?
bool isValueInline() const {
return flags.isInlineStorage();
}
/// Is this type POD?
bool isPOD() const {
return flags.isPOD();
}
/// Is this type bitwise-takable?
bool isBitwiseTakable() const {
return flags.isBitwiseTakable();
}
/// Return the size of this type. Unlike in C, this has not been
/// padded up to the alignment; that value is maintained as
/// 'stride'.
StoredSize getSize() const {
return size;
}
/// Return the stride of this type. This is the size rounded up to
/// be a multiple of the alignment.
StoredSize getStride() const {
return stride;
}
/// Return the alignment required by this type, in bytes.
StoredSize getAlignment() const {
return flags.getAlignment();
}
/// The alignment mask of this type. An offset may be rounded up to
/// the required alignment by adding this mask and masking by its
/// bit-negation.
///
/// For example, if the type needs to be 8-byte aligned, the value
/// of this witness is 0x7.
StoredSize getAlignmentMask() const {
return flags.getAlignmentMask();
}
/// The number of extra inhabitants, that is, bit patterns that do not form
/// valid values of the type, in this type's binary representation.
unsigned getNumExtraInhabitants() const {
return extraInhabitantCount;
}
/// Assert that this value witness table is an enum value witness table
/// and return it as such.
///
/// This has an awful name because it's supposed to be internal to
/// this file. Code outside this file should use LLVM's cast/dyn_cast.
/// We don't want to use those here because we need to avoid accidentally
/// introducing ABI dependencies on LLVM structures.
const struct EnumValueWitnessTable *_asEVWT() const;
/// Get the type layout record within this value witness table.
const TypeLayout *getTypeLayout() const {
return reinterpret_cast<const TypeLayout *>(&size);
}
/// Check whether this metadata is complete.
bool checkIsComplete() const;
/// "Publish" the layout of this type to other threads. All other stores
/// to the value witness table (including its extended header) should have
/// happened before this is called.
void publishLayout(const TypeLayout &layout);
};
VWT中存放的是 size(大小)、alignment(对齐方式)、stride(步长)
,大致结构图:
metadata
中存放了VWT
来管理类型的值。
回过头,示例的IR代码执行的流程大致如下:询问metadata
中VWT:size,stride
分配内存空间,初始化temp
,调用VWT-copy
方法拷贝值到temp
,返回temp
,调用VWT-destory
方法销毁局部变量。
所以,泛型在整个运行过程中的关键依赖于metadata
。
三、泛型函数分析
代码如下:
//如果此时传入的是一个函数呢?
func makeIncrement() -> (Int) -> Int{
var runningTotal = 10
return {
runningTotal += $0
return runningTotal
}
}
func testGenric<T>(_ value: T){}
//m中存储的是一个结构体:{i8*, swift type *}
let m = makeIncrement()
testGenric(m)
分析IR代码:
define i32 @main(i32 %0, i8** %1) #0 {
entry:
%2 = alloca %swift.function, align 8
%3 = bitcast i8** %1 to i8*
; s4main13makeIncrementS2icyF 调用makeIncrement函数,返回一个结构体 {函数调用地址, 捕获值的内存地址}
%4 = call swiftcc { i8*, %swift.refcounted* } @"$s4main13makeIncrementS2icyF"()
; 闭包表达式的地址
%5 = extractvalue { i8*, %swift.refcounted* } %4, 0
; 捕获值的引用类型
%6 = extractvalue { i8*, %swift.refcounted* } %4, 1
; 往m变量地址中存值
; 将 %5 存入 swift.function*结构体中(%swift.function = type { i8*, %swift.refcounted* })
; s4main1myS2icvp ==> main.m : (Swift.Int) -> Swift.Int,即全局变量 m
store i8* %5, i8** getelementptr inbounds (%swift.function, %swift.function* @"$s4main1myS2icvp", i32 0, i32 0), align 8
; 将值放入 f 这个变量中,并强转为指针
store %swift.refcounted* %6, %swift.refcounted** getelementptr inbounds (%swift.function, %swift.function* @"$s4main1myS2icvp", i32 0, i32 1), align 8
; 将%2 强转为 i8*(即 void*)
%7 = bitcast %swift.function* %2 to i8*
call void @llvm.lifetime.start.p0i8(i64 16, i8* %7)
; 取出 function中 闭包表达式的地址
%8 = load i8*, i8** getelementptr inbounds (%swift.function, %swift.function* @"$s4main1myS2icvp", i32 0, i32 0), align 8
%9 = load %swift.refcounted*, %swift.refcounted** getelementptr inbounds (%swift.function, %swift.function* @"$s4main1myS2icvp", i32 0, i32 1), align 8
; 将返回的闭包表达式 当做一个参数传入 方法,所以 retainCount+1
%10 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %9) #2
; 创建了一个对象,存储了 <{ %swift.refcounted, %swift.function }>*
%11 = call noalias %swift.refcounted* @swift_allocObject(%swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata, i32 0, i32 2), i64 32, i64 7) #2
; 将 %swift.refcounted* %11 强转成了一个结构体类型
%12 = bitcast %swift.refcounted* %11 to <{ %swift.refcounted, %swift.function }>*
; 取出 %swift.function (最终的结果就是往 <{ %swift.refcounted, %swift.function }> 的%swift.function 中存值 ==> 做了间接的转换与传递)
%13 = getelementptr inbounds <{ %swift.refcounted, %swift.function }>, <{ %swift.refcounted, %swift.function }>* %12, i32 0, i32 1
; 取出 <i8*, %swift.function>的首地址
%.fn = getelementptr inbounds %swift.function, %swift.function* %13, i32 0, i32 0
; 将 i8* 放入 i8** %.fn 中(即创建的数据结构 <{ %swift.refcounted, %swift.function }> 的 %swift.function 中)
store i8* %8, i8** %.fn, align 8
%.data = getelementptr inbounds %swift.function, %swift.function* %13, i32 0, i32 1
store %swift.refcounted* %9, %swift.refcounted** %.data, align 8
%.fn1 = getelementptr inbounds %swift.function, %swift.function* %2, i32 0, i32 0
; 将 %swift.refcounted 存入 %swift.function 中
store i8* bitcast (void (%TSi*, %TSi*, %swift.refcounted*)* @"$sS2iIegyd_S2iIegnr_TRTA" to i8*), i8** %.fn1, align 8
%.data2 = getelementptr inbounds %swift.function, %swift.function* %2, i32 0, i32 1
store %swift.refcounted* %11, %swift.refcounted** %.data2, align 8
; 将%2强转成了 %swift.opaque* 类型,其中 %2 就是 %swift.function内存空间,即存储的东西(函数地址 + 捕获值地址)
%14 = bitcast %swift.function* %2 to %swift.opaque*
; sS2icMD ==> demangling cache variable for type metadata for (Swift.Int) -> Swift.Int 即函数的metadata
%15 = call %swift.type* @__swift_instantiateConcreteTypeFromMangledName({ i32, i32 }* @"$sS2icMD") #9
; 调用 testGenric 函数
call swiftcc void @"$s4main10testGenricyyxlF"(%swift.opaque* noalias nocapture %14, %swift.type* %15)
......
仿写泛型函数传入函数时的底层结构:
//如果此时传入的是一个函数呢?
struct HeapObject {
var type: UnsafeRawPointer
var refCount1: UInt32
var refCount2: UInt32
}
struct FunctionData<T> {
var ptr: UnsafeRawPointer
var captureValue: UnsafePointer<T>
}
struct Box<T> {
var refCounted: HeapObject
var value: T
}
struct GenData<T> {
var ref: HeapObject
var function: FunctionData<T>
}
func makeIncrement() -> (Int) -> Int{
var runningTotal = 10
return {
runningTotal += $0
return runningTotal
}
}
func testGenric<T>(_ value: T){
//查看T的存储
let ptr = UnsafeMutablePointer<T>.allocate(capacity: 1)
ptr.initialize(to: value)
/*
- 将 %13的值给了 %2即 %swift.function*
%13 = getelementptr inbounds <{ %swift.refcounted, %swift.function }>, <{ %swift.refcounted, %swift.function }>* %12, i32 0, i32 1
- 调用方法 %14 -> %2
%14 = bitcast %swift.function* %2 to %swift.opaque*
call swiftcc void @"$s4main10testGenricyyxlF"(%swift.opaque* noalias nocapture %14, %swift.type* %15)
*/
let ctx = ptr.withMemoryRebound(to: FunctionData<GenData<Box<Int>>>.self, capacity: 1) {
$0.pointee.captureValue.pointee.function.captureValue
}
print(ctx.pointee.value)//捕获的值是10
}
//m中存储的是一个结构体:{i8*, swift type *}
let m = makeIncrement()
testGenric(m)
//打印结果:10
结论:当是一个泛型函数传递过程中,会做一层包装,意味着并不会直接的将m
中的函数值
、type
给testGenric函数
,而是做了一层抽象
,目的是解决不同类型在传递过程中的问题。
总结
- 泛型主要用于解决代码的
抽象能力
,以及提升代码的复用性
; - 如果一个泛型
遵循了某个协议
,则在使用时,要求具体的类型也是必须遵循某个协议的; - 在定义协议时,可以使用
关联类型
给协议中用到的类型起一个占位符名称; -
where语句
主要用于表明泛型需要满足的条件,即限制形式参数的要求
。
- 泛型类型使用
VWT
进行内存管理(即通过VWT区分不同类型
),VWT
由编译器生成,其存储了该类型的size
、alignment
以及针对该类型的基本内存操作
;
1.1 当对泛型类型进行内存操作时(例如:内存拷贝)时,最终会调用对应泛型的VWT
中的基本内存操作;
1.2 泛型类型不同,其对应的VWT也不同;
1.3当希望泛型指定类型时拥有特定功能,可以通过extension实现
。 - 对于
泛型函数
来说,有以下几种情况:
2.1 传入的是一个值类型
,例如,Integer
:该类型的copy
和move
操作会进行内存拷贝
;destory
操作则不进行任何操作;
2.1 传入的是一个引用类型
,如class
:该类型的copy
操作会对引用计数+1
;move
操作会拷贝指针,而不会更新引用计数
;destory
操作会对引用计数-1
。 - 如果泛型函数传入的是
一个函数
,在传递过程中,会做一层包装,简单来说,就是不会直接将函数的函数值+type
给泛型函数,而是做了一层抽象,主要是用于解决不同类型的传递问题
。