泛型函数参数和协议类型函数参数之间的实际区别是什么? [英] What is the in-practice difference between generic and protocol-typed function parameters?

查看:45
本文介绍了泛型函数参数和协议类型函数参数之间的实际区别是什么?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

Given a protocol without any associated types:

protocol SomeProtocol
{
    var someProperty: Int { get }
}

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

What is the difference between these two functions, in practice (meaning not "one is generic and the other is not")? Do they generate different code, do they have different runtime characteristics? Do these differences change when the protocol or functions become non-trivial? (since a compiler could probably inline something like this)

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

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

我主要是在询问编译器所做工作的差异,我了解两者在语言层面的含义.基本上,nonGeneric 是否意味着代码大小恒定但动态调度较慢,而 generic 使用的每个传递的类型的代码大小不断增长,但静态调度速度快?

I'm mostly asking about differences in what the compiler does, I understand the language-level implications of both. Basically, does nonGeneric imply a constant code size but slower dynamic dispatch, vs. generic using a growing code size per type passed, but with fast static dispatch?

推荐答案

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

这是协议不符合自己的结果,因此您不能调用generic(some:) 带有 SomeProtocol 类型参数.

This is a consequence of protocols not conforming to themselves, therefore you cannot call generic(some:) with a SomeProtocol typed argument.

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 参数——但 SomeProtocolnot 一个符合 SomeProtocol 的类型.

This is because the generic function expects an argument of some type T that conforms to SomeProtocol – but SomeProtocol is not a type that conforms to SomeProtocol.

然而,一个非泛型函数,参数类型为 SomeProtocol接受 foo 作为参数:

A non-generic function however, with a parameter type of SomeProtocol, will accept foo as an argument:

nonGeneric(some: foo) // compiles fine

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

This is because it accepts 'anything that can be typed as a SomeProtocol', rather than 'a specific type that conforms to SomeProtocol'.

这个精彩的 WWDC 演讲 中所述,存在的容器"用于表示协议类型的值.

As covered in this fantastic WWDC talk, an 'existential container' is used in order to represent a protocol-typed value.

这个容器包括:

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

  • A value buffer to store the value itself, which is 3 words in length. Values larger than this will be heap allocated, and a reference to the value will be stored in the value buffer (as a reference is just 1 word in size).

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

A pointer to the type's metadata. Included in the type's metadata is a pointer to its value witness table, which manages the lifetime of value in the existential container.

一个或(在 协议组合) 多个指向给定类型的协议见证表的指针.这些表跟踪可用于调用给定协议类型实例的协议要求的类型实现.

One or (in the case of protocol composition) multiple pointers to protocol witness tables for the given type. These tables keep track of the type's implementation of the protocol requirements available to call on the given protocol-typed instance.

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

By default, a similar structure is used in order to pass a value into a generic placeholder typed argument.

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

  • The argument is stored in a 3 word value buffer (which may heap allocate), which is then passed to the parameter.

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

For each generic placeholder, the function takes a metadata pointer parameter. The metatype of the type that's used to satisfy the placeholder is passed to this parameter when calling.

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

For each protocol constraint on a given placeholder, the function takes a protocol witness table pointer parameter.

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

However, in optimised builds, Swift is able to specialise the implementations of generic functions – allowing the compiler to generate a new function for each type of generic placeholder that it's applied with. This allows for arguments to always be simply passed by value, at the cost of increasing code size. However, as the talk then goes onto say, aggressive compiler optimisations, particularly inlining, can counteract this bloat.

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

Because of the fact that generic functions are able to be specialised, method calls on generic arguments passed in are able to be statically dispatched (although obviously not for types that use dynamic polymorphism, such as non-final classes).

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

Protocol-typed functions however generally cannot benefit from this, as they don't benefit from specialisation. Therefore method calls on a protocol-typed argument will be dynamically dispatched via the protocol witness table for that given argument, which is more expensive.

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

Although that being said, simple protocol-typed functions may be able to benefit from inlining. In such cases, the compiler is able to eliminate the overhead of the value buffer and protocol and value witness tables (this can be seen by examining the SIL emitted in a -O build), allowing it to statically dispatch methods in the same way as generic functions. However, unlike generic specialisation, this optimisation is not guaranteed for a given function (unless you apply the @inline(__always) attribute – but usually it's best to let the compiler decide this).

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

Therefore in general, generic functions are favoured over protocol-typed functions in terms of performance, as they can achieve static dispatch of methods without having to be inlined.

在执行重载决议时,编译器将优先使用协议类型函数而不是泛型函数.

When performing overload resolution, the compiler will favour the protocol-typed function over the generic one.

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 is because Swift favours an explicitly typed parameter over a generic one (see this Q&A).

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

As already said, using a generic placeholder allows you to enforce that the same type is used for all parameters/returns that are typed with that particular placeholder.

功能:

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

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

takes two arguments and has a return of the same concrete type, where that type conforms to SomeProtocol.

然而功能:

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

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

carries no promises other than the arguments and return must conform to SomeProtocol. The actual concrete types that are passed and returned do not necessarily have to be the same.

这篇关于泛型函数参数和协议类型函数参数之间的实际区别是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