为什么这个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()
编译器说:“类型 P
不符合协议 P
" (or, in later versions of Swift, "使用'P'作为符合协议的具体类型'P'不受支持 . ”) .
为什么不?不知何故,这感觉就像语言中的漏洞 . 我意识到问题源于将数组 arr
声明为协议类型的数组,但这是不合理的事情吗?我认为协议正是为了帮助提供类似层次结构的结构?
3 回答
如果扩展
CollectionType
协议而不是Array
并通过协议约束作为具体类型,则可以按如下方式重写以前的代码 .编辑:与Swift一起工作18个月,另一个主要版本(提供新的诊断),@ AyBayBay的评论让我想重写这个答案 . 新的诊断是:
这实际上使整个事情变得更加清晰 . 这个扩展名:
自
Element == P
以来P
不被视为P
的具体一致性 . (下面的"put it in a box"解决方案仍然是最常用的解决方案 . )旧答案:
这是元类型的另一个例子 . 斯威夫特真的希望你能找到适合大多数非平凡事物的具体类型 . [P]不是具体类型(您不能为P分配已知大小的内存块) . (我实际上并不是真的;你绝对可以创建一些大小
P
,因为it's done via indirection . )我没有任何证据证明这是"shouldn't"工作的情况 . 这看起来非常像他们的"doesn't work yet"案件之一 . (不幸的是,几乎不可能让Apple确认这些案例之间的区别 . )Array<P>
可以是一个变量类型(Array
不能)的事实表明他们认为你会得到一个更好的答案 . {498516_答案 . "Because the compiler doesn't allow it."(不满意,我知道 . 我的整个生活......)解决方案几乎总是把东西放在一个盒子里 . 我们建造了一个类型橡皮擦 .
当Swift允许你直接执行此操作(我最终期望),它可能只是为您自动创建此框 . 递归枚举正是这段历史 . 你不得不打包它们,这非常烦人和限制,然后最后编译器添加
indirect
更自动地做同样的事情 .为什么协议不符合自己?
在一般情况下允许协议符合自己是不合理的 . 问题在于静态协议要求 .
这些包括:
static
方法和属性初始化者
关联类型(尽管这些类型目前阻止将协议用作实际类型)
我们可以在通用占位符
T
上访问这些要求T : P
- 但是我们无法在协议类型本身上访问它们,因为没有具体的符合类型要转发 . 因此,我们不能允许T
为P
.如果我们允许
Array
扩展适用于[P]
,请考虑以下示例中会发生什么:我们不可能在
[P]
上调用appendNew()
,因为P
(Element
)不是具体类型,因此无法实例化 . 必须在具有具体类型元素的数组上调用它,其中该类型符合P
.这与静态方法和属性要求类似:
我们不能谈论
SomeGeneric<P>
. 我们需要静态协议要求的具体实现(注意上面的例子中没有定义foo()
或bar
的实现) . 虽然我们可以在P
扩展中定义这些需求的实现,但这些只是针对符合P
的具体类型定义的 - 您仍然无法在P
本身上调用它们 .因此,Swift完全不允许我们将协议用作符合其自身的类型 - 因为当该协议具有静态要求时,它不会 .
实例协议要求不成问题,因为您必须在符合协议的实际实例上调用它们(因此必须已实现要求) . 因此,当在类型为
P
的实例上调用需求时,我们可以将该调用转发到底层具体类型的该需求的实现上 .但是,在这种情况下对规则进行特殊例外可能会导致通用代码处理协议的方式出现意外的不一致 . 尽管如此,情况与
associatedtype
要求并没有太大不同 - 这些要求(目前)阻止您将协议用作类型 . 有一个限制,阻止您使用协议作为一种类型,当它具有静态要求时符合自己的类型可能是一个未来版本的选项语言Edit: 如下所述,这看起来就像Swift团队的目标 .
@objc协议
事实上,实际上,这正是语言如何对待
@objc
协议 . 当他们没有静态要求时,他们就会顺从自己 .以下编译就好了:
baz
要求T
符合P
;但我们可以在P
中替换T
,因为P
没有静态要求 . 如果我们向P
添加静态需求,则示例不再编译:因此,针对此问题的一种解决方法是使您的协议
@objc
. 当然,在许多情况下,这不是一个理想的解决方法,因为它会强制您的符合类型成为类,并且需要Obj-C运行时,因此不能使其在非Apple平台(如Linux)上可行 .但我怀疑这种限制是(其中一个)语言为
@objc
协议实现'protocol without static requirements conforms to itself'的主要原因 . 编译器可以显着简化围绕它们编写的通用代码 .为什么?因为
@objc
协议类型的值实际上只是使用objc_msgSend
调度其需求的类引用 . 另一方面,非@objc
协议类型的值更复杂,因为它们同时携带值和见证表,以便管理其(可能是间接存储的)包装值的内存并确定要为其调用的实现 . 不同的要求,分别 .由于
@objc
协议的这种简化表示,这种协议类型P
的值可以与某些通用占位符T : P
类型的'generic value'共享相同的内存表示,可能使Swift团队很容易实现自我一致性 . 但是,对于非@objc
协议,情况并非如此,因为这些通用值目前不包含值或协议见证表 .然而,这个功能是有意的,并且有望推广到非
@objc
协议,正如Swift团队成员Slava Pestov in the comments of SR-55在回复您的查询时所证实的那样(由this question提示):func process <T:P>(item:T) - > T
func f(图片:P){let processed:P = process(item:image)}
添加@objc使其编译;删除它使它不能再次编译 . Stack Overflow中的一些人发现这令人惊讶,并想知道这是故意还是错误的边缘情况 . Slava Pestov添加了评论 - 2017年9月7日下午1:53这是故意的 - 解除这个限制就是这个bug的内容 . 就像我说的那样棘手,我们还没有任何具体的计划 .
所以希望有一天语言能够支持非
@objc
协议 .但目前的解决方案是非
@objc
协议?使用协议约束实现扩展
在Swift 3.1中,如果您希望扩展具有约束,即给定的通用占位符或关联类型必须是给定的协议类型(而不仅仅是符合该协议的具体类型) - 您可以使用
==
约束来定义它 .例如,我们可以将您的数组扩展名编写为:
当然,这现在阻止我们在具有符合
P
的具体类型元素的数组上调用它 . 我们可以通过为Element : P
定义一个额外的扩展来解决这个问题,然后转发到== P
扩展:然而,值得注意的是,这将执行数组的O(n)转换为
[P]
,因为每个元素必须在存在容器中装箱 . 如果性能是一个问题,您可以通过重新实现扩展方法来解决这个问题 . 这不是一个完全令人满意的解决方案 - 希望该语言的未来版本将包括表达'协议类型或符合协议类型'约束的方式 .在Swift 3.1之前,最常见的实现方法是as Rob shows in his answer,只需为
[P]
构建一个包装器类型,然后就可以在其上定义扩展方法了 .将协议类型实例传递给受约束的通用占位符
考虑以下(人为的,但并非罕见的)情况:
我们无法将
p
传递给takesConcreteP(_:)
,因为我们目前无法将P
替换为通用占位符T : P
. 让我们来看看我们可以解决这个问题的几种方法 .1.开放存在
如果我们可以深入研究
P
类型值所包含的基础具体类型并替代它,那该怎么办呢?而不是试图用P
代替T : P
. 不幸的是,这需要一个名为opening existentials的语言功能,该功能目前无法直接供用户使用 .然而,Swift隐含地打开存在(协议类型当访问它们上的成员时(即它挖掘出运行时类型并使其以通用占位符的形式访问) . 我们可以在
P
上的协议扩展中利用这个事实:注意扩展方法采用的隐式泛型
Self
占位符,用于键入隐式self
参数 - 这发生在所有协议扩展成员的幕后 . 当在协议类型值P
上调用这样的方法时,Swift挖掘出底层的具体类型,并使用它来满足Self
通用占位符 . 这就是为什么我们能够用self
调用takesConcreteP(_:)
- 我们用Self
满足T
.这意味着我们现在可以说:
takesConcreteP(_:)
被调用,其通用占位符T
被底层具体类型(在本例中为S
)所满足 . 请注意,这不是取代具体类型而不是P
- 尝试向协议添加静态要求,并查看从takesConcreteP(_:)
内调用时发生的情况 .如果Swift继续禁止协议符合自己,那么下一个最好的替代方案是在尝试将它们作为参数传递给泛型类型的参数时隐式地打开存在 - 有效地完成我们的协议扩展蹦床所做的事情,只是没有样板 .
但请注意,开放存在不是解决不符合自身协议问题的一般解决方案 . 它不涉及协议类型值的异构集合,它们可能都具有不同的底层具体类型 . 例如,考虑:
出于同样的原因,具有多个
T
参数的函数也会有问题,因为参数必须采用相同类型的参数 - 但是如果我们有两个P
值,我们无法在编译时保证它们都具有相同的值基础混凝土类型 .为了解决这个问题,我们可以使用类型橡皮擦 .
2.构建一个类型橡皮擦
作为Rob says,type eraser,是解决不符合自身协议问题的最通用解决方案 . 它们允许我们通过将实例需求转发到底层实例,将协议类型的实例包装在符合该协议的具体类型中 .
所以,让我们构建一个类型擦除框,将
P
的实例要求转发到符合P
的基础任意实例:现在我们可以用
AnyP
来代替P
:现在,考虑一下为什么我们必须建造那个盒子 . 正如我们早期讨论的那样,对于协议具有静态要求的情况,Swift需要具体类型 . 考虑
P
是否有静态要求 - 我们需要在AnyP
中实现它 . 但它应该被实施为什么?我们在这里处理符合P
的任意实例 - 我们不知道它们的底层具体类型如何实现静态需求,因此我们无法在AnyP
中有意义地表达它 .因此,在这种情况下的解决方案仅在实例协议要求的情况下才真正有用 . 在一般情况下,我们仍然不能将
P
视为符合P
的具体类型 .