协议本身不符合吗?

为什么这个 Swift 代码无法编译?

protocol P { }
struct S: P { }


let arr:[P] = [ S() ]


extension Array where Element : P {
func test<T>() -> [T] {
return []
}
}


let result : [S] = arr.test()

编译器说: “ Type P不符合协议 P”(或者,在 Swift 的后续版本中,“不支持使用‘ P’作为符合协议‘ P’的具体类型。”).

为什么不呢?不知怎么的,这感觉像是语言的漏洞。我意识到这个问题源于将数组 arr声明为数组 协议类型,但是这样做是不是不合理呢?我以为协议是用来帮助提供类似类型层次结构的结构的?

23242 次浏览

编辑: W/Swift,另一个主要版本(提供了一个新的诊断) ,还有来自@AyBayBay 的评论让我想重写这个答案。新的诊断方法是:

“不支持将‘ P’用作符合协议‘ P’的具体类型。”

这样一来,整个事情就清楚多了。这个扩展:

extension Array where Element : P {

不适用于 Element == P,因为 P不被认为是 P的具体一致性。(下面的“把它放在盒子里”解决方案仍然是最普遍的解决方案。)


老答案:

这是另一个关于变型的案例。Swift 真的希望您为大多数非琐碎的事情建立一个具体的类型。[P]不是一个具体的类型(您不能为 P分配已知大小的内存块)。 (我不认为这是真的; 您完全可以创建大小为 P的东西,因为 是通过间接方式完成的。)我不认为有任何证据表明这是一个“不应该”的工作。这看起来很像他们的一个“还没起作用”的案子。(不幸的是,几乎不可能让苹果确认这两种情况之间的区别。)事实上,Array<P>可以是一个可变类型(而 Array不能) ,这表明他们已经在这个方向上做了一些工作,但 Swift 元类型有很多尖锐的边缘和未实现的情况。我不认为你会得到一个比这更好的“为什么”的答案。“因为编译器不允许”(不满意,我知道。我的整个斯威夫特生活...)

解决办法几乎总是把东西放在一个盒子里。

protocol P { }
struct S: P { }


struct AnyPArray {
var array: [P]
init(_ array:[P]) { self.array = array }
}


extension AnyPArray {
func test<T>() -> [T] {
return []
}
}


let arr = AnyPArray([S()])
let result: [S] = arr.test()

当 Swift 允许你直接做这件事(我希望最终能做到) ,它可能只是通过为你自动创建这个盒子。递归枚举恰好有这样的历史。你不得不把它们装箱,这非常烦人,而且有限制性,最后编译器添加了 indirect来更自动地做同样的事情。

如果将 CollectionType协议而不是 Array和约束扩展为具体的类型,则可以按以下方式重写前面的代码。

protocol P { }
struct S: P { }


let arr:[P] = [ S() ]


extension CollectionType where Generator.Element == P {
func test<T>() -> [T] {
return []
}
}


let result : [S] = arr.test()

为什么协议不能自我调节呢?

在一般情况下,允许协议遵从自身是不合理的,问题在于静态协议需求。

这些措施包括:

