Swift通用强制误解

我正在使用信号库。

假设我定义了符合BaseProtocol BaseProtocol协议和ChildClass

 protocol BaseProtocol {} class ChildClass: BaseProtocol {} 

现在我想存储像这样的信号:

 var signals: Array<Signal<BaseProtocol>> = [] let signalOfChild = Signal<ChildClass>() signals.append(signalOfChild) 

我收到错误:

Swift通用错误

但是,我可以写下一行没有任何编译器错误

 var arrays = Array<Array<BaseProtocol>>() let arrayOfChild = Array<ChildClass>() arrays.append(arrayOfChild) 

在这里输入图像描述

那么,通用Swift数组和genericsSignal有什么区别呢?

不同之处在于Array (和SetDictionary )得到了编译器的特殊处理,允许协变(我在这个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") 

没有任何问题,因为anyArrayanyArray副本 – 因此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中,你可以通过在Signaltypes的扩展中定义一个(方便的)初始化程序来实现这一点,其中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来说都是相似的。