斯威夫特通用强制误解

时间:2017-02-01 10:02:40

标签: swift generics

我正在使用Signals库。

假设我定义了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数组和通用信号之间的区别是什么?

1 个答案:

答案 0 :(得分:9)

区别在于Array(以及SetDictionary)得到了编译器的特殊处理,允许协方差(我稍微详细一点in this Q&A )。

然而,任意泛型类型为invariant,这意味着如果X<T> X<U>T != U之间的任何其他类型关系,则TU完全无关。 Signal<ChildClass>(例如子类型)无关紧要。适用于您的案例,Signal<BaseProtocol>ChildClass是不相关的类型,即使BaseProtocol是[{1}}的子类型(另请参阅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" // 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")

没有问题,因为anyArrayintArray副本 - 因此append(_:)的逆转不是问题。

但是,这不能应用于任意通用值类型,因为值类型可以包含任意数量的通用引用类型,这使我们回到了允许对定义逆变事物的泛型引用类型进行非法操作的危险道路。 / p>

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的{​​{1}}个实例,我们有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. ] 的类型,在创建类型时指定-eraser。

transform

然而,将相同的BaseProtocol函数传递给每个初始化函数有点笨拙。

在Swift 3.1(Xcode 8.3 beta版)中,您可以通过在扩展程序中专门为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执行转换(其中{ {1}}符合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)
]

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相似。