草庐IT

iOS开发-Swift进阶之泛型!

iOS是大鑫呀 2023-03-28 原文
swift进阶总汇

本文主要介绍泛型及其底层原理

泛型

泛型主要用于解决代码的抽象能力 + 代码的复用性

例如下面的例子,其中的T就是泛型

func test<T>(_ a: T, _ b: T)->Bool{ return a == b } //经典例子swap,使用泛型,可以满足不同类型参数的调用 func swap<T>(_ a: inout T, _ b: inout T){ let tmp = a a = b b = tmp }

类型约束

在一个类型参数后面放置协议或者是类,例如下面的例子,要求类型参数T遵循Equatable协议

func test<T: Equatable>(_ a: T, _ b: T)->Bool{ return a == b } 当传入的参数是没有遵循Equatable协议时,会报错

关联类型

在定义协议时,使用关联类型协议中用到的类型起一个占位符名称

  • 此时的数组中的类型是Int
struct CJLStack { private var items = [Int]() mutating func push(_ item: Int){ items.append(item) } mutating func pop() -> Int?{ if items.isEmpty { return nil } return items.removeLast() } }
  • 如果想使用其他类型呢?可以通过协议来实现
protocol CJLStackProtocol { //协议中使用类型的占位符 associatedtype Item } struct CJLStack: CJLStackProtocol{ //在使用时,需要指定具体的类型 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() } }

where语句

where语句主要用于 表明泛型需要满足的条件,即限制形式参数的要求,如下所示

//***********3、where语句:表明泛型需要满足的条件 protocol CJLStackProtocol { //协议中使用类型的占位符 associatedtype Item var itemCount: Int {get} mutating func pop() -> Item? func index(of index: Int) -> Item } struct CJLStack: CJLStackProtocol{ //在使用时,需要指定具体的类型 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: CJLStackProtocol, T2: CJLStackProtocol>(_ 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 下面这种写法也是可以的

//写法二 protocol CJLStackProtocol { //协议中使用类型的占位符 associatedtype Item var itemCount: Int {get} mutating func pop() -> Item? func index(of index: Int) -> Item } struct CJLStack: CJLStackProtocol{ //在使用时,需要指定具体的类型 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] } } extension CJLStackProtocol where Item: Equatable{}
  • 当希望泛型指定类型时拥有特定功能,可以像下面这么写(在上述写法二的基础上增加extension)
//当希望泛型指定类型时拥有特定功能,可以像下面这么写 extension CJLStackProtocol where Item == Int{ func test(){ print("test") } } var s = CJLStack() s.test() <!--打印结果--> test
  • 如果将where后的Int改成Double类型,是无法找到test函数的

泛型函数

我们在上面介绍了泛型的基本语法,下面来分析下泛型的底层原理

以下面一个简单的泛型函数为例

//简单的泛型函数 func testGenric<T>(_ value: T) -> T{ let tmp = value return tmp } class CJLTeacher { var age: Int = 18 var name: String = "Kody" } //传入Int类型 testGenric(10) //传入元组 testGenric((10, 20)) //传入实例对象 testGenric(CJLTeacher()) 从上面的代码中可以看出,泛型函数可以接受任何类型

疑问:那么泛型是如何区分不同的参数,来管理不同类型的内存呢?

  • 查看SIL代码,并没有什么内存相关的信息

  • 查看IR代码,从中可以得出VWT中存放的是 size(大小)、alignment(对齐方式)、stride(步长)、destorycopy(函数)

    所以VWT+PWT的存储结构图示如下所示

源码分析

  • 在swift-source中搜索valueWitnesses(在Metadata.h中) 对于每一个类型(Int或者自定义),都在metadata中存储了一个VWT(用来管理当前类型的值)

