Swift通用强制误解
我正在使用信号库。
假设我定义了符合BaseProtocol
BaseProtocol协议和ChildClass
。
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数组和genericsSignal有什么区别呢?
不同之处在于Array
(和Set
和Dictionary
)得到了编译器的特殊处理,允许协变(我在这个Q&A中稍微详细地介绍了这一点 )。
然而,任意genericstypes是不变的 – 因此在types系统的眼中,即使ChildClass
可以转换为BaseProtocol
(请参阅本Q&A ), Signal<ChildClass>
和Signal<BaseProtocol>
也被视为完全不相关的types。
其中一个原因是它会完全破坏generics引用types,它定义了关于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"
这是完全错误的,因为您不能将一个String
分配给一个Int
属性。
这种事情对Array
来说是安全的原因是它是一个值types – 因此当你这样做的时候:
let intArray = [2, 3, 4] var anyArray : [Any] = intArray anyArray.append("wassup")
没有任何问题,因为anyArray
是anyArray
的副本 – 因此append(_:)
的intArray
不成问题。
然而,这不能应用于任意的通用值types,因为值types可以包含任意数量的通用引用types,这导致我们回避允许对定义逆变事物的通用引用types进行非法操作的危险之路。
正如Rob在他的回答中所说的 ,如果您需要维护对相同底层实例的引用,则参考types的解决scheme是使用types擦除器。
如果我们考虑这个例子:
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())
包装任何T
符合BaseProtocol
Signal<T>
实例的types擦除器可能如下所示:
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
的不同types,其中T
是符合BaseProtocol
某种types。
然而,这个包装的一个问题是,我们被限制在BaseProtocol
方面的BaseProtocol
。 如果我们有AnotherProtocol
并且想要一个符合AnotherProtocol
Signal
实例的types擦除器呢?
解决这个问题的方法之一就是将transform
函数传递给type-eraser,使我们能够执行任意的upcast。
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
的不同types,其中T
是可以转化为某个U
某种types,这是在创buildtypes橡皮擦时指定的。
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
的初始化程序来BaseProtocol
调用者的负担:
extension AnySignal where T == BaseProtocol { init<U : BaseProtocol>(_ base: Signal<U>) { self.init(base, transform: { $0 }) } }
(并重复您想要转换的任何其他协议types)
现在你可以说:
let signals: [AnySignal<BaseProtocol>] = [ AnySignal(childSignal), AnySignal(anotherSignal) ]
(实际上你可以在这里删除数组的显式types注释,编译器会推断它是[AnySignal<BaseProtocol>]
,但是如果你想允许更多的方便初始化,我会保持它的显式)
值types或要创build新实例的引用types的解决scheme是执行从Signal<T>
(其中T
符合BaseProtocol
)到Signal<BaseProtocol>
。
在Swift 3.1中,你可以通过在Signal
types的扩展中定义一个(方便的)初始化程序来实现这一点,其中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) ]
Pre 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
来说都是相似的。