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

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

问题描述

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

  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 意味着一个不变的代码大小,但动态调度较慢,而泛型使用每种类型的不断增长的代码大小通过,但快速静态调度?

解决方案

(我意识到OP对语言含义和更多关于编译器做了什么 - 但我觉得还应该列出泛型函数参数和协议类型函数参数之间的一般差异)



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



这是不符合自己的协议,因此您不能使用<$ c $调用 generic(some:) c> SomeProtocol 类型参数。

  struct Foo:SomeProtocol {
var someProperty:Int
}

//当然,这里的解决方案是删除多余的'SomeProtocol'类型注释
//并让foo成为Foo类型,但这个问题适用于任何地方
//'符合SomeProtocol'类型变量的任何东西都是必需的。
让foo:SomeProtocol = Foo(someProperty:42)

generic(some:something)//编译器错误:无法用参数列表调用'generic'
//类型'(some:SomeProtocol)'

这是因为泛型函数需要某种类型的参数 T 符合 SomeProtocol - 但 SomeProtocol 不是符合 SomeProtocol 的类型。



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

  nonGeneric(some:foo)//编译罚款

这是因为它接受'任何可以键入为 SomeProtocol '的东西,而不是'符合 SomeProtocol '。



2。专业化



这篇很棒的WWDC谈话,一个'存在性容器'用于表示一个协议类型的值。



这个容器包含:




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

  • p>指向类型元数据的指针。包含在类型的元数据中的是一个指向它的价值见证表的指针,它管理着存储容器中的值的生命周期。 一个或(在协议组合)指向给定类型的协议见证表的多个指针。这些表记录了可用于协议类型实例的协议要求的类型实现。


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


  • 存储参数然后传递给参数。

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


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



  • 然而,在经过优化的版本中,Swift能够 >通用函数的实现 - 允许编译器为其应用的每种类型的通用占位符生成一个新函数。这允许参数始终以值为单位进行传递,代价是增加代码大小。然而,随着谈话的进行,编译器优化,特别是内联,可以抵消这种膨胀。

    3。协议要求的发送

    由于泛型函数能够被专门化,所以传入的泛型参数的方法调用能够被静态调度(尽管显然不适用于使用动态多态的类型,比如非final类)。

    协议类型函数通常不能从中受益,因为它们不能从专业化中受益。因此,对协议类型参数的方法调用将通过协议见证表动态地分配给定的参数,这是比较昂贵的。尽管如此,简单的协议 可以从内联中受益。在这种情况下,编译器能够消除值缓冲区和协议和值见证表的开销(这可以通过检查-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))//协议类型

    这是因为Swift倾向于使用明确的类型化参数而不是普通类型的参数(请参阅本Q& A )。



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



    函数:

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

    有两个参数,并且返回 具体类型,其类型符合 SomeProtocol



    但是函数:

      func nongeneric(a:SomeProtocol,b:SomeProtocol) - > SomeProtocol {
    返回a.someProperty< b.some楼盘? b:a
    }

    除参数外不带任何承诺,返回必须符合 SomeProtocol 。传递并返回的实际具体类型不一定必须相同。


    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
    }
    

    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?

    解决方案

    (I realise that OP is asking less about the language implications and more about what the compiler does – but I feel it's also worthwhile also to list the general differences between generic and protocol-typed function parameters)

    1. A generic placeholder constrained by a protocol must be satisfied with a concrete type

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

    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.

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

    nonGeneric(some: foo) // compiles fine
    

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

    2. Specialisation

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

    This container consists of:

    • 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.

    • 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.

    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.

    3. Dispatch of protocol requirements

    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.

    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.

    4. Overload resolution

    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
    

    This is because Swift favours an explicitly typed parameter over a generic one (see this Q&A).

    5. Generic placeholders enforce the same type

    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.

    The function:

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

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

    However the function:

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

    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天全站免登陆