一尘不染

泛型和协议类型的功能参数在实践中有什么区别?

swift

给定一个没有任何关联类型的协议:

protocol SomeProtocol
{
    var someProperty: Int { get }
}

实际上,这两个功能有什么区别(意味着不是“一个是通用的,另一个不是”)?它们是否生成不同的代码,它们具有不同的运行时特性?当协议或功能变得平凡时,这些差异会改变吗?(因为编译器可能会内联这样的内容)

func generic<T: SomeProtocol>(some: T) -> Int
{
    return some.someProperty
}

func nonGeneric(some: SomeProtocol) -> Int
{
    return some.someProperty
}

我主要是在询问编译器功能的差异,我了解两者的语言水平含义。基本上,这是否nonGeneric意味着代码大小恒定,但动态分配速度较慢,而不是generic通过传递的每种类型使用不断增长的代码大小,但静态分配速度快?


阅读 314

收藏
2020-07-07

共1个答案

一尘不染

(我意识到OP很少询问语言的含义,而更多地询问编译器的功能-但我认为列出通用和协议类型的函数参数之间的一般差异也很值得)

1.受协议约束的通用占位符必须满足具体类型

这是协议不符合自身的结果,因此您不能generic(some:)使用SomeProtocol类型化的参数进行调用。

struct Foo : SomeProtocol {
    var someProperty: Int
}

// of course the solution here is to remove the redundant 'SomeProtocol' type annotation
// and let foo be of type Foo, but this problem is applicable anywhere an
// 'anything that conforms to SomeProtocol' typed variable is required.
let foo : SomeProtocol = Foo(someProperty: 42)

generic(some: something) // compiler error: cannot invoke 'generic' with an argument list
                         // of type '(some: SomeProtocol)'

这是由于通用函数预计某些类型的参数T符合SomeProtocol-但是SomeProtocol不是
一个类型符合SomeProtocol

然而,一个非通用的功能,与参数类型的SomeProtocol 接受foo作为参数:

nonGeneric(some: foo) // compiles fine

这是因为它接受“可以键入为”的任何内容SomeProtocol,而不是“符合”的特定类型SomeProtocol

2.专业化

就像在这个梦幻般的WWDC演讲中所涵盖的那样,使用“现有容器”来表示协议类型的值。

该容器包括:

  • 一个值缓冲区,用于存储值本身,长度为3个字。大于此值的值将被堆分配,并且对该值的引用将存储在值缓冲区中(因为引用的大小仅为1个字)。

  • 指向类型的元数据的指针。类型的元数据中包括指向其值见证表的指针,该表管理存在容器中值的生存期。

  • 指向给定类型的协议见证表的一个或多个(在协议组合的情况下)。这些表跟踪可在给定协议类型实例上调用的协议要求的类型实现。

默认情况下,使用类似的结构以便将值传递到通用占位符类型的参数。

  • 参数存储在一个3字的值缓冲区(可以进行堆分配)中,然后将其传递给参数。

  • 对于每个通用占位符,该函数采用元数据指针参数。调用时,用于满足占位符的类型的元类型将传递给此参数。

  • 对于给定占位符上的每个协议约束,该函数采用协议见证表指针参数。

但是,在优化的构建中,Swift能够 专门
化泛型函数的实现–允许编译器为其应用的每种类型的泛型占位符生成一个新函数。这使得参数始终可以按值简单地传递,但代价是增加了代码大小。但是,正如随后所说的那样,积极的编译器优化(尤其是内联)可以抵消这种膨胀。

3.分配协议要求

由于可以对泛型函数进行专门化处理,因此可以静态分配对传入的泛型参数的方法调用(尽管显然不针对使用动态多态性的类型,例如非最终类)。

但是,协议类型的功能通常无法从中受益,因为它们无法从专业化中受益。因此,将通过协议见证表针对该给定参数动态调度对协议类型参数的方法调用,这将更加昂贵。

尽管这样说,简单的协议类型的函数 可能 可以从内联中受益。在这种情况下,编译器 能够消除的值缓冲器和协议和值证人表(这可以通过检查在一个-
O构建发射的SIL中看到)的开销,从而允许其静态调度的方法一样的方法通用功能。但是,与泛型专业化不同,对于给定的函数,不能保证这种优化(除非您应用@inline(__always)属性-但通常最好让编译器来决定这一点)。

因此,一般而言,就性能而言,泛型函数比协议类型的函数更受青睐,因为它们无需嵌入即可实现方法的静态分配。

4.过载解析

执行重载解析时,编译器将优先使用协议类型的函数,而不是通用函数。

struct Foo : SomeProtocol {
    var someProperty: Int
}

func bar<T : SomeProtocol>(_ some: T) {
    print("generic")
}

func bar(_ some: SomeProtocol) {
    print("protocol-typed")
}

bar(Foo(someProperty: 5)) // protocol-typed

这是因为Swift 比普通的参数更喜欢 显式
类型的参数。

5.通用占位符强制使用相同类型

如前所述,使用通用占位符可以使您强制使用该特定占位符键入的所有参数/返回使用相同类型。

功能:

func generic<T : SomeProtocol>(a: T, b: T) -> T {
    return a.someProperty < b.someProperty ? b : a
}

接受两个参数,并具有 相同 具体类型的返回值,其中该类型符合SomeProtocol

但是功能:

func nongeneric(a: SomeProtocol, b: SomeProtocol) -> SomeProtocol {
    return a.someProperty < b.someProperty ? b : a
}

除了论据外没有任何承诺,回报必须符合SomeProtocol。传递和返回的实际具体类型不一定必须相同。

2020-07-07