为什么协议不符合自己?
在一般情况下允许协议符合自己是不合理的。问题在于静态协议要求。
这些包括:
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()
a [P]
,因为P
(the 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
forT
因为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
可以与 some generic placeholder 类型的“通用值”共享相同的内存表示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()
然而值得注意的是,这将执行数组到 a 的 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)
我们不能传递p
到takesConcreteP(_:)
,因为我们目前不能替代P
通用占位符T : P
。让我们看一下可以解决此问题的几种方法。
1. 开放存在主义
P
与其尝试替换,不如T : P
深入挖掘类型P
化值正在包装的底层具体类型并替换它呢?不幸的是,这需要一种称为开放存在主义的语言功能,目前用户无法直接使用该功能。
但是,Swift在访问它们上的成员时会隐式打开存在(协议类型的值)(即,它挖掘出运行时类型并使其以通用占位符的形式访问)。我们可以在协议扩展中利用这个事实P
:
extension P {
func callTakesConcreteP/*<Self : P>*/(/*self: Self*/) {
takesConcreteP(self)
}
}
请注意扩展方法采用的隐式通用Self
占位符,它用于键入隐式self
参数——这发生在所有协议扩展成员的幕后。当在协议类型的 value 上调用这样的方法时P
,Swift 挖掘出底层的具体类型,并使用它来满足Self
泛型占位符。这就是我们能够与 通话takesConcreteP(_:)
的原因self
——我们对 感到T
满意Self
。
这意味着我们现在可以说:
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
.