协议不符合自身?


125

为什么此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协议类型的数组,但这是不合理的事情吗?我认为协议确实可以帮助提供类型分层结构之类的结构吗?


1
当您删除该let arr行中的类型注释时,编译器将推断类型,[S]然后代码进行编译。看来协议类型不能以与类-超类关系相同的方式使用。
vadian 2015年

1
@vadian正确,这就是我说“我意识到问题源于将数组arr声明为协议类型的数组”时的问题。但是,正如我继续在我的问题中要说的那样,协议的全部重点通常是它们可以与类-超类关系相同的方式使用!它们旨在为结构世界提供一种层次结构。他们通常这样做。问题是,为什么不能在这里工作?
马特2015年

1
在Xcode 7.1中仍然不起作用,但是错误消息现在为“不支持使用'P'作为不符合协议'P'的具体类型”
Martin R

1
@MartinR这是一条更好的错误消息。但是在我看来,这仍然是语言上的一个漏洞。
马特2015年

当然!即使有protocol P : Q { },P不符合Q.
马丁- [R

Answers:


66

编辑:使用Swift还要工作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了自动执行相同功能的功能。


此答案中有很多有用的信息,但是,智博智答案中的实际解决方案比此处介绍的拳击解决方案更好。
jsadler '16

@jsadler问题不是如何解决限制,而是为什么存在限制。实际上,就解释而言,Tomohiro的解决方法提出的问题多于其答案。如果==在我的数组示例中使用,则会收到错误消息,相同类型的要求会使通用参数'Element'变为非通用。”为什么Tomohiro的使用不会==产生相同的错误?
matt

@Rob Napier我仍然对您的回应感到困惑。与原始版本相比,Swift如何在您的解决方案中看到更多具体性?您似乎只是将事物包装在一个结构中……Idk也许我在努力理解快速类型系统,但这一切似乎都像是魔法伏都
教徒

@AyBayBay更新答案。
罗布·纳皮尔

非常感谢@RobNapier我总是对您的回复速度感到惊讶,并且坦率地说,您如何找到时间与您一样多地帮助他人。但是,您的新编辑肯定会使它成为透视图。我还要指出的另一件事是,了解类型擦除也对我有所帮助。特别是这篇文章做得很棒:krakendev.io/blog/generic-protocols-and-their-缺点 TBH Idk我对其中的一些东西有何看法。似乎我们正在考虑该语言中的漏洞,但是Idk苹果将如何在其中构建一些语言
。– AyBayBay

109

为什么协议不符合自己?

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

这些包括:

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

我们不可能叫appendNew()[P],因为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; 但是我们可以代替它PT因为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运行时,因此在非Apple平台(例如Linux)上不可行。

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

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

由于此@objc协议的简化表示形式,因此该协议类型的值P可以与某个通用占位符类型的“通用值”共享相同的内存表示形式T : P,这可能使Swift团队更容易实现自我整合。对于非@objc协议,情况并非如此,但是由于此类通用值当前不包含值或协议见证表。

但是,此功能有意的,希望将其推广到非@objc协议中,正如Swift团队成员Slava Pestov 在SR-55的评论中针对您对此问题的询问所证实的(此问题提示):

Matt Neuburg添加了评论-2017年9月7日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) }

添加@objc使其编译;删除它使其无法再次编译。我们有些人在Stack Overflow上发现了这一点令人惊讶,并想知道这是故意的还是有缺陷的边缘情况。

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

当然,这现在使我们无法在具有符合的具体类型元素的数组上调用它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)到a的转换[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)

由于当前无法替代通用占位符,因此无法传递p给。让我们看一下解决这个问题的几种方法。takesConcreteP(_:)PT : P

1.开放性存在

而不是试图替代PT : P,如果我们能深入到什么底层的具体类型,该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.建立一种类型的橡皮擦

正如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


2
也许我只是很笨,但是我不明白为什么静态情况很特殊。我们(编译器)在编译时对协议的静态属性的了解与对协议的实例属性的了解或多或少一样,即采用者将实现它。那有什么区别呢?
马特

1
@matt一个协议类型的实例(即包裹在existential中的具体类型的实例P)很好,因为我们可以将对实例需求的调用转发到基础实例。但是,对于协议类型本身(即P.Protocol,实际上只是描述协议的类型)–没有采用者,因此没有什么可以调用静态需求的,这就是为什么在上面的示例中我们不能拥有SomeGeneric<P>(这是P.Type(存在性元型)有所不同,后者描述了符合的事物的具体元型P-但这是另一个故事)
Hamish

我在本页顶部询问的问题是,为什么协议类型采用者很好,而协议类型本身却不好。我了解协议类型本身没有采用者。—我不明白的是,为什么将静态调用转发给采用类型要比将实例调用转发给采用类型更难。您在争辩说这里存在困难的原因是因为静态需求的本质,但是我看不出静态需求比实例需求更困难。
马特

@matt并不是说静态需求比实例需求“难” –编译器可以通过实例的存在性(例如,实例类型为P)和存在的元类型(即,元类型)P.Type很好地处理。问题在于,对于泛型–我们并没有真正地进行比较。如果TP,有没有具体的underyling(元)型向前静态要求(TP.Protocol,不是P.Type)....
麦高

1
我真的不在乎健全性等,我只想编写应用程序,如果感觉可以正常运行就应该这样做。语言应该只是一种工具,而不是产品本身。如果在某些情况下确实无法使用,则可以在这种情况下禁止使用它,但是让其他人使用它适用的情况并让他们继续编写应用程序。
乔纳森。

17

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

我不认为收藏VS阵是与此有关,最重要的变化是使用== PVS : P。使用==,原始示例也适用。==的潜在问题(取决于上下文)是它排除了子协议:如果我创建了protocol SubP: P,然后定义arr为,[SubP]那么arr.test()它将不再起作用(错误:SubP和P必须等效)。
伊姆雷
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.