提问人:matt 提问时间:10/14/2015 最后编辑:Vadim Kotovmatt 更新时间:1/14/2023 访问量:26885
协议不符合自身?
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()
编译器说:“类型不符合协议”(或者,在更高版本的 Swift 中,“不支持使用'P'作为符合协议'P'的具体类型。P
P
为什么不呢?不知何故,这感觉就像是语言中的一个漏洞。我意识到问题源于将数组声明为协议类型的数组,但这样做是不合理的吗?我以为协议正好是为了帮助为结构提供类似类型层次结构的东西吗?arr
答:
编辑:与另一个主要版本(提供新的诊断)Swift合作了18个月,以及@AyBayBay的评论使我想重写这个答案。新的诊断是:
“不支持使用'P'作为符合协议'P'的具体类型。”
这实际上使整个事情变得更加清晰。此扩展:
extension Array where Element : P {
当 since 不被视为 的具体一致性时,则不适用。(下面的“把它放在盒子里”的解决方案仍然是最通用的解决方案。Element == P
P
P
旧答案:
这是又一个元型的案例。Swift 真的希望你能够为大多数非平凡的事情找到一个具体的类型。(我不认为这是真的;你绝对可以创造一些大小的东西,因为它是通过间接完成的。我不认为有任何证据表明这是一个“不应该”工作的案例。这看起来很像他们的“还不起作用”的案例之一。(不幸的是,几乎不可能让苹果确认这些情况之间的区别。事实上,可以是变量类型(不能是变量类型)表明他们已经在这个方向上做了一些工作,但 Swift 元类型有很多尖锐的边缘和未实现的情况。我不认为你会得到比这更好的“为什么”答案。“因为编译器不允许这样做。”(不令人满意,我知道。我的整个斯威夫特生活......[P]
不是具体类型(不能为 P
分配已知大小的内存块)。P
Array<P>
Array
解决方案几乎总是把东西放在一个盒子里。我们构建了一个类型橡皮擦。
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
评论
==
==
如果将 protocol 而不是 and constraint by protocol 扩展为具体类型,则可以按如下方式重写前面的代码。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()
评论
== P
: P
protocol SubP: P
arr
[SubP]
arr.test()
为什么协议不符合自身?
在一般情况下,允许协议符合自身是不合理的。问题在于静态协议要求。
这些包括:
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()
我们不可能调用 ,因为 (the ) 不是具体类型,因此无法实例化。必须在具有具体类型元素的数组上调用它,其中该类型符合 。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
要求符合 ;但是我们可以替换 IN 因为没有静态要求。如果我们向 添加静态要求,则该示例不再编译: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)'
因此,解决此问题的一种方法是使您的协议 .诚然,在许多情况下,这不是一个理想的解决方法,因为它会强制您的符合类型成为类,并且需要 Obj-C 运行时,因此无法使其在 Linux 等非 Apple 平台上可行。@objc
但我怀疑这种限制是该语言已经为协议实现了“没有静态要求的协议符合自身”的主要原因(之一)。编译器可以大大简化围绕它们编写的通用代码。@objc
为什么?因为协议类型的值实际上只是类引用,其需求使用 .另一方面,非协议类型的值则更复杂,因为它们同时携带值表和见证表,以便管理其(可能间接存储的)包装值的内存,并分别确定要为不同的需求调用哪些实现。@objc
objc_msgSend
@objc
由于协议的这种简化表示,这种协议类型的值可以与某个泛型占位符类型的“通用值”共享相同的内存表示,大概使 Swift 团队很容易允许自洽。但是,对于非协议来说,情况并非如此,因为此类泛型值当前不携带值或协议见证表。@objc
P
T : P
@objc
然而,这个功能是有意为之的,并希望能够推广到非协议,正如 Swift 团队成员 Slava Pestov 在 SR-55 的评论中所证实的那样,以回应你对它的询问(由这个问题提示):@objc
Matt Neuburg 添加了评论 - 7 Sep 2017 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) }
添加使其编译;删除它会使其不再编译。 我们中的一些人在 Stack Overflow 上对此感到惊讶,并希望 要知道这是故意的还是有缺陷的边缘情况。
@objc
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()
当然,这现在阻止了我们在具有符合 的具体类型元素的数组上调用它。我们可以通过为 when 定义一个额外的扩展来解决这个问题,然后转发到扩展: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()
然而,值得注意的是,这将执行数组到 a 的 O(n) 转换,因为每个元素都必须装箱在一个存在容器中。如果性能是一个问题,则可以通过重新实现扩展方法来解决此问题。这并不是一个完全令人满意的解决方案——希望该语言的未来版本将包含一种表达“协议类型或符合协议类型”约束的方法。[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)
}
}
请注意扩展方法采用的隐式泛型占位符,该占位符用于键入隐式参数 - 这发生在所有协议扩展成员的后台。当在协议类型值上调用这样的方法时,Swift 会挖掘出底层的具体类型,并使用它来满足泛型占位符。这就是为什么我们能够打电话 - 我们对 .Self
self
P
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
评论
P
P.Protocol
SomeGeneric<P>
P.Type
P
评论
let arr
[S]
protocol P : Q { }