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

时间:2016-07-18 21:45:32

标签: swift generics swift-protocols

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

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使用每种类型传递的代码大小增加,但是使用快速静态调度?

2 个答案:

答案 0 :(得分:19)

(我意识到OP对语言含义的要求较少,而对编译器的作用更多 - 但我觉得列出泛型和协议类型函数参数之间的一般差异也是值得的)功能

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

这是protocols not conforming to themselves的结果,因此您无法使用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 不是符合{的类型{1}}。

非通用函数,参数类型为SomeProtocol接受SomeProtocol作为参数:

foo

这是因为它接受任何可以键入nonGeneric(some: foo) // compiles fine &#39;而不是符合SomeProtocol&#39;的特定类型的内容。

2。专业化

正如this fantastic WWDC talk所述,一个存在主义容器&#39;用于表示协议类型的值。

此容器包含:

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

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

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

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

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

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

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

但是,在优化版本中,Swift能够专门化泛型函数的实现 - 允许编译器为其应用的每种类型的通用占位符生成新函数。这允许始终简单地通过值传递参数,代价是增加代码大小。然而,正如谈话所说的那样,积极的编译器优化,特别是内联,可以抵消这种膨胀。

3。调度协议要求

由于泛型函数能够是专用的,因此传入的泛型参数的方法调用能够被静态调度(尽管显然不适用于使用动态多态的类型,例如非最终类)。 p>

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

尽管如此,简单的协议类型函数可以能够从内联中受益。在这种情况下,编译器 能够消除值缓冲区和协议和值见证表的开销(这可以通过检查-O构建中发出的SIL来看到),允许它静态地调度方法的方式与泛型函数相同。但是,与通用专业化不同,对于给定的函数不能保证这种优化(除非你apply the @inline(__always) attribute - 但通常最好让编译器决定这一点)。

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

4。重载分辨率

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

SomeProtocol

这是因为Swift更喜欢显式类型参数而非普通参数(参见this Q&A)。

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

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

功能:

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

接受两个参数并返回相同的具体类型,其中该类型符合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 } 。传递和返回的实际具体类型不一定必须相同。

答案 1 :(得分:1)

如果您的if let errorValue = error where errorValue.code == ErrorNotExist { } 方法有多个涉及generic的参数,则会有所不同。

T

在上述方法中,func generic<T: SomeProtocol>(some: T, someOther: T) -> Int { return some.someProperty } some必须是同一类型。它们可以是符合someOther的任何类型,但它们必须是相同的类型。

然而,没有泛型:

SomeProtocol

func nonGeneric(some: SomeProtocol, someOther: SomeProtocol) -> Int { return some.someProperty } some可以是不同的类型,只要它们符合someOther