区别在于编译器会特殊处理Array
(以及Set
和Dictionary
),允许协变(我稍微详细地介绍了一下这个Q&A)。
然而,任意泛型类型是不变的,也就是说,如果T != U
,则X<T>
是与X<U>
完全无关的类型——T
和U
之间的任何其他类型关系(如子类型化)都是无关紧要的。应用于您的情况,即使ChildClass
是BaseProtocol
的子类型(也请参阅此问题解答),Signal<ChildClass>
和Signal<BaseProtocol>
也是无关的类型。
这是因为这将完全破坏泛型引用类型的定义,这些类型针对
T
的逆变性质(如函数参数和属性设置器)进行定义。
例如,如果您将
Signal
实现为:
class Signal<T> {
var t: T
init(t: T) {
self.t = t
}
}
如果你能够说:
let signalInt = Signal(t: 5)
let signalAny: Signal<Any> = signalInt
然后你可以说:
signalAny.t = "wassup" // assigning a String to a Signal<Int>'s `t` property.
这是完全错误的,因为您无法将一个字符串(String)赋值给一个整数(Int)属性。
这种行为在数组(Array)中是安全的原因是它是一种值类型 - 因此当您执行以下操作时:
let intArray = [2, 3, 4]
var anyArray : [Any] = intArray
anyArray.append("wassup")
没有问题,因为anyArray
是intArray
的一个副本 - 因此append(_:)
的逆变性不是问题。
然而,这不能应用于任意泛型值类型,因为值类型可以包含任意数量的泛型引用类型,这将使我们回到允许泛型引用类型定义逆变事物的非法操作的危险道路上。
正如Rob在他的回答中所言,对于参考类型,如果您需要保持对相同基础实例的引用,则解决方案是使用类型擦除器。
如果我们考虑下面的例子:
protocol BaseProtocol {}
class ChildClass: BaseProtocol {}
class AnotherChild : BaseProtocol {}
class Signal<T> {
var t: T
init(t: T) {
self.t = t
}
}
let childSignal = Signal(t: ChildClass())
let anotherSignal = Signal(t: AnotherChild())
一个类型擦除器,可以包装任何符合
BaseProtocol
的
T
的
Signal<T>
实例,可能会像这样:
struct AnyBaseProtocolSignal {
private let _t: () -> BaseProtocol
var t: BaseProtocol { return _t() }
init<T : BaseProtocol>(_ base: Signal<T>) {
_t = { base.t }
}
}
let signals = [AnyBaseProtocolSignal(childSignal), AnyBaseProtocolSignal(anotherSignal)]
现在,我们可以使用异构类型的
Signal
进行交流,其中
T
是符合
BaseProtocol
的某种类型。
然而,这个包装器的一个问题是我们只能用
BaseProtocol
交流。如果我们有
AnotherProtocol
并想要一个类型擦除器来处理符合
AnotherProtocol
的
Signal
实例,怎么办?
解决方法之一是将一个
transform
函数传递给类型擦除器,允许我们执行任意向上转换。
struct AnySignal<T> {
private let _t: () -> T
var t: T { return _t() }
init<U>(_ base: Signal<U>, transform: @escaping (U) -> T) {
_t = { transform(base.t) }
}
}
现在,我们可以用异构类型的
Signal
进行交流,其中
T
是可转换为某个指定的
U
类型的一些类型,在创建类型擦除器时需要进行指定。
let signals: [AnySignal<BaseProtocol>] = [
AnySignal(childSignal, transform: { $0 }),
AnySignal(anotherSignal, transform: { $0 })
]
然而,将相同的
transform
函数传递给每个初始化器有些不便。
在 Swift 3.1(可用于 Xcode 8.3 beta)中,您可以通过在扩展中为
BaseProtocol
定义自己的初始化程序来减轻调用者的负担:
extension AnySignal where T == BaseProtocol {
init<U : BaseProtocol>(_ base: Signal<U>) {
self.init(base, transform: { $0 })
}
}
现在,您只需要说:
(并为您想要转换的任何其他协议类型重复此操作)
let signals: [AnySignal<BaseProtocol>] = [
AnySignal(childSignal),
AnySignal(anotherSignal)
]
(您实际上可以在此处删除数组的显式类型注释,编译器将推断其为[AnySignal<BaseProtocol>]
- 但如果您要允许更多方便的初始化程序,则应保持显式)
对于值类型或想要特定“创建”新实例的引用类型,解决方案是从
Signal<T>
(其中
T
符合
BaseProtocol
) 执行转换到
Signal<BaseProtocol>
。
在 Swift 3.1 中,您可以通过在
Signal
类型的扩展中定义一个(便捷)初始化器来实现这一点,其中
T == BaseProtocol
。
extension Signal where T == BaseProtocol {
convenience init<T : BaseProtocol>(other: Signal<T>) {
self.init(t: other.t)
}
}
let signals: [Signal<BaseProtocol>] = [
Signal(other: childSignal),
Signal(other: anotherSignal)
]
在 Swift 3.1 之前,可以通过实例方法来实现此功能:
extension Signal where T : BaseProtocol {
func asBaseProtocol() -> Signal<BaseProtocol> {
return Signal<BaseProtocol>(t: t)
}
}
let signals: [Signal<BaseProtocol>] = [
childSignal.asBaseProtocol(),
anotherSignal.asBaseProtocol()
]
在这两种情况下,对于一个
struct
,程序流程将是相似的。