  • 继续来到Metadataimpl.h文件,查看其中的元组的源码

然后回到刚开始的泛型函数testGenric

func testGenric<T>(_ value: T) -> T{ //tmp在栈上申请空间,如何知道申请多大呢?可以通过metadata中存储的vwt得知 //copy let tmp = value //destory return tmp } 其IR代码的详细分析如下

; Function Attrs: argmemonly nounwind willreturn 泛型函数 declare void @llvm.lifetime.start.p0i8(i64 immarg, i8* nocapture) #1 ; %swift.type* %T 表示 传入类型的matadata define hidden swiftcc void @"$s4main10testGenricyxxlF"(%swift.opaque* noalias nocapture sret %0, %swift.opaque* noalias nocapture %1, %swift.type* %T) #0 { entry: %T1 = alloca %swift.type*, align 8 %tmp.debug = alloca i8*, align 8 %2 = bitcast i8** %tmp.debug to i8* call void @llvm.memset.p0i8.i64(i8* align 8 %2, i8 0, i64 8, i1 false) store %swift.type* %T, %swift.type** %T1, align 8 %3 = bitcast %swift.type* %T to i8*** %4 = getelementptr inbounds i8**, i8*** %3, i64 -1 ; valueWitnesses 值目录表,将其存入了 %swift.vwtable* 中 %T.valueWitnesses = load i8**, i8*** %4, align 8, !invariant.load !46, !dereferenceable !47 ; 做了一个类型转换 %5 = bitcast i8** %T.valueWitnesses to %swift.vwtable* ; 在valueWitnesses中获取当前这个类型的size大小 %6 = getelementptr inbounds %swift.vwtable, %swift.vwtable* %5, i32 0, i32 8 %size = load i64, i64* %6, align 8, !invariant.load !46 ; 然后根据获取的size,分配内存空间 %7 = alloca i8, i64 %size, align 16 call void @llvm.lifetime.start.p0i8(i64 -1, i8* %7) %8 = bitcast i8* %7 to %swift.opaque* ; 初始化tmp的内存空间 store i8* %7, i8** %tmp.debug, align 8 %9 = getelementptr inbounds i8*, i8** %T.valueWitnesses, i32 2 %10 = load i8*, i8** %9, align 8, !invariant.load !46 ; copy 拷贝 %initializeWithCopy = bitcast i8* %10 to %swift.opaque* (%swift.opaque*, %swift.opaque*, %swift.type*)* %11 = call %swift.opaque* %initializeWithCopy(%swift.opaque* noalias %8, %swift.opaque* noalias %1, %swift.type* %T) #6 %12 = call %swift.opaque* %initializeWithCopy(%swift.opaque* noalias %0, %swift.opaque* noalias %8, %swift.type* %T) #6 %13 = getelementptr inbounds i8*, i8** %T.valueWitnesses, i32 1 %14 = load i8*, i8** %13, align 8, !invariant.load !46 ; destory 销毁 %destroy = bitcast i8* %14 to void (%swift.opaque*, %swift.type*)* call void %destroy(%swift.opaque* noalias %8, %swift.type* %T) #6 %15 = bitcast %swift.opaque* %8 to i8* call void @llvm.lifetime.end.p0i8(i64 -1, i8* %15) ret void } 所以,从IR代码中可以得知,当前泛型是通过ValueWitnessTable来进行内存操作

源码调试

调试分为两种,值类型引用类型

引用类型调试

  • 源码调试如下

  • retain函数中加断点调试

  • 通过lldb调试如下:obj中存储CJLTeacher变量

结论:对于引用类型,会调用retain进行引用计数+1,对于destory来说,就会调用release进行引用计数-1

  • 泛型类型使用VWT进行内存管理,VWT由编译器生成,其存储了该类型的size、alignment以及针对该类型的基本内存操作

  • 当对泛型类型进行内存操作时(例如:内存拷贝)时,最终会调用对应泛型的VWT中的基本内存操作

  • 泛型类型不同,其对应的VWT也不同

值类型调试

  • initializeWithTake方法中加断点
结论:值类型是通过当前内存的copy、move来进行内存拷贝。对于destory,内部调用析构函数

总结

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发交流群:130 595 548,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)

  • 对于一个值类型,例如Integer,

  • 1、该类型的copymove操作会进行内存拷贝

  • 2、destory操作则不进行任何操作

  • 对于一个引用类型,如class,

  • 1、该类型的copy操作会对引用计数+1

  • 2、move操作会拷贝指针,而不会更新引用计数;

  • 3、destory操作会对引用计数-1

泛型函数传入函数的分析

上面都是对变量进行的分析,那么一问来了

如果泛型函数中传的是一个函数呢?

代码如下所示,此时传入的m,是传入的整个结构体吗?

//如果此时传入的是一个函数呢? 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函数,而是做了一层抽象,目的是解决不同类型在传递过程中的问题

总结

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发交流群:130 595 548,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)

  • 泛型主要用于解决代码的抽象能力,以及提升代码的复用性

  • 如果一个泛型遵循了某个协议,则在使用时,要求具体的类型也是必须遵循某个协议的

  • 在定义协议时,可以使用关联类型协议中用到的类型起一个占位符名称

  • where语句主要用于表明泛型需要满足的条件,即限制形式参数的要求

  • 泛型类型使用VWT进行内存管理(即通过VWT区分不同类型),VWT由编译器生成,其存储了该类型的size、alignment以及针对该类型的基本内存操作

  • 1、当对泛型类型进行内存操作时(例如:内存拷贝)时,最终会调用对应泛型的VWT中的基本内存操作

  • 2、泛型类型不同,其对应的VWT也不同

  • 希望泛型指定类型时拥有特定功能,可以通过extension实现

  • 对于泛型函数来说,有以下几种情况:

  • 1、该类型的copy操作会对引用计数+1

  • 2、move操作会拷贝指针,而不会更新引用计数;

  • 3、destory操作会对引用计数-1

  • 1、该类型的copymove操作会进行内存拷贝

  • 2、destory操作则不进行任何操作

  • 传入的是一个值类型,例如Integer,

  • 传入的是一个引用类型,如class,

  • 如果泛型函数传入的是一个函数,在传递过程中,会做一层包装,简单来说,就是不会直接将函数的函数值+type给泛型函数,而是做了一层抽象,主要是用于解决不同类型的传递问题

