协议不符合自身?

153
为什么这段 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()

编译器提示:"类型 P 不符合协议 P"(或者在 Swift 的后续版本中,"不支持将 'P' 用作符合协议 'P' 的具体类型。")。
为什么会这样呢?这感觉像是语言中的一个漏洞。我知道,问题源自将数组 arr 声明为协议类型的数组,但这样做是否合理呢?我认为协议正是为了帮助结构体提供像类型层次结构一样的东西。

2
当您在“let arr”行中删除类型注释时,编译器会将类型推断为[S],代码可以编译。看起来协议类型不能像类-超类关系那样使用。 - vadian
2
@vadian正确,这就是我在我的问题中所指的当我说“我意识到问题源于将数组arr声明为协议类型的数组”时。但是,正如我在问题中所说的,协议的整个要点通常是它们可以像类 - 超类关系一样使用!它们旨在为结构体的世界提供一种分层结构。而且它们通常确实做到了。问题是,为什么这里不起作用? - matt
1
仍然无法在Xcode 7.1中工作,但错误信息现在是“使用'P'作为符合协议'P'的具体类型不受支持”。 - Martin R
1
@MartinR 这是一个更好的错误信息。但对我来说,它仍然感觉像语言中的一个漏洞。 - matt
@MartinR 那是我尝试的第一件事 :) - matt
显示剩余3条评论
3个回答

153

为什么协议不能符合自身?

一般情况下,允许协议符合自身是不可靠的。问题在于静态协议要求。

这些要求包括:

  • 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要求并没有太大不同,后者(目前)防止您将协议用作类型。在具有静态要求且防止您使用符合自身的协议作为类型的限制可能是未来语言版本的选择。编辑:正如下面所探讨的那样,这似乎是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。尽管在许多情况下,这并不是理想的解决方法,因为它强制使您的符合类型成为类,并要求使用Obj-C运行时,因此在诸如Linux等非苹果平台上不可行。
但我怀疑这个限制是语言已经实现“没有静态要求的协议符合自身”的(其中之一)主要原因,用于@objc协议。编写围绕它们的通用代码可以由编译器显着简化。
为什么?因为@objc协议类型的值实际上只是类引用,其要求使用objc_msgSend进行调度。另一方面,非@objc协议类型的值更加复杂,因为它们携带值和见证表,以便管理其(可能间接存储的)包装值的内存,并确定为不同要求调用哪些实现。
由于对@objc协议的简化表示,该协议类型P的值可以与某些通用占位符T:P类型的“通用值”共享相同的内存表示,这使得Swift团队可以轻松地允许自我一致性。然而,对于非@objc协议来说,并不是如此,因为这样的通用值目前不包含值或协议见证表。
然而,此功能是有意的,并希望将其推广到非@objc协议上,正如Swift团队成员Slava Pestov在SR-55的评论中(由此问题引起)确认的那样。

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 的具体类型元素的数组上调用它。我们可以通过定义一个额外的扩展来解决这个问题,当 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 之前,最通用的方法是,就像 Rob 在他的答案中所示 的那样,简单地为 [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类型值所包装的底层具体类型并替换它,那会怎么样?不幸的是,这需要一种称为打开存在类型的语言特性,目前用户无法直接使用。
然而,当访问它们的成员时(即挖掘出运行时类型并以通用占位符的形式使其可访问),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)满足。请注意,这不是“协议符合自身”,因为我们替换的是具体类型而不是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值,则无法保证它们在编译时具有相同的基础具体类型。
为了解决这个问题,我们可以使用类型擦除器。
Rob所说类型擦除器是协议不符合自身问题的最通用解决方案。它们允许我们将协议类型的实例包装在符合该协议的具体类型中,通过将实例要求转发到底层实例来实现。
因此,让我们构建一个类型擦除盒子,将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的具体类型。

