议定书不符合自己的要求?

mat*_*att 117 generics swift swift-protocols

为什么这个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()
Run Code Online (Sandbox Code Playgroud)

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

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

Ham*_*ish 89

为什么协议不符合自己?

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

这些包括:

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

我们可以在一个通用的占位符,访问这些要求T地方T : P-但我们不能访问它们的协议类型本身,因为没有具体的符合的类型转发到.因此,我们不能让TP.

如果我们允许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()
Run Code Online (Sandbox Code Playgroud)

我们不可能调用appendNew()a [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()
Run Code Online (Sandbox Code Playgroud)

我们不能谈论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)
Run Code Online (Sandbox Code Playgroud)

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)'
Run Code Online (Sandbox Code Playgroud)

因此,解决此问题的一个方法是制定您的协议@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

这确实编译:

@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) }
Run Code Online (Sandbox Code Playgroud)

添加@objc使其编译; 删除它使它不能再次编译.Stack Overflow中的一些人发现这令人惊讶,并想知道这是故意还是错误的边缘情况.

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()
Run Code Online (Sandbox Code Playgroud)

当然,这现在阻止我们在具有符合的具体类型元素的数组上调用它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()
Run Code Online (Sandbox Code Playgroud)

然而值得注意的是,这将执行对数组的O(n)转换[P],因为每个元素必须在存在容器中加框.如果性能是一个问题,您可以通过重新实现扩展方法来解决这个问题.这不是一个完全令人满意的解决方案 - 希望该语言的未来版本将包括表达'协议类型符合协议类型'约束的方式.

在Swift 3.1之前,正如Rob在他的回答中所说,实现这一目标的最常用方法是简单地为a构建一个包装器类型[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)
Run Code Online (Sandbox Code Playgroud)

我们不能通ptakesConcreteP(_:),因为我们目前无法替代P的通用占位符T : P.让我们来看看我们可以解决这个问题的几种方法.

1.开放存在

而不是试图替代PT : P,如果我们能够深入到底层的具体类型,该是什么P类型的值包装和替代品呢?不幸的是,这需要一种称为开放存在的语言功能,该功能目前不能直接供用户使用.

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

extension P {
  func callTakesConcreteP/*<Self : P>*/(/*self: Self*/) {
    takesConcreteP(self)
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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

p.callTakesConcreteP()
Run Code Online (Sandbox Code Playgroud)

并通过其底层具体类型(在本例中)满足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) 
Run Code Online (Sandbox Code Playgroud)

出于同样的原因,具有多个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) }
}
Run Code Online (Sandbox Code Playgroud)

现在我们可以谈谈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)
Run Code Online (Sandbox Code Playgroud)

现在,考虑一下为什么我们必须建造那个盒子.正如我们早期讨论的那样,对于协议具有静态要求的情况,Swift需要具体类型.考虑是否P有静态要求 - 我们需要实现它AnyP.但它应该被实施为什么?我们正在处理符合P此处的任意实例- 我们不知道它们的基本具体类型如何实现静态需求,因此我们无法有意义地表达这一点AnyP.

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

  • 也许我只是很密集,但我不明白为什么静态情况很特殊。我们(编译器)在编译时对协议的静态属性的了解与我们对协议的实例属性的了解一样多或少,即采用者将实现它。那么有什么区别呢? (3认同)
  • @matt 协议类型的实例(即包装在存在性`P` 中的具体类型的实例)很好,因为我们可以将实例需求的调用转发给底层实例。然而,对于协议类型*本身*(即`P.Protocol`,字面上只是描述协议的类型)——没有采用者,因此没有什么可以调用静态需求,这就是为什么在上面的例子中我们不能有`SomeGeneric&lt;P&gt;`(它与`P.Type`(存在元类型)不同,它描述了符合`P`的东西的具体元类型——但那是另一回事了) (2认同)
  • 嗯,我的示例中的协议没有静态要求。(事实上​​它根本没有任何要求。)但它仍然不能被编译器接受。 (2认同)
  • 我真的不关心健全性等,我只想编写应用程序,如果感觉它应该工作它就应该工作。语言应该只是一种工具,而不是产品本身。如果在某些情况下它确实不起作用,那么在这些情况下可以禁止它,但让其他人使用它适用的情况,并让他们继续编写应用程序。 (2认同)

Rob*_*ier 62

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

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

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

extension Array where Element : P {
Run Code Online (Sandbox Code Playgroud)

Element == P因为P不被视为具体的一致性而不适用P.(下面的"把它放在一个盒子里"的解决方案仍然是最通用的解决方案.)


旧答案:

这是元类型的另一个例子.斯威夫特真的希望你能找到适合大多数非平凡事物的具体类型.[P]不是具体类型(您不能分配已知大小的内存块P).(我认为这不是真的;你绝对可以创造一些大小的东西,P因为它是通过间接完成的.)我认为没有任何证据证明这是"不应该"工作的情况.这看起来非常像他们的"还没有工作"的情况.(不幸的是,几乎不可能让Apple确认这些情况之间的区别.)事实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()
Run Code Online (Sandbox Code Playgroud)

当Swift允许您直接执行此操作(我最终期望)时,可能只是为您自动创建此框.递归枚举正是这段历史.你不得不打包它们,这非常令人讨厌和限制,然后最后编译器添加indirect了更自动地做同样的事情.


小智 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()
Run Code Online (Sandbox Code Playgroud)

  • 我不认为 Collection 与 Array 在这里相关,重要的变化是使用 `== P` 与 `: P`。使用 == 原始示例也可以工作。== 的一个潜在问题(取决于上下文)是它排除了子协议:如果我创建一个 `protocol SubP: P`,然后将 `arr` 定义为 `[SubP]`,然后将 `arr.test() ` 将不再起作用(错误:SubP 和 P 必须相等)。 (3认同)