我正在使用 Signals图书馆。
假设我定义了 BaseProtocol 协议(protocol)和 ChildClass
符合 BaseProtocol
.
protocol BaseProtocol {}
class ChildClass: BaseProtocol {}
现在我想存储如下信号:
var signals: Array<Signal<BaseProtocol>> = []
let signalOfChild = Signal<ChildClass>()
signals.append(signalOfChild)
我得到错误:
但我可以编写下一行而不会出现任何编译器错误:
var arrays = Array<Array<BaseProtocol>>()
let arrayOfChild = Array<ChildClass>()
arrays.append(arrayOfChild)
那么,泛型 Swift Array 和泛型 Signal 有什么区别呢?
最佳答案
区别在于 Array
(和 Set
和 Dictionary
)从编译器得到特殊处理,允许协方差(我更详细地讨论了这个 in this Q&A )。
然而,任意泛型类型是 invariant , 表示 X<T>
是与 X<U>
完全无关的类型如果 T != U
– T
之间的任何其他类型关系和 U
(例如子类型)是无关紧要的。适用于您的案例,Signal<ChildClass>
和 Signal<BaseProtocol>
是不相关的类型,即使 ChildClass
是 BaseProtocol
的子类型(另见 this Q&A)。
原因之一是它会完全破坏定义与 T
相关的逆变事物(例如函数参数和属性 setter )的通用引用类型。 .
例如,如果您实现了 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
属性(property)。为什么这种事情对
Array
来说是安全的是它是一个值类型 - 因此当你这样做时:let intArray = [2, 3, 4]
var anyArray : [Any] = intArray
anyArray.append("wassup")
没有问题,如
anyArray
是 intArray
的副本– 因此 append(_:)
的逆变性不是问题。然而,这不能应用于任意的泛型值类型,因为值类型可以包含任意数量的泛型引用类型,这导致我们回到允许对定义逆变事物的泛型引用类型进行非法操作的危险道路。
As Rob says在他的回答中,如果您需要维护对同一底层实例的引用,则引用类型的解决方案是使用类型橡皮擦。
如果我们考虑这个例子:
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())
一个类型橡皮擦,可以包裹任何
Signal<T>
T
的实例符合 BaseProtocol
可能看起来像这样: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
并想要 Signal
的类型橡皮擦T
的实例符合 AnotherProtocol
?一种解决方案是通过
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 })
// or AnySignal(childSignal, transform: { $0 as BaseProtocol })
// to be explicit.
]
但是,通过相同的
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 })
}
}
(并重复您要转换为的任何其他协议(protocol)类型)
现在你可以说:
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
,这两种情况下的过程相似。 .
关于Swift泛型强制的误解,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56757847/