Swift泛型强制转换误解

15
我将使用Signals库。
假设我定义了BaseProtocol协议和遵循BaseProtocol的ChildClass。
(注:原文中的link1为超链接,翻译时应将其保留为html标签)
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 有什么区别?


1
谢谢,现在我明白了。不幸的是,我不能把这个评论作为答案接受。 - romanilchyshyn
1
我继续写了一个回答 :) - Hamish
1个回答

20

区别在于编译器会特殊处理Array(以及SetDictionary),允许协变(我稍微详细地介绍了一下这个Q&A)。

然而,任意泛型类型是不变的,也就是说,如果T != U,则X<T>是与X<U>完全无关的类型——TU之间的任何其他类型关系(如子类型化)都是无关紧要的。应用于您的情况,即使ChildClassBaseProtocol的子类型(也请参阅此问题解答),Signal<ChildClass>Signal<BaseProtocol>也是无关的类型。

这是因为这将完全破坏泛型引用类型的定义,这些类型针对 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(_:)的逆变性不是问题。

然而,这不能应用于任意泛型值类型,因为值类型可以包含任意数量的泛型引用类型,这将使我们回到允许泛型引用类型定义逆变事物的非法操作的危险道路上。


正如Rob在他的回答中所言,对于参考类型,如果您需要保持对相同基础实例的引用,则解决方案是使用类型擦除器。

如果我们考虑下面的例子:

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

一个类型擦除器,可以包装任何符合BaseProtocolTSignal<T>实例,可能会像这样:
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 并想要一个类型擦除器来处理符合 AnotherProtocolSignal 实例,怎么办?
解决方法之一是将一个 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 })
    }
}

现在,您只需要说:

(并为您想要转换的任何其他协议类型重复此操作)

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,程序流程将是相似的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接