4
也许我太蠢了,但我不理解为什么静态情况很特殊。我们(编译器)在编译时对协议的静态属性和实例属性的了解程度是一样的,即采纳者将会实现它,没有任何区别吧? - matt
2
@matt 一个协议类型的实例(即包装在存在类型“P”中的具体类型实例)是可以的,因为我们可以将调用实例要求的操作转发到底层实例。然而,对于协议类型本身(即“P.Protocol”,字面上只是描述协议的类型)- 没有采纳者,因此没有任何东西可以调用静态要求,这就是为什么在上面的示例中我们不能有SomeGeneric<P>(对于P.Type(存在元类型),它描述符合P的某个具体元类型 - 但那是另一回事)。 - Hamish
2
好的,我的示例中的协议没有静态要求。(实际上它根本没有任何要求。)但编译器仍然无法接受它。 - matt
1
@matt 这正是Jordan Rose在此错误报告中提到的行为原因:“总体而言,这是正确的:并不是所有协议都符合自身。最简单的情况是具有初始化器要求的协议:你将构造哪种具体类型? - Hamish
2
我真的不关心 soundness 等等,我只想编写应用程序,如果感觉应该可以工作,那么它就应该可以。语言只应该是一种工具,而不是产品本身。如果有一些情况确实无法使用,那么在这些情况下禁止使用,但让其他人使用它可以工作的情况,并让他们继续编写应用程序。 - Jonathan.
显示剩余10条评论

69

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

"使用'P'作为符合协议'P'的具体类型是不支持的。"

实际上,这使得整个问题更加清晰。这个扩展:

extension Array where Element : P {

Element == P 时不适用,因为 P 不被视为 P 的具体一致性。(下面的“将其放入一个盒子中”的解决方案仍然是最通用的解决方案。)

旧回答:

这又是一种元类型的情况。Swift 非常希望您对大多数非平凡事物取得具体类型。 [P] 不是一个具体类型(您不能为 P 分配一个已知大小的内存块)。 (我认为这并不真实;你完全可以创建一个大小为 P 的东西,因为它通过间接方式完成。)我不认为有任何证据表明这是一个"不应该"工作的案例。这看起来非常像他们“尚未工作”的案例之一。(不幸的是,几乎不可能让 Apple 确认这些案例之间的差异。)Array<P> 可以是一个变量类型(其中 Array 不能),这表明他们已经在这个方向上做了一些工作,但 Swift 元类型有很多锋利的边缘和未实现的案例。我不认为您会得到比这更好的“为什么”答案。"因为编译器不允许它。"(不令人满意,我知道。我的整个 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”来更自动化地执行相同的操作。

这个答案中有很多有用的信息,但是Tomohiro的答案中的实际解决方案比这里提出的装箱解决方案更好。 - jsadler
1
@jsadler 这个问题不是如何绕过限制,而是为什么存在这个限制。事实上,就解释而言,Tomohiro的解决方法引发了更多问题。如果我们在我的数组示例中使用==,会出现错误,即“同类型要求使通用参数'Element'非通用”。为什么Tomohiro使用==不会生成相同的错误? - matt
1
@Rob Napier,您的回复仍然让我感到困惑。相较于原始方案,Swift 如何在您的解决方案中看到更多的具体性?您似乎只是将事物包装在一个结构体中...我不知道,也许我很难理解 Swift 类型系统,但这一切似乎都像魔法般神秘。 - AyBayBay
@AyBayBay 更新了答案。 - Rob Napier
非常感谢@RobNapier,您的回复速度总是让我惊叹,而且坦白地说,您如何找到时间帮助别人的程度也让我惊叹。尽管如此,您的新编辑确实让我有了更清晰的认识。还有一件事我想指出,理解类型擦除也对我有所帮助。特别是这篇文章做得非常好:https://krakendev.io/blog/generic-protocols-and-their-shortcomings老实说,我不知道如何评价其中的一些内容。似乎我们正在考虑语言中的漏洞,但我不知道苹果公司会如何构建其中的一些内容。 - AyBayBay
值得注意的是,这里确实有一些情况是“不应该起作用”的,但它们主要与静态要求(包括初始化器)相关,正如我在下面的回答中所说。但是,在这种特殊情况下,让 P 符合自身应该是安全的 - 尽管可以提出这样的论点,即打破规则可能会导致令人惊讶的不一致性 (尽管这与当前 associatedtype 要求的情况并没有太大区别)。 - Hamish

19

如果您扩展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()

4
我认为收集 vs 数组在这里并不相关,重要的变化是使用 == P vs : P。使用==时,原始示例也能正常工作。使用==的一个潜在问题(取决于上下文)是它排除了子协议:如果我创建了一个 protocol SubP: P,然后将 arr 定义为 [SubP],那么 arr.test() 将不再起作用(错误:SubP 和 P 必须相等)。 - imre

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接