Why isn't [SomeStruct] convertible to [Any]?
Asked Answered
S

2

16

Consider the following:

struct SomeStruct {}

var foo: Any!
let bar: SomeStruct = SomeStruct()

foo = bar // Compiles as expected

var fooArray: [Any] = []
let barArray: [SomeStruct] = []

fooArray = barArray // Does not compile; Cannot assign value of type '[SomeStruct]' to type '[Any]'

I've been trying to find the logic behind this, but with no luck. It's worth mentioning if you change the struct to a class, it works perfectly.

One could always add a workaround and map each object of the fooArray and cast them to type of Any, but that is not the issue here. I'm looking for an explanation on why this is behaving like it is.

Can someone please explain this?

This SO question led me to this problem.

Spillway answered 12/5, 2016 at 13:35 Comment(9)
Possible duplicate of Casting arrays to specific types in SwiftDumah
Consider what would happen if you added, say, a String to the array (there is only one).Sulphonamide
@Sulphonamide The assignment here can actually be fine in Swift because Arrays are value-type, so adding a String to fooArray won't affect barArray.Centigram
Probably a limit of the compiler. It doesn't support this assignment syntax yet. There is an easy workaround as @Dumah suggested.Elbertina
@Dumah That question is about downcasting an array to a more specific type, whereas this question is about upcasting an array to a given protocol that the elements already conform to.Soursop
@originaluser2 Nevertheless, the answer is pretty much the same and the compiler also don't tell one from other.Dumah
@Dumah Sure, but I would argue here that the compiler should be able to implicitly cast an array of a given struct to an array of a given protocol that all the elements conform to – but I would never argue that the compiler should be able to implicitly cast an array of Any down an array of Int. The solution may be similar, but the questions aren't. I'm not doubting the usefulness of the Q&A you linked, only the "possible duplicate" ;)Soursop
@originaluser2 I agree with you after your particular and very specific phrasing. Neither this nor the referenced question make such perfect distinction though.Dumah
@werediver, thank you for pitching in. As stated in my question, I wasn't interested in a fix. Just an explanation on why the code was invalid.Spillway
S
19

Swift 3 Update

As of Swift 3 (specifically the build that ships with Xcode 8 beta 6), collection types can now perform under the hood conversions from value-typed element collections to abstract-typed element collections.

This means that the following will now compile:

protocol SomeProtocol {}
struct Foo : SomeProtocol {}

let arrayOfFoo : [Foo] = []

let arrayOfSomeProtocol : [SomeProtocol] = arrayOfFoo
let arrayOfAny : [Any] = arrayOfFoo

Pre Swift 3

This all starts with the fact that generics in Swift are invariant – not covariant. Remembering that [Type] is just syntactic sugar for Array<Type>, you can abstract away the arrays and Any to hopefully see the problem better.

protocol Foo {}
struct Bar : Foo {}

struct Container<T> {}

var f = Container<Foo>()
var b = Container<Bar>()

f = b // error: cannot assign value of type 'Container<Bar>' to type 'Container<Foo>'

Similarly with classes:

class Foo {}
class Bar : Foo {}

class Container<T> {}

var f = Container<Foo>()
var b = Container<Bar>()

f = b // error: cannot assign value of type 'Container<Bar>' to type 'Container<Foo>'

This kind of covariant behaviour (upcasting) simply isn't possible with generics in Swift. In your example, Array<SomeStruct> is seen as a completely unrelated type to Array<Any> due to the invariance.

However, arrays have an exception to this rule – they can silently deal with conversions from subclass types to superclass types under the hood. However, they don't do the same when converting an array with value-typed elements to an array with abstract-typed elements (such as [Any]).

To deal with this, you have to perform your own element-by-element conversion (as individual elements are covariant). A common way of achieving this is through using map(_:):

var fooArray : [Any] = []
let barArray : [SomeStruct] = []

// the 'as Any' isn't technically necessary as Swift can infer it,
// but it shows what's happening here
fooArray = barArray.map {$0 as Any} 

A good reason to prevent an implicit 'under the hood' conversion here is due to the way in which Swift stores abstract types in memory. An 'Existential Container' is used in order to store values of an arbitrary size in a fixed block of memory – meaning that expensive heap allocation can occur for values that cannot fit within this container (allowing just a reference to the memory to be stored in this container instead).

Therefore because of this significant change in how the array is now stored in memory, it's quite reasonable to disallow an implicit conversion. This makes it explicit to the programmer that they're having to cast each element of the array – causing this (potentially expensive) change in memory structure.

For more technical details about how Swift works with abstract types, see this fantastic WWDC talk on the subject. For further reading about type variance in Swift, see this great blog post on the subject.

Finally, make sure to see @dfri's comments below about the other situation where arrays can implicitly convert element types – namely when the elements are bridgeable to Objective-C, they can be done so implicitly by the array.

Soursop answered 12/5, 2016 at 14:48 Comment(4)
Thank you for a detailed and perfectly understandable answer!Spillway
Since you mention the exception of conversion from an array of a subclass type (and instances) to an array of superclass type, I might add an additional exception: arrays of elements that conform to the internal protocol _ObjectiveCBridgeable (e.g. Int, implicitly bridged to and NSNumber type, UInt, Double, String and some more) are assignable directly to arrays of element type AnyObject (Array<AnyObject>), using, behind the hood, member-per-member implicitly bridged conversion from the native Swift types to corresponding Cocoa data types.Twedy
... see e.g. Interoperability - Working with Cocoa Data Types as well as this Q&A regarding the _ObjectiveCBridgeable protocol.Twedy
@dfri A very good point! I have edited my answer accordingly.Soursop
W
1

Swift can't automatically convert between array which holds value types and reference types. Just map the array to the type you need:

fooArray = barArray.map({ $0 }) // Does compile

Weldon answered 12/5, 2016 at 14:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.