为什么协议不符合自身?
在一般情况下允许协议遵守自身是不合理的。问题在于静态协议要求。
这些包括:
-
static
方法和属性
- 初始化器
- 关联类型(尽管这些当前阻止将协议用作实际类型)
我们可以通过通用占位符访问这些要求T
where T : P
– 然而我们cannot在协议类型本身上访问它们,因为没有具体的一致类型可以转发。因此我们不能允许T
to be 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()
on a [P]
, 因为P
(the Element
) 不是具体类型,因此无法实例化。它must在具有具体类型元素的数组上调用,其中该类型符合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>
。我们需要静态协议要求的具体实现(注意如何有no的实施foo()
or bar
在上面的例子中定义)。尽管我们可以在P
扩展,这些仅为符合以下条件的具体类型定义P
– 你仍然不能打电话给他们P
本身。
正因为如此,Swift 完全不允许我们使用协议作为符合自身的类型——因为当该协议有静态要求时,它就没有静态要求。
实例协议要求没有问题,因为您must在符合协议的实际实例上调用它们(因此必须已实现要求)。因此,当调用实例上的需求时,键入为P
,我们可以将该调用转发到该需求的底层具体类型的实现上。
然而,在这种情况下对规则进行特殊例外可能会导致通用代码处理协议的方式出现令人惊讶的不一致。话虽这么说,但情况并没有太大不同associatedtype
要求——(当前)阻止您使用协议作为类型。当协议具有静态要求时,限制您无法将协议用作符合自身的类型,这可能是该语言未来版本的一个选项
Edit:正如下面所探讨的,这看起来确实是 Swift 团队的目标。
@objc
协议
事实上,实际上就是这样exactly语言如何对待@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
for 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
然而,协议作为通用值当前不携带值或协议见证表。
不过这个功能is有意为之,并希望能够推广到非@objc
协议,由 Swift 团队成员 Slava Pestov 确认回应您的询问(由这个问题 https://stackoverflow.com/q/46103802/2976878):
马特·纽伯格 (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) }
Adding @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
。我们可以通过定义一个额外的扩展来解决这个问题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) 转换为[P]
,因为每个元素都必须装在一个存在的容器中。如果性能是一个问题,您可以通过重新实现扩展方法来简单地解决这个问题。这不是一个entirely令人满意的解决方案——希望该语言的未来版本将包含一种表达“协议类型”的方法or符合协议类型的约束。
在 Swift 3.1 之前,实现此目的的最通用方法是:正如罗布在他的回答中所表明的那样 https://stackoverflow.com/a/33524927/2976878,就是简单地为 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
to takesConcreteP(_:)
,因为我们目前无法替代P
对于通用占位符T : P
。让我们看一下解决这个问题的几种方法。
1. 开放存在主义
而不是试图替代P
for T : P
,如果我们能够深入研究底层的具体类型会怎样?P
输入的值被包装并替代它?不幸的是,这需要一个称为开放存在主义 https://github.com/apple/swift/blob/master/docs/GenericsManifesto.md#opening-existentials,目前用户无法直接使用。
然而,斯威夫特does访问存在值(协议类型值)时隐式打开它们上的成员(即,它挖掘出运行时类型并使其以通用占位符的形式进行访问)。我们可以在协议扩展中利用这个事实P
:
extension P {
func callTakesConcreteP/*<Self : P>*/(/*self: Self*/) {
takesConcreteP(self)
}
}
注意隐式泛型Self
扩展方法采用的占位符,用于键入隐式self
参数 – 这发生在所有协议扩展成员的幕后。当对协议类型值调用此类方法时P
,Swift 挖掘出底层的具体类型,并用它来满足Self
通用占位符。这就是为什么我们能够调用takesConcreteP(_:)
with self
– 我们很满意T
with Self
.
这意味着我们现在可以说:
p.callTakesConcreteP()
And takesConcreteP(_:)
使用通用占位符调用T
满足底层具体类型(在本例中S
)。请注意,这不是“符合自身的协议”,因为我们正在替换具体类型而不是P
– 尝试向协议添加静态要求,看看从内部调用它时会发生什么takesConcreteP(_:)
.
如果 Swift 继续不允许协议遵守自身,那么下一个最佳替代方案是在尝试将它们作为参数传递给泛型类型的参数时隐式打开存在性 - 有效地完成我们的协议扩展 Trampoline 所做的事情,只是没有样板。
但请注意,开放存在并不是解决协议不符合自身问题的通用解决方案。它不处理协议类型值的异构集合,这些值可能都有不同的底层具体类型。例如,考虑:
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. 构建一个橡皮擦
As Rob says https://stackoverflow.com/a/33524927/2976878, a 类型橡皮擦 http://robnapier.net/erasure,是解决协议不符合自身问题的最通用的解决方案。它们允许我们通过将实例需求转发到底层实例,将协议类型的实例包装在符合该协议的具体类型中。
所以,让我们构建一个转发的类型擦除盒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
.
因此,这种情况下的解决方案仅在以下情况下才真正有用:instance协议要求。一般情况下,我们还是不能治疗P
作为符合以下条件的具体类型P
.