Swift Generics & Upcasting

Swift Generics & Upcasting

Swift generics are not covariant. That is to say, exactly what the error says: you can't automatically say a Basket<Apple> is a kind of Basket<Fruit> even if Apple is a kind of Fruit. There is good reason for this.

Consider the following code:

class Fruit {}
class Apple: Fruit {}
class Orange: Fruit {}

class Basket<T: Fruit> {
private var items: [T]
func add(item: T) {
items.append(item)
}
init() {}
}

func addItem<T: Fruit>(var basket: Basket<T>, item: T) {
basket.add(item)
}

let basket:Basket<Apple> = Basket()

addItem(basket as Basket<Fruit>, Orange())

This would be legal code if Basket<Apple> were considered a Basket<Fruit>, and I'd be allowed to add an orange to a basket of apples.

Generic method to upcast protocol implementer to protocol in Swift

You can try something like:

// This is your function
func upcast<T, U>(instance: T) -> U? {
return instance as? U
}

// This is what you can use to upcast sequence into an array
func flatUpcast<S: SequenceType, T, U where S.Generator.Element == T>(sequence: S) -> [U] {
return sequence.flatMap() {
upcast($0)
}
}

// Playground try-out
protocol Drawable { }

struct Shape: Drawable { }

let shape = Shape()
let shapes = [shape, shape, shape]
let drawables: [Drawable] = flatUpcast(shapes)

Upcast non-generic ObjC Classes to parent class for Swift method parameter

You're parameterizing on Parent rather than Type, which the thing you're actually changing. You mean:

func callFunc<T>(parent: Parent<T>) { ... }

While you could explicitly call out T: Type, this isn't really necessary, since Parent already enforces that.

Using Generics in completionHandler

Swift generics are not covariant (with special hard-coded exceptions for Array which involve copying the elements). That means that Result<Apple> is not a subtype of Result<Fruit>. See Swift Generics & Upcasting for examples of why.

In your case, what would prevent you from passing a Result<MessageBody> to a callback that expected a Result<MessageAck>? For example:

for callback in callbacks {
callback(result)
}

How could you know this was legal at compile time for any given type of result?

EDIT (BETTER ANSWER):

You can hide the type inside a closure to get what you want. Try this:

class SocketAPIClient: APIClient {
typealias MessageId = String
private var callbacks = [Receipt: ((Result<SocketMessage>) -> Void)]() // <--- Change

func send<T>(request: SocketAPIRequest, completion: ((Result<T>) -> Void)?) where T : ResponseType {

// Store the closure we don't understand inside a closure we do
callbacks[stompFrame.receiptId] = { result in
switch result {
case .success(let message):
completion?(.success(T.init(with: message)))
case .failure(let error):
completion?(.failure(error))
}
}
}
}

Now, instead of trying to hold T directly in callbacks, it's held in each individual closure, hidden from the rest of the class, and T never escapes this function. When you get to wherever you call callback in your code, just pass it the Result<SocketMessage> that I assume you already have somewhere.


OLD ANSWER:

The simplest solution to your problem is to have the callback always pass a Result<Data> and remove T entirely:

protocol APIClient {
func send(request: SocketAPIRequest, completion: ((Result<Data>) -> Void)?)
}

Then leave it to the MessageAck (in the completion handler) to deserialize itself from the raw data.

There are other ways to achieve all this with type erasers, but they're much more complex and sometimes very fiddly.

Swift - upcasting array of protocol to array of super protocol causes error

The reason has to do with how protocols inherit differently from classes.

Consider first that protocols can have default implementations, for example:

protocol MammalLocomotion {
func legs() -> Int
}

extension MammalLocomotion {
func legs () -> Int {
return 2
}
}

protocol CowLocomotion : MammalLocomotion {

}

extension CowLocomotion {
func legs () -> Int {
return 4
}
}

Let's make classes that conform to these protocols:

class Mammal : MammalLocomotion {

}

class Cow : Mammal, CowLocomotion {

}

let mammal = Mammal()
let cow = Cow()

Their legs() methods respond as we'd expect:

mammal.legs() // 2
cow.legs() // 4

But now let's cast cow to Mammal:

let cowAsMammal : Mammal = cow

cowAsMammal.legs() // 2

cow had 4 legs, but now it has 2. This is because, with protocols, the currently known type determines which default implementation is used. So casting the array doesn't work — I think the reasoning is that it would be unexpected for an array cast to alter its contained objects' behavior.

Workaround

As you've noted, this won't work:

let farm : [CowLocomotion] = [Cow(), Cow(), Cow()]
let mammalFarm : [MammalLocomotion] = farm // doesn't work

If you want, you can work around this limitation by mapping the array to the protocol you want:

let farm = [Cow(), Cow(), Cow()]

farm.forEach { print($0.legs()) } // prints 4, 4, 4

let mammalFarm = farm.map { $0 as MammalLocomotion }

mammalFarm.forEach { print($0.legs()) } // prints 2, 2, 2

More information on how protocols inherit is available in the Protocol-Oriented Programming in Swift session from this year's WWDC - transcript here.

Why can't Kotlin implicitly upcast Array of String into Array of Any?

You can't safely cast a type to a supertype or subtype like this, because an Array<String> does not qualify as an Array<Any>. If you tried to put a Double in your supposed Array<Any>, it would throw an exception, because the actual type is a String array. Arrays are a special case of generics that don't have type erasure, so they are locked to the type they were instantiated with.

What you could do is cast it to an Array<out Any>, because you can safely use it that way. You can pull Strings out of the array and they qualify as instances of Any. The reverse is not true. You can't put any instance of Any and its subclasses into a String array.

When it comes to List, you can cast it to a List with the supertype type and you don't even have to manually cast it. It's safe so the cast can be done implicitly.

val s = listOf("some string")
val upcasted: List<Any> = s // implicit cast

So why don't you have to cast to List<out Any>? The List interface doesn't have any functions that allow you to add stuff to it. It is defined with an out-projected type in its declaration, so when you type List<Any> it is already the same thing as a List<out Any>

If you try to do this with a MutableList, which accepts putting items into it and is not defined with an out-projected type, then you will run into the same warning as you did with the array.

val s = mutableListOf("some string")
val upcasted: MutableList<Any> = s as MutableList<Any> // warning here and implicit cast impossible

The difference between this and the Array is that there is type-erasure, so you won't have a runtime exception if you try to add some non-String to this list. But there is the possibility of hidden bugs if you are not careful, hence the warning.



Related Topics



Leave a reply



Submit