有关iOS开发-Swift进阶之泛型!的更多相关文章

  1. ruby - 使用 C 扩展开发 ruby​​gem 时,如何使用 Rspec 在本地进行测试? - 2

    我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当

  2. Ruby Sinatra 配置用于生产和开发 - 2

    我已经在Sinatra上创建了应用程序,它代表了一个简单的API。我想在生产和开发上进行部署。我想在部署时选择,是开发还是生产,一些方法的逻辑应该改变,这取决于部署类型。是否有任何想法,如何完成以及解决此问题的一些示例。例子:我有代码get'/api/test'doreturn"Itisdev"end但是在部署到生产环境之后我想在运行/api/test之后看到ItisPROD如何实现? 最佳答案 根据SinatraDocumentation:EnvironmentscanbesetthroughtheRACK_ENVenvironm

  3. ruby - 如何验证 IO.copy_stream 是否成功 - 2

    这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下

  4. ruby - 是否可以覆盖 gemfile 进行本地开发? - 2

    我们的git存储库中目前有一个Gemfile。但是,有一个gem我只在我的环境中本地使用(我的团队不使用它)。为了使用它,我必须将它添加到我们的Gemfile中,但每次我checkout到我们的master/dev主分支时,由于与跟踪的gemfile冲突,我必须删除它。我想要的是类似Gemfile.local的东西,它将继承从Gemfile导入的gems,但也允许在那里导入新的gems以供使用只有我的机器。此文件将在.gitignore中被忽略。这可能吗? 最佳答案 设置BUNDLE_GEMFILE环境变量:BUNDLE_GEMFI

  5. ruby - 在 Windows 机器上使用 Ruby 进行开发是否会适得其反? - 2

    这似乎非常适得其反,因为太多的gem会在window上破裂。我一直在处理很多mysql和ruby​​-mysqlgem问题(gem本身发生段错误,一个名为UnixSocket的类显然在Windows机器上不能正常工作,等等)。我只是在浪费时间吗?我应该转向不同的脚本语言吗? 最佳答案 我在Windows上使用Ruby的经验很少,但是当我开始使用Ruby时,我是在Windows上,我的总体印象是它不是Windows原生系统。因此,在主要使用Windows多年之后,开始使用Ruby促使我切换回原来的系统Unix,这次是Linux。Rub

  6. Ruby 文件 IO 定界符? - 2

    我正在尝试解析一个文本文件,该文件每行包含可变数量的单词和数字,如下所示:foo4.500bar3.001.33foobar如何读取由空格而不是换行符分隔的文件?有什么方法可以设置File("file.txt").foreach方法以使用空格而不是换行符作为分隔符? 最佳答案 接受的答案将slurp文件,这可能是大文本文件的问题。更好的解决方案是IO.foreach.它是惯用的,将按字符流式传输文件:File.foreach(filename,""){|string|putsstring}包含“thisisanexample”结果的

  7. ruby-on-rails - 在 Rails 开发环境中为 .ogv 文件设置 Mime 类型 - 2

    我正在玩HTML5视频并且在ERB中有以下片段:mp4视频从在我的开发环境中运行的服务器很好地流式传输到chrome。然而firefox显示带有海报图像的视频播放器,但带有一个大X。问题似乎是mongrel不确定ogv扩展的mime类型,并且只返回text/plain,如curl所示:$curl-Ihttp://0.0.0.0:3000/pr6.ogvHTTP/1.1200OKConnection:closeDate:Mon,19Apr201012:33:50GMTLast-Modified:Sun,18Apr201012:46:07GMTContent-Type:text/plain

  8. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  9. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  10. Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting - 2

    1.错误信息:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:requestcanceledwhilewaitingforconnection(Client.Timeoutexceededwhileawaitingheaders)或者:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:TLShandshaketimeout2.报错原因:docker使用的镜像网址默认为国外,下载容易超时,需要修改成国内镜像地址(首先阿里

随机推荐