繁体   English   中英

协议不符合自身?

[英]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()

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

为什么不? 不知何故,这感觉像是语言上的一个漏洞。 我意识到问题源于将数组arr声明为协议类型的数组,但这是不合理的做法吗? 我认为协议确实可以帮助提供具有类型层次结构之类的结构吗?

为什么协议不符合自身?

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

这些包括:

  • static方法和属性
  • 初始化程序
  • 关联类型(尽管这些当前阻止将协议用作实际类型)

我们可以在通用占位符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()

我们不可能在[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()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 运行时,因此在非 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) }

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

Slava Pestov 添加了评论 - 2017 年 9 月 7 日下午 1:53

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

因此,希望有一天语言也能支持非@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()

然而值得注意的是,这将执行数组到[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类型值所包装的底层具体类型并用它代替呢? 不幸的是,这需要一种称为openexistentials的语言功能,目前用户无法直接使用该功能。

然而,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值,我们无法在编译时保证它们都具有相同的参数底层具体类型。

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

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有意义地表达这AnyP

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

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

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

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

extension Array where Element : P {

当不适Element == P ,因为P是没有考虑具体的一致性P (下面的“把它放在一个盒子里”的解决方案仍然是最通用的解决方案。)


旧答案:

这是元类型的另一种情况。 Swift 真的希望你为大多数非平凡的事情找到一个具体的类型。 [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()

当 Swift 允许您直接执行此操作时(我确实希望最终这样做),它可能只是通过自动为您创建此框。 递归枚举正好有这样的历史。 你必须将它们装箱,这非常烦人和限制,最后编译器添加了indirect来更自动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()

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM