泛型和协议类型函数参数之间的实际差异是什么?

JHZ*_*JHZ 17 generics swift swift-protocols

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

protocol SomeProtocol
{
    var someProperty: Int { get }
}
Run Code Online (Sandbox Code Playgroud)

这两个函数在实践中有什么区别(意思不是"一个是通用的而另一个不是")?它们是否生成不同的代码,它们具有不同的运行时特征吗?当一个或多个协议变得非常重要时,这些差异是否会发生变化?(因为编译器可能内联这样的东西)

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

func nonGeneric(some: SomeProtocol) -> Int
{
    return some.someProperty
}
Run Code Online (Sandbox Code Playgroud)

我主要询问编译器的不同之处,我理解两者的语言级含义.基本上,确实nonGeneric意味着一个恒定的代码大小但速度较慢的动态调度,而不是generic每个类型传递的代码大小增加,但是使用快速静态调度?

Ham*_*ish 22

(我意识到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)'
Run Code Online (Sandbox Code Playgroud)

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

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

nonGeneric(some: foo) // compiles fine
Run Code Online (Sandbox Code Playgroud)

这是因为它接受'任何可以输入'的东西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
Run Code Online (Sandbox Code Playgroud)

这是因为Swift更喜欢显式类型参数而不是通用类型参数(参见此问答).

5.通用占位符强制执行相同的类型

如前所述,使用通用占位符允许您强制将相同类型用于使用该特定占位符键入的所有参数/返回.

功能:

func generic<T : SomeProtocol>(a: T, b: T) -> T {
    return a.someProperty < b.someProperty ? b : a
}
Run Code Online (Sandbox Code Playgroud)

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

但功能:

func nongeneric(a: SomeProtocol, b: SomeProtocol) -> SomeProtocol {
    return a.someProperty < b.someProperty ? b : a
}
Run Code Online (Sandbox Code Playgroud)

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