  • static方法和属性
  • 初始化程序
  • 关联类型(尽管这些类型目前阻止将协议作为实际类型使用)

我们可以在通用占位符 T上访问这些需求,其中 T : P-然而,我们 不能在协议类型本身上访问这些需求,因为没有具体的一致性类型可以转发。因此,我们不能让 T成为 P

考虑一下如果我们允许 Array扩展适用于 [P],在下面的例子中会发生什么:

protocol P {
init()
}


struct S  : P {}
struct S1 : P {}


extension Array where Element : P {
mutating func appendNew() {
// If Element is P, we cannot possibly construct a new instance of it, as you cannot
// construct an instance of a protocol.
append(Element())
}
}


var arr: [P] = [S(), S1()]


// error: Using 'P' as a concrete type conforming to protocol 'P' is not supported
arr.appendNew()

我们不可能在 [P]上调用 appendNew(),因为 P(Element)不是具体的类型,因此不能被实例化。它对具有具体类型元素的数组调用 必须的,其中该类型符合 P

这与静态方法和属性需求类似:

protocol P {
static func foo()
static var bar: Int { get }
}


struct SomeGeneric<T : P> {


func baz() {
// If T is P, what's the value of bar? There isn't one – because there's no
// implementation of bar's getter defined on P itself.
print(T.bar)


T.foo() // If T is P, what method are we calling here?
}
}


// error: Using 'P' as a concrete type conforming to protocol 'P' is not supported
SomeGeneric<P>().baz()

我们不能谈论 SomeGeneric<P>。我们需要静态协议需求的具体实现(请注意上面示例中定义的 foo()bar没有实现)。虽然我们可以在 P扩展中定义这些需求的实现,但是这些实现只针对符合 P的具体类型定义——您仍然不能在 P本身上调用它们。

正因为如此,Swift 完全不允许我们将协议作为一种符合其自身的类型来使用——因为当该协议具有静态需求时,它就不具有静态需求。

实例协议需求没有问题,因为您在符合协议的实际实例上调用它们(因此必须已经实现了需求)。因此,当对类型为 P的实例调用需求时,我们只需将该调用转发到底层具体类型对该需求的实现。

然而,在这种情况下,为规则制定特殊的异常可能会导致通用代码处理协议的方式出现令人惊讶的不一致。尽管如此,这种情况与 associatedtype需求并没有太大的不同—— associatedtype需求(目前)阻止您使用协议作为类型。当协议具有静态需求时,有一个限制可以阻止您将协议用作符合其自身的类型,这可能是该语言未来版本的一个选项

编辑: 正如下面所探讨的,这看起来确实像斯威夫特团队的目标。


@objc协议

事实上,这就是 没错语言对待 @objc协议的方式。当他们没有静态需求时,他们就顺应了自己。

以下内容汇编得很好:

import Foundation


@objc protocol P {
func foo()
}


class C : P {
func foo() {
print("C's foo called!")
}
}


func baz<T : P>(_ t: T) {
t.foo()
}


let c: P = C()
baz(c)

baz要求 T符合 P; 但是我们可以用 P代替 T,因为 P没有静态要求。如果我们向 P添加一个静态需求,示例就不再编译:

import Foundation


@objc protocol P {
static func bar()
func foo()
}


class C : P {


static func bar() {
print("C's bar called")
}


func foo() {
print("C's foo called!")
}
}


func baz<T : P>(_ t: T) {
t.foo()
}


let c: P = C()
baz(c) // error: Cannot invoke 'baz' with an argument list of type '(P)'

因此,解决这个问题的一个方法是将协议 @objc。当然,在许多情况下,这并不是一个理想的解决方案,因为它强制您的一致类型为类,并且需要 Obj-C 运行时,因此不能在非苹果平台(如 Linux)上使用。

但是我怀疑这个限制是为什么语言已经为 @objc协议实现了“没有静态需求的协议符合它自己”的主要原因之一。编译器可以显著简化围绕它们编写的泛型代码。

为什么?因为 @objc协议类型的值实际上只是类引用,其需求是使用 objc_msgSend分派的。另一方面,非 @objc协议类型的值更加复杂,因为它们同时携带值表和见证表,以便管理它们(可能间接存储的)封装值的内存,并分别确定需要调用哪些实现来满足不同的需求。

由于 @objc协议的这种简化表示,这种协议类型的值 P可以与某种通用占位符 T : P大概吧类型的“通用值”共享相同的内存表示,这使得 Swift 团队很容易允许自我一致性。然而,对于非 @objc协议,情况并非如此,因为这样的通用值目前并不包含值或协议见证表。

然而,这个特性 是有意的,并希望推出到非 @objc协议,正如斯威夫特团队成员斯拉瓦 Pestov 在 SR-55的评论中在回应您的询问(由 这个问题提示) :

Matt Neuburg 发表评论——2017年9月7日下午1:33

这确实编译了:

@objc protocol P {}
class C: P {}


func process<T: P>(item: T) -> T { return item }
func f(image: P) { let processed: P = process(item:image) }

添加 @objc使它可以编译; 删除它使它不能再次编译。 我们中的一些人在堆栈溢出发现这个惊喜,并希望 看看这是故意的还是有问题的。

斯拉瓦 · 佩斯托夫(Slava Pestov)发表评论——2017年9月7日下午1:53

这是故意的——解除这个限制就是这个 bug 的目的。 就像我说的,这很棘手,我们还没有任何具体的计划。

因此,希望有一天这种语言也能支持非 @objc协议。

但是,对于非 @objc协议,目前有哪些解决方案?


使用协议约束实现扩展

在 Swift 3.1中,如果您希望扩展具有给定的通用占位符或关联类型必须是给定的协议类型(而不仅仅是符合该协议的具体类型)的约束,那么您可以简单地使用 ==约束来定义它。

例如,我们可以将数组扩展名写为:

extension Array where Element == P {
func test<T>() -> [T] {
return []
}
}


let arr: [P] = [S()]
let result: [S] = arr.test()

当然,现在这样可以防止我们在具有符合 P的具体类型元素的数组上调用它。我们可以解决这个问题,只需定义一个额外的扩展来表示 Element : P的时间,然后转发到 == P的扩展:

extension Array where Element : P {
func test<T>() -> [T] {
return (self as [P]).test()
}
}


let arr = [S()]
let result: [S] = arr.test()

但是值得注意的是,这将执行数组到 [P]的 O (n)转换,因为每个元素都必须装箱在一个存在容器中。如果性能是一个问题,您可以通过重新实现扩展方法来简单地解决这个问题。这不是一个令人满意的 完全相信解决方案-希望该语言的未来版本将包括一种表示“协议类型 或者符合协议类型”约束的方法。

在 Swift 3.1之前,实现这一点的最一般方法是为 [P]构建一个包装器类型,然后在该类型上定义扩展方法。


将协议类型的实例传递给受约束的泛型占位符

考虑下面的情况(人为的,但并不罕见) :

protocol P {
var bar: Int { get set }
func foo(str: String)
}


struct S : P {
var bar: Int
func foo(str: String) {/* ... */}
}


func takesConcreteP<T : P>(_ t: T) {/* ... */}


let p: P = S(bar: 5)


// error: Cannot invoke 'takesConcreteP' with an argument list of type '(P)'
takesConcreteP(p)

我们无法将 p传递给 takesConcreteP(_:),因为我们目前无法用 P替代通用占位符 T : P。让我们看看解决这个问题的几种方法。

1. 开启存在主义

与其尝试用 P代替 T : P,不如我们深入研究一下 P类型值包装的底层具体类型,然后用它来代替 T : P?不幸的是,这需要一个名为 开场存在主义的语言特性,目前用户还不能直接使用这个特性。

然而,Swift 是的在访问存在项(协议类型的值)上的成员时隐式地打开存在项(即它挖掘出运行时类型,并以通用占位符的形式使其可访问)。我们可以在 P的协议扩展中利用这个事实:

extension P {
func callTakesConcreteP/*<Self : P>*/(/*self: Self*/) {
takesConcreteP(self)
}
}

请注意,扩展方法使用的是隐式通用 Self占位符,用于键入隐式 self参数——这在所有协议扩展成员的后台发生。当对协议类型的值 P调用这样的方法时,Swift 挖掘出底层的具体类型,并使用它来满足 Self通用占位符。这就是为什么我们能够调用 takesConcreteP(_:)self-我们满足 TSelf

这意味着我们现在可以说:

p.callTakesConcreteP()

调用 takesConcreteP(_:)时,其通用占位符 T由底层具体类型(在本例中为 S)满足。请注意,这并不是“协议自身一致”,因为我们正在替换一个具体的类型而不是 P-尝试向协议中添加一个静态需求,然后看看在 takesConcreteP(_:)中调用它时会发生什么。

如果 Swift 继续不允许协议符合自身,那么下一个最好的选择将是在试图将存在项作为参数传递给泛型类型的参数时隐式地打开存在项——有效地做我们的协议扩展蹦床所做的事情,只是不使用样板。

然而,请注意,打开存在主义并不是解决协议不一致问题的通用方案。它不处理协议类型值的异构集合,这些值可能都有不同的底层具体类型。例如,考虑:

struct Q : P {
var bar: Int
func foo(str: String) {}
}


// The placeholder `T` must be satisfied by a single type
func takesConcreteArrayOfP<T : P>(_ t: [T]) {}


// ...but an array of `P` could have elements of different underlying concrete types.
let array: [P] = [S(bar: 1), Q(bar: 2)]


// So there's no sensible concrete type we can substitute for `T`.
takesConcreteArrayOfP(array)

出于同样的原因,带有多个 T参数的函数也会出现问题,因为参数必须带有相同类型的参数——然而,如果我们有两个 P值,我们无法保证在编译时它们都具有相同的底层具体类型。

为了解决这个问题,我们可以使用类型橡皮擦。

2. 制作一个橡皮擦

作为 罗伯说类型橡皮擦类型橡皮擦,是解决协议不一致问题的最通用的解决方案。它们允许我们通过将实例需求转发到底层实例,将协议类型的实例包装在符合该协议的具体类型中。

因此,让我们构建一个类型擦除框,将 P的实例需求转发到一个符合 P的底层任意实例上:

struct AnyP : P {


private var base: P


init(_ base: P) {
self.base = base
}


var bar: Int {
get { return base.bar }
set { base.bar = newValue }
}


func foo(str: String) { base.foo(str: str) }
}

现在我们可以用 AnyP来代替 P:

let p = AnyP(S(bar: 5))
takesConcreteP(p)


// example from #1...
let array = [AnyP(S(bar: 1)), AnyP(Q(bar: 2))]
takesConcreteArrayOfP(array)

现在,想想我们为什么要造那个盒子。正如我们前面所讨论的,Swift 需要一个具体的类型来处理协议具有静态需求的情况。考虑一下 P是否有一个静态需求-我们需要在 AnyP中实现它。但是,它应该作为什么来实施呢?我们在这里处理的是符合 P的任意实例——我们不知道它们的底层具体类型是如何实现静态需求的,因此我们不能在 AnyP中有意义地表达这一点。

因此,这种情况下的解决方案只有在 例子协议需求的情况下才真正有用。在一般情况下,我们仍然不能将 P视为符合 P的具体类型。