2017-02-01 29 views
4

我正在使用Signals庫。Swift通用強制誤解

比方說,我定義了BaseProtocol協議和ChildClass,它符合BaseProtocol

protocol BaseProtocol {} 
class ChildClass: BaseProtocol {} 

現在我想要存儲類似的信號:

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

我得到錯誤:

Swift generic error

但我可以寫下一行沒有任何編譯器錯誤:

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

enter image description here

那麼,通用Swift數組和泛型Signal有什麼區別呢?

+0

謝謝,現在我明白了。不幸的是,我不能接受這個評論作爲答案。 – romanilchyshyn

+0

我繼續寫下了答案:) – Hamish

回答

5

不同的是,Array(和SetDictionary)從編譯器獲得特殊待遇,允許協方差(我進入這個稍微更詳細in this Q&A)。

然而,任意泛型類型是不變的 - 因此在類型系統的眼睛,Signal<ChildClass>Signal<BaseProtocol>被視爲完全無關的類型,即使ChildClass可轉換爲BaseProtocol(見this Q&A)。

這樣做的一個原因是它會完全破壞泛型引用類型,這些引用類型相對於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安全的是,這是一個價值型 - 因此,當你這樣做:

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()) 

A型的橡皮擦,它包裝任何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函數傳遞給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方面講,其中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測試版),您可以通過一個擴展,專門定義自己的初始化器爲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>] - 但如果你要允許更多conven ience initialisers,我會保持它明確)


值類型的解決方案,或者要特別創建一個新的實例引用類型,來爲Signal<T>執行轉換(其中T符合BaseProtocol)至Signal<BaseProtocol>

在夫特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) 
] 

預夫特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