协议不符合自身?

Protocol doesn't conform to itself?

提问人:matt 提问时间:10/14/2015 最后编辑:Vadim Kotovmatt 更新时间:1/14/2023 访问量:26885

问:

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

编译器说:“类型不符合协议”(或者,在更高版本的 Swift 中,“不支持使用'P'作为符合协议'P'的具体类型。PP

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

泛型 SWIFT 协议

评论

2赞 vadian 10/23/2015
删除行中的类型批注时,编译器会将类型推断为,然后编译代码。看起来协议类型的使用方式与类 - 超类关系不同。let arr[S]
2赞 matt 10/23/2015
@vadian 没错,这就是我在问题中所说的“我意识到问题源于将数组 arr 声明为协议类型的数组”时所指的。但是,正如我在问题中继续说的那样,协议的全部意义通常在于它们可以以与类相同的方式使用——超类关系!它们旨在为结构世界提供一种分层结构。他们通常会这样做。问题是,为什么这不应该在这里工作?
1赞 Martin R 10/29/2015
在 Xcode 7.1 中仍然不起作用,但错误消息现在是“不支持使用'P'作为符合协议'P'的具体类型”。
1赞 matt 10/29/2015
@MartinR 这是一个更好的错误消息。但对我来说,这仍然感觉像是语言中的一个洞。
0赞 Martin R 10/29/2015
确定!即使有 ,P 也不符合 Q。protocol P : Q { }

答:

69赞 Rob Napier 11/4/2015 #1

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

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

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

extension Array where Element : P {

当 since 不被视为 的具体一致性时,则不适用。(下面的“把它放在盒子里”的解决方案仍然是最通用的解决方案。Element == PPP


旧答案:

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

解决方案几乎总是把东西放在一个盒子里。我们构建了一个类型橡皮擦。

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

评论

0赞 jsadler 4/2/2016
这个答案中有很多有用的信息,但 Tomohiro 答案中的实际解决方案比这里介绍的拳击解决方案要好。
1赞 matt 6/24/2016
@jsadler 问题不在于如何绕过这个限制,而在于为什么存在这个限制。事实上,就解释而言,Tomohiro 的解决方法提出的问题多于答案。如果我们在我的数组示例中使用,我们会得到一个错误,相同类型要求使泛型参数'Element'成为非泛型。为什么 Tomohiro 的使用 不会产生相同的错误?====
1赞 AyBayBay 3/29/2017
@Rob纳皮尔,我仍然对你的反应感到困惑。与原始解决方案相比,Swift 如何看待您的解决方案更加具体?你似乎只是把东西包裹在一个结构中......Idk,也许我正在努力理解 swift 类型系统,但这一切似乎都是神奇的巫毒教
0赞 Rob Napier 3/29/2017
@AyBayBay 更新了答案。
0赞 AyBayBay 3/30/2017
非常感谢你@RobNapier我总是对你的回复速度感到惊讶,坦率地说,你是如何抽出时间帮助别人的。尽管如此,您的新编辑肯定会将其置于正确的位置。我想指出的另一件事是,理解类型擦除也帮助了我。特别是这篇文章做得非常出色:krakendev.io/blog/generic-protocols-and-their-shortcomings TBH Idk 我对其中一些东西的看法。似乎我们正在解释语言中的漏洞,但不知道苹果将如何构建其中的一些漏洞。
19赞 Tomohiro Kumagai 2/16/2016 #2

如果将 protocol 而不是 and constraint by protocol 扩展为具体类型,则可以按如下方式重写前面的代码。CollectionTypeArray

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赞 imre 2/18/2020
我不认为 Collection vs Array 在这里相关,重要的变化是使用 vs .使用 == 时,原始示例也有效。== 的一个潜在问题(取决于上下文)是它排除了子协议:如果我创建一个 ,然后定义为 then 将不再起作用(错误:SubP 和 P 必须等效)。== P: Pprotocol SubP: Parr[SubP]arr.test()
153赞 Hamish 4/14/2017 #3

为什么协议不符合自身?

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

这些包括:

  • static方法和属性
  • 初始值设定项
  • 关联类型(尽管这些类型目前阻止将协议用作实际类型)

我们可以在通用占位符上访问这些要求,但是我们不能在协议类型本身上访问它们,因为没有具体的符合类型可以转发。因此,我们不能允许 .TT : PTP

考虑一下,如果我们允许扩展适用于: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()

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

这是一个类似的故事,具有静态方法和属性要求:

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()barPPP

正因为如此,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要求符合 ;但是我们可以替换 IN 因为没有静态要求。如果我们向 添加静态要求,则该示例不再编译:TPPTPP

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

因此,解决此问题的一种方法是使您的协议 .诚然,在许多情况下,这不是一个理想的解决方法,因为它会强制您的符合类型成为类,并且需要 Obj-C 运行时,因此无法使其在 Linux 等非 Apple 平台上可行。@objc

但我怀疑这种限制是该语言已经为协议实现了“没有静态要求的协议符合自身”的主要原因(之一)。编译器可以大大简化围绕它们编写的通用代码。@objc

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

由于协议的这种简化表示,这种协议类型的值可以与某个泛型占位符类型的“通用值”共享相同的内存表示,大概使 Swift 团队很容易允许自洽。但是,对于非协议来说,情况并非如此,因为此类泛型值当前不携带值或协议见证表。@objcPT : P@objc

然而,这个功能有意为之的,并希望能够推广到非协议,正如 Swift 团队成员 Slava Pestov 在 SR-55 的评论中所证实的那样,以回应你对它的询问(由这个问题提示):@objc

Matt Neuburg 添加了评论 - 7 Sep 2017 1:33 PM

这确实编译了:

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

添加使其编译;删除它会使其不再编译。 我们中的一些人在 Stack Overflow 上对此感到惊讶,并希望 要知道这是故意的还是有缺陷的边缘情况。@objc

Slava Pestov 添加了评论 - 7 Sep 2017 1:53 PM

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

因此,希望语言有朝一日也能支持非协议。@objc

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


实现具有协议约束的扩展

在 Swift 3.1 中,如果你想要一个带有约束的扩展,即给定的泛型占位符或关联类型必须是给定的协议类型(而不仅仅是符合该协议的具体类型),你可以简单地用约束来定义它。==

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

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

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

当然,这现在阻止了我们在具有符合 的具体类型元素的数组上调用它。我们可以通过为 when 定义一个额外的扩展来解决这个问题,然后转发到扩展:PElement : P== P

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

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

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

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

我们不能传递给 ,因为我们目前无法替换通用占位符。让我们来看看解决这个问题的几种方法。ptakesConcreteP(_:)PT : P

1.打开存在主义

与其试图替换 ,不如深入研究类型化值所包装的底层具体类型并替换它呢?不幸的是,这需要一种称为“打开存在主义”的语言功能,目前用户无法直接使用该功能。PT : PP

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

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

请注意扩展方法采用的隐式泛型占位符,该占位符用于键入隐式参数 - 这发生在所有协议扩展成员的后台。当在协议类型值上调用这样的方法时,Swift 会挖掘出底层的具体类型,并使用它来满足泛型占位符。这就是为什么我们能够打电话 - 我们对 .SelfselfPSelftakesConcreteP(_:)selfTSelf

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

p.callTakesConcreteP()

并被调用,其泛型占位符由底层混凝土类型(在本例中)满足。请注意,这不是“符合自己的协议”,因为我们替换的是具体类型,而不是 - 尝试向协议添加静态要求,看看当你从内部调用它时会发生什么。takesConcreteP(_:)TSPtakesConcreteP(_:)

如果 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) 

出于同样的原因,具有多个参数的函数也会有问题,因为参数必须采用相同类型的参数——但是,如果我们有两个值,我们就无法保证它们在编译时都具有相同的底层具体类型。TP

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

2. 构建字体橡皮擦

正如 Rob 所说类型橡皮擦是解决协议不符合自身问题的最通用解决方案。它们允许我们将实例需求转发到底层实例,从而将协议类型的实例包装在符合该协议的具体类型中。

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

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

现在我们可以用而不是来谈谈:AnyPP

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

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

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

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

评论

4赞 matt 4/14/2017
也许我只是很密集,但我不明白为什么静态情况很特殊。我们(编译器)在编译时对 prototol 的静态属性的了解与我们对协议的实例属性的了解一样多或少,即采用者将实现它。那么有什么区别呢?
2赞 Hamish 4/14/2017
@matt 协议类型的实例(即包装在 existential 中的具体类型实例)很好,因为我们可以将对实例需求的调用转发到底层实例。然而,对于协议类型本身(即,字面上只是描述协议的类型)——没有采用者,因此没有什么可以调用静态需求,这就是为什么在上面的例子中我们不能有(对于(存在元类型)来说是不同的,它描述了符合的东西的具体元类型——但那是另一回事)PP.ProtocolSomeGeneric<P>P.TypeP
1赞 matt 4/14/2017
我在本页顶部提出的问题是,为什么协议类型采用者很好,而协议类型本身却不行。我知道对于协议类型本身,没有采用者。— 我不明白的是,为什么将静态调用转发到采用类型比将实例调用转发到采用类型更难。你说这里存在困难的原因是特别是因为静态需求的性质,但我看不出静态需求比实例需求更难。
2赞 matt 4/14/2017
好吧,我示例中的协议没有静态要求。(事实上,它根本没有要求。但编译器仍然不能接受它。
2赞 Jonathan. 3/22/2019
我真的不在乎健全性等,我只想编写应用程序,如果感觉它应该工作,它就应该工作。语言应该只是一个工具,而不是产品本身。如果在某些情况下它确实不起作用,那么在这些情况下禁止它,而是让其他人使用它的工作情况,让他们继续编写应用程序。