In Swift, I notice that I can upcast an object that conforms to a protocol called, let's say SubProtocol
to another protocol called SuperProtocol
which is a super protocol of SubProtocol
. But I can't do the same with an array of the protocol. Here's the example code that I ran in Playground:
protocol SuperProtocol {
}
protocol SubProtocol: SuperProtocol {
}
class MyObject: SubProtocol {
}
let value1: SubProtocol = MyObject()
let value2: SuperProtocol = value1 // No error here. Upcasting works.
let array1: [SubProtocol] = [MyObject()]
let array2: [SuperProtocol] = array1 // Error here "Cannot convert value of type '[SubProtocol]' to specified type '[SuperProtocol]'"
This seems counter-intuitive, and I'm wondering why it's not allowed.
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.
Try this code - just checked, works fine
let array2: [SuperProtocol] = array1.map { $0 as SuperProtocol }