Swift generic coercion misunderstanding

2019-01-01 11:22发布

问题:

I\'m using Signals library.

Let\'s say I defined BaseProtocol protocol and ChildClass which conforms BaseProtocol.

protocol BaseProtocol {}
class ChildClass: BaseProtocol {}

Now I want to store signals like:

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

I get error:

\"Swift

But I can write next lines without any compiler error:

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

\"enter

So, what the difference between generic Swift Array and generic Signal?

回答1:

The difference is that Array (and Set and Dictionary) get special treatment from the compiler, allowing for covariance (I go into this in slightly more detail in this Q&A).

However arbitrary generic types are invariant, meaning that X<T> is a completely unrelated type to X<U> if T != U – any other typing relation between T and U (such as subtyping) is irrelevant. Applied to your case, Signal<ChildClass> and Signal<BaseProtocol> are unrelated types, even though ChildClass is a subtype of BaseProtocol (see also this Q&A).

One reason for this is it would completely break generic reference types that define contravariant things (such as function parameters and property setters) with respect to T.

For example, if you had implemented Signal as:

class Signal<T> {

    var t: T

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

If you were able to say:

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

you could then say:

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

which is completely wrong, as you cannot assign a String to an Int property.

The reason why this kind of thing is safe for Array is that it\'s a value type – thus when you do:

let intArray = [2, 3, 4]

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

there are no problems, as anyArray is a copy of intArray – thus the contravariance of append(_:) is not a problem.

However, this cannot be applied to arbitrary generic value types, as value types can contain any number of generic reference types, which leads us back down the dangerous road of allowing an illegal operation for generic reference types that define contravariant things.


As Rob says in his answer, the solution for reference types, if you need to maintain a reference to the same underlying instance, is to use a type-eraser.

If we consider the example:

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 type-eraser that wraps any Signal<T> instance where T conforms to BaseProtocol could look like this:

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

This now lets us talk in terms of heterogenous types of Signal where the T is some type that conforms to BaseProtocol.

However one problem with this wrapper is that we\'re restricted to talking in terms of BaseProtocol. What if we had AnotherProtocol and wanted a type-eraser for Signal instances where T conforms to AnotherProtocol?

One solution to this is to pass a transform function to the type-eraser, allowing us to perform an arbitrary 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) }
    }
}

Now we can talk in terms of heterogenous types of Signal where T is some type that\'s convertible to some U, which is specified at the creation of the type-eraser.

let signals: [AnySignal<BaseProtocol>] = [
    AnySignal(childSignal, transform: { $0 }),
    AnySignal(anotherSignal, transform: { $0 })
    // or AnySignal(childSignal, transform: { $0 as BaseProtocol })
    // to be explicit.
]

However, the passing of the same transform function to each initialiser is a little unwieldy.

In Swift 3.1 (available with Xcode 8.3 beta), you can lift this burden from the caller by defining your own initialiser specifically for BaseProtocol in an extension:

extension AnySignal where T == BaseProtocol {

    init<U : BaseProtocol>(_ base: Signal<U>) {
        self.init(base, transform: { $0 })
    }
}

(and repeat for any other protocol types you want to convert to)

Now you can just say:

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)


The solution for value types, or reference types where you want to specifically create a new instance, to is perform a conversion from Signal<T> (where T conforms to BaseProtocol) to Signal<BaseProtocol>.

In Swift 3.1, you can do this by defining a (convenience) initialiser in an extension for Signal types where 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, this can be achieved with an instance method:

extension Signal where T : BaseProtocol {
    func asBaseProtocol() -> Signal<BaseProtocol> {
        return Signal<BaseProtocol>(t: t)
    }
}

// ...

let signals: [Signal<BaseProtocol>] = [
    childSignal.asBaseProtocol(),
    anotherSignal.asBaseProtocol()
]

The procedure in both cases would be similar for a struct.