为什么协议不符合自己?
在一般情况下,允许协议符合自己是不正确的。问题在于静态协议要求。
这些包括:
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()
上[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()
我们不能谈谈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 PM
这样可以编译:
@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添加了评论-7 Sep 2017 1:53 PM
这是有意的-解除此限制就是此错误的含义。就像我说的那样,这很棘手,我们还没有任何具体计划。
因此,希望有一天语言也将支持非@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()
但是,值得注意的是,这将执行数组O(n)到a的转换[P]
,因为每个元素都必须装在一个存在的容器中。如果性能是一个问题,则可以通过重新实现扩展方法来解决。这不是一个完全令人满意的解决方案–希望该语言的未来版本将包括一种表达“协议类型或符合协议类型”约束的方法。
在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
类型值是包装和替代品呢?不幸的是,这需要一种称为开放存在性的语言功能,该功能目前无法直接提供给用户。
但是,Swift 在访问它们上的成员时会隐式打开存在(协议类型的值)(即,它会挖掘运行时类型并以通用占位符的形式进行访问)。我们可以在以下协议扩展中利用这一事实P
:
extension P {
func callTakesConcreteP/*<Self : P>*/(/*self: Self*/) {
takesConcreteP(self)
}
}
请注意Self
扩展方法采用的隐式通用占位符,该占位符用于键入隐式self
参数-这发生在具有所有协议扩展成员的幕后。在协议类型的值上调用此类方法时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
。
let arr
行中的类型注释时,编译器将推断类型,[S]
然后代码进行编译。看来协议类型不能以与类-超类关系相同的方式使用。