Swift 泛型强制转换的误解

2023-12-29

我在用着Signals https://github.com/artman/Signals图书馆。

假设我定义了 BaseProtocol 协议并且ChildClass符合BaseProtocol.

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 Array 和通用 Signal 有什么区别呢?


不同之处在于Array (and Set and Dictionary)从编译器获得特殊待遇,允许协变(我对此进行了更详细的介绍在本次问答中 https://stackoverflow.com/q/37188580/2976878).

然而任意泛型类型是不变的 https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science), 意思是X<T>是一个完全不相关的类型X<U> if T != U– 之间的任何其他类型关系T and U(例如子类型)是无关紧要的。应用到你的案例中,Signal<ChildClass> and Signal<BaseProtocol>是不相关的类型,即使ChildClass是一个子类型BaseProtocol(也可以看看本次问答 https://stackoverflow.com/q/38590548/2976878).

原因之一是它会完全破坏定义逆变事物(例如函数参数和属性设置器)的通用引用类型T.

例如,如果您已经实施了Signal as:

class Signal<T> {

    var t: T

    init(t: T) {
        self.t = t
    }
}

If你可以说:

let signalInt = Signal(t: 5)
let signalAny: Signal<Any> = signalInt

然后你可以说:

signalAny.t = "wassup" // assigning a String to a Signal<Int>'s `t` property.

这是完全地错误的,因为你不能分配String to an Int财产。

这种东西之所以安全Array是它是一个值类型 - 因此当你这样做时:

let intArray = [2, 3, 4]

var anyArray : [Any] = intArray
anyArray.append("wassup")

没有问题,因为anyArray is a copy of intArray– 因此逆变append(_:)不是问题。

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


正如罗布所说 https://stackoverflow.com/a/38591285/2976878在他的回答中,如果您需要维护对同一底层实例的引用,则引用类型的解决方案是使用类型擦除器。

如果我们考虑这个例子:

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?

解决这个问题的一种方法是通过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 where 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)
]

(You can actually remove the explicit type annotation for the array here, and the compiler will infer it to be [AnySignal<BaseProtocol>] – but if you're going to allow for more convenience initialisers, I would keep it explicit)


针对值类型或您想要专门处理的引用类型的解决方案create一个新实例,要执行转换 from Signal<T> (where T符合BaseProtocol) to 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.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Swift 泛型强制转换的误解 的相关文章

随机推荐