通用函数参数和协议类型函数参数之间的实际区别是什么?

What is the in-practice difference between generic and protocol-typed function parameters?

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

protocol SomeProtocol
{
    var someProperty: Int { get }
}

这两个函数在实践中有什么区别(意思不是 "one is generic and the other is not")?它们是否生成不同的代码,是否具有不同的运行时特征?当协议或功能变得重要时,这些差异会改变吗? (因为编译器可能会内联这样的东西)

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

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

我主要问的是编译器的功能差异,我了解两者在语言层面的含义。基本上,nonGeneric 是否意味着代码大小不变但动态调度速度较慢,而 generic 对每种传递的类型使用不断增加的代码大小,但静态调度速度较快?

如果您的 generic 方法有多个涉及 T 的参数,就会有所不同。

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

在上面的方法中,somesomeOther必须是同一类型。它们可以是任何符合 SomeProtocol 的类型,但它们必须是 相同的 类型。

然而,没有泛型:

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

somesomeOther可以是不同的类型,只要符合SomeProtocol.

(我意识到 OP 较少询问语言含义,更多询问编译器的作用——但我觉得也有必要列出通用函数参数和协议类型函数参数之间的一般差异)

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

这是 的结果,因此您不能使用 SomeProtocol 类型参数调用 generic(some:)

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)'

这是因为泛型函数需要一个符合 SomeProtocol 的某种类型 T 的参数——但是 SomeProtocol 而不是 符合 SomeProtocol.

的类型

但是,参数类型为 SomeProtocol 的非泛型函数 接受 foo 作为参数:

nonGeneric(some: foo) // compiles fine

这是因为它接受 'anything that can be typed as a SomeProtocol',而不是 'a specific type that conforms to SomeProtocol'。

2。专业

this fantastic WWDC talk 所述,'existential container' 用于表示协议类型的值。

这个容器包括:

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

  • 指向类型元数据的指针。类型的元数据中包含指向其值见证的指针 table,它管理存在容器中值的生命周期。

  • 一个或(在 protocol composition 的情况下)多个指向给定类型的协议见证 table 的指针。这些 table 跟踪可用于调用给定协议类型实例的协议要求的类型实现。

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

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

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

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

但是,在优化构建中,Swift 能够 专门化 泛型函数的实现——允许编译器为每种类型的泛型占位符生成一个新函数它适用于。这允许参数总是简单地按值传递,但代价是增加了代码大小。然而,正如接下来的谈话所说,积极的编译器优化,尤其是内联,可以抵消这种膨胀。

3。协议要求的调度

由于泛型函数能够被特化,对传入的泛型参数的方法调用能够被静态分派(尽管显然不适用于使用动态多态性的类型,例如非最终 类).

然而,协议类型的函数通常无法从中受益,因为它们无法从专业化中受益。因此,对协议类型参数的方法调用将通过该给定参数的协议见证 table 动态调度,这更昂贵。

尽管如此,简单的协议类型函数可能能够从内联中获益。在这种情况下,编译器 能够消除值缓冲区和协议的开销以及值见证 tables(这可以通过检查在 - O build),允许它以与泛型函数相同的方式静态分派方法。然而,与泛型特化不同的是,对于给定的函数,这种优化并不能得到保证(除非你——但通常最好让编译器决定这一点)。

因此,一般而言,泛型函数在性能方面优于协议类型函数,因为它们可以实现方法的静态分派而无需内联。

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 更喜欢 显式 类型参数而不是通用参数(参见 this Q&A)。

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

如前所述,使用通用占位符可让您强制对使用该特定占位符键入的所有 parameters/returns 使用相同的类型。

函数:

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

有两个参数并且有一个 相同 具体类型的 return,该类型符合 SomeProtocol.

然而函数:

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

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