为什么协议不能符合自身?
一般情况下,允许协议符合自身是不可靠的。问题在于静态协议要求。
这些要求包括:
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() {
append(Element())
}
}
var arr: [P] = [S(), S1()]
arr.appendNew()
我们不可能在
[P]
上调用
appendNew()
,因为
P
(即
Element
)不是具体类型,所以无法实例化。它必须在具有具体类型元素的数组上被调用,其中该类型符合
P
。
静态方法和属性要求也是类似的情况:
protocol P {
static func foo()
static var bar: Int { get }
}
struct SomeGeneric<T : P> {
func baz() {
print(T.bar)
T.foo()
}
}
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)
因此,解决这个问题的一个变通方法是将您的协议设置为
@objc
。尽管在许多情况下,这并不是理想的解决方法,因为它强制使您的符合类型成为类,并要求使用Obj-C运行时,因此在诸如Linux等非苹果平台上不可行。
但我怀疑这个限制是语言已经实现“没有静态要求的协议符合自身”的(其中之一)主要原因,用于
@objc
协议。编写围绕它们的通用代码可以由编译器显着简化。
为什么?因为
@objc
协议类型的值实际上只是类引用,其要求使用
objc_msgSend
进行调度。另一方面,非
@objc
协议类型的值更加复杂,因为它们携带值和见证表,以便管理其(可能间接存储的)包装值的内存,并确定为不同要求调用哪些实现。
由于对
@objc
协议的简化表示,该协议类型
P
的值可以与某些通用占位符
T:P
类型的“通用值”共享相同的内存表示,这使得Swift团队可以轻松地允许自我一致性。然而,对于非
@objc
协议来说,并不是如此,因为这样的通用值目前不包含值或协议见证表。
然而,此功能是有意的,并希望将其推广到非
@objc
协议上,正如Swift团队成员Slava Pestov在
SR-55的评论中(由
此问题引起)确认的那样。
Matt Neuburg added a comment - 7 Sep 2017 1:33 PM
This does compile:
@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
makes it compile; removing it makes it not compile again.
Some of us over on Stack Overflow find this surprising and would like
to know whether that's deliberate or a buggy edge-case.
Slava Pestov added a comment - 7 Sep 2017 1:53 PM
It's deliberate – lifting this restriction is what this bug is about.
Like I said it's tricky and we don't have any concrete plans yet.
希望有一天编程语言能够支持非
@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()
然而值得注意的是,这会将数组转换为一个
[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)
takesConcreteP(p)
我们无法将
p
传递给
takesConcreteP(_:)
,因为我们目前无法将
P
替换为通用占位符
T : P
。让我们看一下解决此问题的几种方法。
1. 打开存在类型
与其尝试将
P
替换为
T : P
,如果我们可以深入到
P
类型值所包装的底层具体类型并替换它,那会怎么样?不幸的是,这需要一种称为
打开存在类型的语言特性,目前用户无法直接使用。
然而,当访问它们的成员时(即挖掘出运行时类型并以通用占位符的形式使其可访问),Swift确实隐式打开存在类型(协议类型值)。我们可以在
P
的协议扩展中利用这个事实:
extension P {
func callTakesConcreteP() {
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) {}
}
func takesConcreteArrayOfP<T : P>(_ t: [T]) {}
let array: [P] = [S(bar: 1), Q(bar: 2)]
takesConcreteArrayOfP(array)
出于同样的原因,具有多个
T
参数的函数也会有问题,因为这些参数必须采用相同类型的参数 - 然而,如果我们有两个
P
值,则无法保证它们在编译时具有相同的基础具体类型。
为了解决这个问题,我们可以使用类型擦除器。
如
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)
let array = [AnyP(S(bar: 1)), AnyP(Q(bar: 2))]
takesConcreteArrayOfP(array)
现在,考虑一下为什么我们必须建立那个盒子。正如我们之前讨论的那样,Swift需要一个具体类型来满足协议有静态需求的情况。如果P有静态需求——我们就需要在AnyP中实现它。但是应该如何实现呢?我们正在处理符合P的任意实例,我们不知道它们的底层具体类型如何实现静态需求,因此我们不能有意义地在AnyP中表达这一点。
因此,在这种情况下,解决方案只在实例协议要求的情况下才真正有用。在一般情况下,我们仍然不能将P视为符合P的具体类型。
[S]
,代码可以编译。看起来协议类型不能像类-超类关系那样使用。 - vadian