协议不符合自身?

Protocol doesn't conform to itself?

为什么这个 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 does not conform to protocol P"(或者,在 Swift 的更高版本中,"Using 'P' as a concrete type conforming to protocol 'P' is not supported.")。

为什么不呢?不知何故,这感觉就像语言中的一个洞。我意识到问题源于将数组 arr 声明为协议类型 的数组 ,但这是一件不合理的事情吗?我认为协议的存在正是为了帮助提供类似于类型层次结构的结构?

编辑:与 Swift 一起工作了 18 个月,这是另一个主要版本(提供了新的诊断),@AyBayBay 的评论让我想重写这个答案。新的诊断是:

"Using 'P' as a concrete type conforming to protocol 'P' is not supported."

这实际上使整个事情变得更加清晰。此扩展程序:

extension Array where Element : P {

Element == P 时不适用,因为 P 不被视为 P 的具体一致性。 (下面的"put it in a box"解决方案仍然是最通用的解决方案。)


旧答案:

这是元类型的另一种情况。 Swift 真的 希望您为大多数重要的事情找到一个具体的类型。 [P] 不是具体类型(您不能为 P 分配已知大小的内存块)。(我认为这不是真的;您绝对可以创建大小为 P 的东西,因为 it's done via indirection。)我认为没有任何证据表明这是 "shouldn't" 工作的案例。这看起来非常像他们的 "doesn't work yet" 案例之一。 (不幸的是,几乎不可能让 Apple 确认这些情况之间的区别。)Array<P> 可以是变量类型(而 Array 不能)这一事实表明他们已经在这方面做了一些工作方向,但 Swift 元类型有很多尖锐的边缘和未实现的案例。我认为您不会得到比这更好的 "why" 答案。 "Because the compiler doesn't allow it."(不满意,我知道。我的整个 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(),因为 PElement)不是具体类型,因此无法实例化。它 必须 在具有具体类型元素的数组上调用,其中该类型符合 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()barno 实现)。尽管我们可以在 P 扩展中定义这些要求的实现,但这些只是为符合 P 的具体类型定义的——您仍然不能在 P 本身上调用它们。

因此,Swift 完全不允许我们将协议用作符合自身的类型——因为当该协议有静态要求时,它没有。

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

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

编辑: 如下所述,这看起来确实像 Swift 团队的目标。


@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。当然,在许多情况下这不是一个理想的解决方法,因为它强制您的符合类型为 classes,并且需要 Obj-C 运行时,因此不能使其在非 Apple 平台上可行,例如Linux.

但我怀疑这个限制是(其中一个)语言已经为 @objc 协议实现 'protocol without static requirements conforms to itself' 的主要原因。编译器可以显着简化围绕它们编写的通用代码。

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

由于 @objc 协议的这种简化表示,此类协议类型 P 的值可以与某些通用占位符 'generic value' 类型共享相同的内存表示 T : P 大概 让 Swift 团队很容易允许自我一致性。对于非 @objc 协议,情况并非如此,但是因为此类通用值当前不携带值或协议见证表。

然而,此功能 有意为之,并有望推广到非 @objc 协议,Swift 团队成员 Slava Pestov in the comments of SR-55 in response to your query about it (prompted by this question):

Matt Neuburg added a comment - 7 Sep 2017 1:33 PM

This does compile:

@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) }

Adding @objc makes it compile; removing it makes it not compile again. Some of us over on Stack Overflow find this surprising and would like to know whether that's deliberate or a buggy edge-case.

Slava Pestov added a comment - 7 Sep 2017 1:53 PM

It's deliberate – lifting this restriction is what this bug is about. Like I said it's tricky and we don't have any concrete plans yet.

所以希望有一天语言也能支持非@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 的具体类型元素的数组上调用它。我们可以通过为 when 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()

但是值得注意的是,这会将数组执行 O(n) 转换为 [P],因为每个元素都必须装在一个存在的容器中。如果性能是一个问题,您可以通过重新实现扩展方法来简单地解决这个问题。这不是一个 完全 令人满意的解决方案——希望该语言的未来版本将包含一种表达 'protocol type or conforms to protocol type' 约束的方法。

在 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 类型值所包装的底层具体类型,然后用它代替,会怎样?遗憾的是,这需要一种名为 opening existentials 的语言功能,目前用户无法直接使用该功能。

然而,Swift 确实在访问它们的成员时隐式打开存在性(协议类型值)(即它挖掘出运行时类型并使其可访问通用占位符的形式)。我们可以在 P:

上的协议扩展中利用这一事实
extension P {
  func callTakesConcreteP/*<Self : P>*/(/*self: Self*/) {
    takesConcreteP(self)
  }
}

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

满足 T

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

p.callTakesConcreteP()

并且 takesConcreteP(_:) 被调用,其通用占位符 T 被底层具体类型满足(在本例中为 S)。请注意,这不是 "protocols conforming to themselves",因为我们正在替换具体类型而不是 P – 尝试向协议添加静态要求并查看从 [=72= 中调用它时会发生什么].

如果 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。构建类型橡皮擦

作为, a type eraser,是解决协议不符合自身问题的最通用的解决方案。它们允许我们通过将实例要求转发给底层实例,将协议类型的实例包装在符合该协议的具体类型中。

因此,让我们构建一个类型擦除盒,将 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.[=109= 中有意义地表达这一点]

因此,这种情况下的解决方案只有在instance协议要求的情况下才真正有用。一般情况下,我们还是不能把P当作一个符合P.

的具体类型