面向协议的编程,第3部分

关于面向协议编程的最终文章。


在这一部分中,我们将研究泛型类型变量的存储和复制方式以及调度方法如何与它们一起工作。


非共享版本


protocol Drawable { func draw() } func drawACopy(local: Drawable) { local.draw() } let line = Line() drawACopy(line) let point = Point() drawACopy(point) 

非常简单的代码。 drawACopy采用Drawable类型的参数,并调用其draw方法-就这样。


通用版


让我们看一下上面代码的通用版本:


 func drawACopy<T: Drawable>(local: T) { local.draw() } ... 

似乎什么都没有改变。 我们仍然可以只将drawACopy函数作为它的drawACopy版本,仅此而已,但是像平常一样最有趣。
通用代码具有两个重要功能:


  1. 静态多态性(也称为参数化)
  2. 调用上下文中的特定且唯一的类型(在编译时定义了通用类型T)

考虑一个例子:


 func foo<T: Drawable>(local: T) { bar(local) } func bar<T: Drawable>(local: T) { ... } let point = Point(...) foo(point) 

当我们调用foo函数时,最有趣的部分开始。 编译器确切知道变量point的类型-就是Point。 此外,从foo函数的T:Drawable类型可以从我们将已知Point类型的变量传递给该函数的那一刻起就可以自由地推断出来:T = Point。 所有类型在编译时都是已知的,编译器可以执行所有出色的优化-最重要的是内联foo调用。


 This: ```swift let point = Point(...) foo<T = Point>(point) Becomes this: ```swift bar<T = Point>(point) 

编译器只是将foo调用嵌入其实现中,并显示T:Drawable bar的通用类型。 换句话说,编译器首先嵌入对T = Point类型的foo方法的调用,然后嵌入先前嵌入的结果-类型T = Point的bar方法。


通用方法的实现


 func drawACopy<T: Drawable>(local: T) { local.draw() } drawACopy(Point(...)) 

在内部, drawACopy Swift使用协议方法表(包含T方法的所有实现)和生命周期表(包含T实例的所有生命周期方法)。 在伪代码中,它看起来像这样:


 func drawACopy<T: Drawable>(local: T, pwt: T.PWT, vwt: T.VWT) {...} drawACopy(Point(...), Point.pwt, Point.vwt) 

VWT和PWT是T中的关联类型(associatedtype)-作为类型别名(typealias),只有更好。 Point.pwt和Point.vwt是静态属性。


由于在我们的示例中T是Point,因此T定义明确,因此不需要创建容器。 在上一个drawACopydrawACopy版本(本地:Drawable)中,根据需要执行了一个存在性容器的创建-我们在本文的第二部分中对此进行了研究。


由于创建了参数,因此功能中需要生命周期表。 众所周知,Swift中的参数是通过值传递的,而不是通过链接传递的,因此,必须将其复制,并且此参数的copy方法像该参数一样属于生命周期表。 那里还有其他生命周期方法:分配,销毁和释放。


由于使用了通用代码参数的方法,因此通用函数中需要生命周期表。


广义还是非广义?


确实,使用泛型类型比仅使用协议类型可以使代码执行更快吗? 广义函数func foo<T: Drawable>(arg: T)比类似协议的对应函数fun foo(arg: Drawable)快?


我们注意到通用代码给出了一种更加静态的多态形式。 它还包括称为“通用代码专业化”的编译器优化。 让我们看看:


同样,我们有相同的代码:


 func drawACopy<T: Drawable>(local: T) { local.draw() } drawACopy(Point(...)) drawACopt(Line(...)) 

泛型函数的特殊化将创建具有此函数的特殊泛型类型的副本。 例如,如果我们使用类型为Point的变量调用drawACopy ,则编译器将创建此函数的专用版本drawACopyOfPoint (本地:Point),并且得到:


 func drawACopyOfPoint(local: Point) { local.draw() } func drawACopyOfLine(local: Line) { local.draw() } drawACopy(Point(...)) drawACopt(Line(...)) 

在此之前,可以通过粗略的编译器优化来减少什么:


 Point(...).draw() Line(...).draw() 

所有这些技巧都是可用的,因为只有在定义了所有通用类型的情况下才可以调用通用函数-在drawACopy方法中drawACopy很好地定义了通用类型(T)。


通用存储属性


考虑一个简单的结构对:


 struct Pair { let fst: Drawable let snd: Drawable } let pair = Pair(fst: Line(...), snd: Line(...)) 

当以这种方式使用它时,我们在堆上获得了2个分配(在第二部分中描述了这种情况下的确切内存条件),但是我们可以借助通用代码来避免这种情况。


Pair的通用版本如下所示:


 struct Pair<T: Drawable> { let fst: T let snd: T } 

从在通用版本中定义类型T的那一刻起,属性类型fstsnd相同的,并且也已定义。 由于定义了类型,因此编译器可以为这两个属性fstsnd分配专门的内存量。


有关专用内存量的更多详细信息:


当我们使用Pairfst版本时,属性类型fstsnd是Drawable。 任何类型都可以对应Drawable,即使它占用10 KB的内存。 也就是说,Swift无法得出有关此类型大小的结论,而将使用通用内存位置,例如,存在容器。 任何类型都可以存储在此容器中。 在使用通用代码的情况下,可以很好地识别类型,还可以识别属性的实际大小,并且Swift可以创建专门的内存位置。 例如(通用版本):


 let pair = Pair(Point(...), Point(...)) 

类型T现在是Point。 Point占用了N个字节的内存,在Pair中,我们得到了两个字节。 Swift将分配2 * N的内存并将其放在那里。


因此,使用Pair的通用版本,我们可以消除堆上不必要的分配,因为类型很容易识别,并且可以特别地进行定位-无需创建通用内存模板,因为众所周知。


结论


1.专用通用代码-值类型


具有最佳执行速度,因为:


  • 复制时没有堆分配
  • 通用代码-您为特殊类型编写函数
  • 没有参考计数
  • 静态方法分派

2.专门的通用代码-参考类型


它具有平均执行速度,因为:


  • 实例化时每个堆的分配
  • 有一个参考计数
  • 通过虚拟表动态提交方法

3.非专业通用代码-小值


  • 没有堆分配-将值放置在现有容器的值缓冲区中
  • 没有引用计数(因为堆上未放置任何内容)
  • 通过协议方法表发送动态方法

4.非专业通用代码-大值


  • 放置在堆上-将值放置在值缓冲区中
  • 有一个参考计数
  • 通过协议方法表动态分配

这种材料并不意味着类是不好的,结构是好的,并且与通用代码结合的结构是最好的。 我们要说的是,作为程序员,您有责任为自己的任务选择工具。 当您需要保留较大的值并且存在链接的语义时,类确实非常好。 结构最适合于较小的值,并且在您需要它们的语义时也是如此。 协议最适合通用代码和结构,等等。 所有工具都特定于您要解决的任务,具有积极和消极的一面。


而且,在不需要动力时不必为此付出代价 。 找到运行时间要求最少的正确抽象。


  • 结构类型-意义的语义
  • 类类型-身份
  • 通用代码-静态多态
  • 协议类型-动态多态

使用间接存储可处理较大的值。


并且不要忘记-选择正确的工具是您的责任。
感谢您对这个主题的关注。 我们希望这些文章对您有所帮助,并且很有趣。


祝你好运

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


All Articles