A quick general misunderstanding of coercion

I am using Signals .

Let's say I defined the BaseProtocol protocol and ChildClass , which corresponds to BaseProtocol .

 protocol BaseProtocol {} class ChildClass: BaseProtocol {} 

Now I want to store signals such as:

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

I get an error:

Quick general mistake

But I can write the following lines without any compiler error:

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

enter image description here

So what is the difference between a common Swift Array and a common signal?

+4
generics swift
source share
1 answer

The difference is that Array (and Set and Dictionary ) receive special processing from the compiler, given covariance (I get into this a bit more into this Q & A ).

However, arbitrary generic types are invariant, therefore, in the eyes of the system type, Signal<ChildClass> and Signal<BaseProtocol> considered as completely unrelated types, although ChildClass can be converted to BaseProtocol (see also this Q & A ).

One reason for this is that it completely destroys the common reference types that define contravariant things (such as method parameters and property definition tools) with respect to T

For example, if you implemented Signal as:

 class Signal<T> { var t: T init(t: T) { self.t = t } } 

If you could say:

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

you could say:

 signalAny.t = "wassup" 

which is completely wrong, since you cannot assign String to the Int property.

The reason that such a thing is safe for Array is because it is a value type - this way when you do:

 let intArray = [2, 3, 4] var anyArray : [Any] = intArray anyArray.append("wassup") 

There is no problem, since anyArray is a copy of intArray - therefore the contravariance of append(_:) not a problem.

However, this cannot be applied to arbitrary generic value types, since value types can contain any number of reference types, which leads us to the dangerous way of resolving an illegal operation for common reference types that define contra-things.


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

If we look at an 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()) 

An eraser type that wraps any instance of Signal<T> , where T matches BaseProtocol , might 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)] 

Now we will talk about heterogeneous Signal types, where T is some type that matches BaseProtocol .

However, one problem with this shell is that we are limited to conversations in terms of BaseProtocol . What if we had AnotherProtocol and wanted an eraser type for Signal instances, where T corresponds to AnotherProtocol ?

One solution to this is to pass the transform function to an eraser type, which allows us to do arbitrary enhancements.

 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 speak in terms of Signal heterogeneous types, where T is some type that is converted to some U , which is set when creating the eraser type.

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

However, passing the same transform function to each initializer is a bit cumbersome.

In Swift 3.1 (available with beta version of Xcode 8.3) you can remove this load from the caller by specifying your own initializer specifically for BaseProtocol in the extension:

 extension AnySignal where T == BaseProtocol { init<U : BaseProtocol>(_ base: Signal<U>) { self.init(base, transform: { $0 }) } } 

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

Now you can simply say:

 let signals: [AnySignal<BaseProtocol>] = [ AnySignal(childSignal), AnySignal(anotherSignal) ] 

(In fact, you can remove the explicit type annotation for the array here, and the compiler will read it [AnySignal<BaseProtocol>] - but if you intend to use more convenient initializers, I would save it explicitly)


The solution for value types or reference types in which you want to specifically create a new instance is to convert from Signal<T> (where T corresponds to BaseProtocol ) to Signal<BaseProtocol> .

In Swift 3.1, you can do this by specifying an initializer (convenience) in the 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 using the 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 will be the same for a struct .

+5
source share

All Articles