How to allow a generic collection to perform under the hood conversion in swift
It has always been impossible.
Swift generics are normally invariant, but the Swift standard library
collection types — even though those types appear to be regular
generic types — use some sort of magic inaccessible to mere mortals
that lets them be covariant.
Potential workaround. ♂️
protocol Subclass: AnyObject {
associatedtype Superclass
}
extension B: Subclass {
typealias Superclass = A
}
class SpecialSet<Object>: ExpressibleByArrayLiteral {
required init(arrayLiteral _: Object...) { }
init<T: Subclass>(_: SpecialSet<T>) where T.Superclass == Object { }
}
let specialSetB: SpecialSet = [B(), B()]
let specialSetA = SpecialSet<A>(specialSetB)
Why isn't [SomeStruct] convertible to [Any]?
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.
Putting two generic Arrays into one Swift Dictionary with Generics
Elaborating on the answer from @RobNapier, here is a similar approach that uses enum
for both, keys and values of the dictionary:
enum Key: Equatable, Hashable {
case IntKey(Int)
case StringKey(String)
var hashValue: Int {
switch self {
case .IntKey(let value) : return 0.hashValue ^ value.hashValue
case .StringKey(let value) : return 1.hashValue ^ value.hashValue
}
}
init(_ value: Int) { self = .IntKey(value) }
init(_ value: String) { self = .StringKey(value) }
}
func == (lhs: Key, rhs: Key) -> Bool {
switch (lhs, rhs) {
case (.IntKey(let lhsValue), .IntKey(let rhsValue)) : return lhsValue == rhsValue
case (.StringKey(let lhsValue), .StringKey(let rhsValue)) : return lhsValue == rhsValue
default: return false
}
}
enum Value {
case IntValue(Int)
case StringValue(String)
init(_ value: Int) { self = .IntValue(value) }
init(_ value: String) { self = .StringValue(value) }
}
var dict = [Key: Value]()
dict[Key(1)] = Value("One")
dict[Key(2)] = Value(2)
dict[Key("Three")] = Value("Three")
dict[Key("Four")] = Value(4)
Casting type conforming to multiple protocols as a single protocol
Swift cant perform a full collection type conversion (only available for some behind-the-hood automatically Objective-C-bridgeable objects, or between collections of super- and subclass elements) where the elements of the collection themselves are associated in the sense that one can be assigned to the other. You need to explicitly help out the compiler to show that element-by-element conversion is valid, e.g. using a .map
operation prior to calling printNames
printNames(identifiableAndNamableItems.map{ $0 })
/* 0: Jeff
1: Fido */
Note also that you needn't go all out with multiple protocols to see this behaviour; it is likewise apparent for e.g. the following more minimal example
protocol Foo { }
struct Bar: Foo {}
let bar = Bar()
let foo: Foo = bar // ok
let barArr: [Bar] = [Bar(), Bar()]
let fooArr: [Foo] = barArr // cannot convert value of type '[Bar]' to specified type '[Foo]'
// let fooArr: [Foo] = barArr.map{ $0 } // OK
Value of type 'T' cannot be converted to
Even though it's inside of an if
block, the compiler doesn't know that T
is string
.
Therefore, it doesn't let you cast. (For the same reason that you cannot cast DateTime
to string
)
You need to cast to object
, (which any T
can cast to), and from there to string
(since object
can be cast to string
).
For example:
T newT1 = (T)(object)"some text";
string newT2 = (string)(object)t;
How to create a type that either hold an `ArrayInt` or `UnsafePointerUInt8`
OK, it’s not pretty but here is a generic function that will work on any kind of collection, which means you can pass in either an Array
, or an UnsafeMutableBufferPointer
, which means you can use it on a malloc’d memory range, or using the array’s .withUnsafeMutableBufferPointer
.
Unfortunately, some of the necessities of the generic version make it slightly less efficient than the non-generic version when used on an array. But it does show quite a nice performance boost over arrays in -O
when used with a buffer:
func storePrimes<C: MutableCollectionType where C.Generator.Element: IntegerType>(inout store: C) {
if isEmpty(store) { return }
var candidate: C.Generator.Element = 3
var primeCount = store.startIndex
store[primeCount++] = 2
var isPrime: Bool
while primeCount != store.endIndex {
isPrime = true
var oldPrimeCount = store.startIndex
for oldPrime in store {
if oldPrimeCount++ == primeCount { break }
if candidate % oldPrime == 0 { isPrime = false; break }
if candidate < oldPrime &* oldPrime { isPrime = true; break }
}
if isPrime { store[primeCount++] = candidate }
candidate = candidate.advancedBy(2)
}
}
let totalCount = 2_000_000
var primes = Array<CInt>(count: totalCount, repeatedValue: 0)
let startTime = CFAbsoluteTimeGetCurrent()
storePrimes(&primes)
// or…
primes.withUnsafeMutableBufferPointer { (inout buffer: UnsafeMutableBufferPointer<CInt>) -> Void in
storePrimes(&buffer)
}
let now = CFAbsoluteTimeGetCurrent()
let totalTime = now - startTime
println("Total time: \(totalTime), per second: \(Double(totalCount)/totalTime)")
CustomStringConvertible in enum
TL;DR - It doesn't work because an array of Attribute
s cannot be assigned to an array of String
s, they are both mismatched types, and Swift does not do automatic conversion between types, and an explict conversion needs to be specified.
In Swift, when you initialise an array using an array literal, the following happens under the hood:
let words = ["hello", "world"]
- The compiler recognises that an array literal is being assigned to a variable named
words
. Since we have not specified the type of thewords
, implicitly an array is assumed. The type of the elements underlying the array is determined based on the contents of the array literal. - In this case, the array literal is a collection of
String
types; this is easily understood by the compiler - Since the LHS type is an array, the RHS structure is an array literal, and since the LHS type (
Array
) conforms to a pre-defined protocol calledExpressibleByArrayLiteral
which has an associated type constraint to matchElement
, the compiler will actually be converting our line to the following
Example:
let words = [String].init(arrayLiteral: ["hello", "world"]) // we do not call this init directly
This is how initialising with array literals work. In the above example, since we did not specify the type of array, the implicit type setting will work. If we specified a mismatched type, the assignment will fail, since ExpressibleByArrayLiteral
requires the associated Element
type of the array literal and the actual array you are assigning to to match.
So the following fails:
let words:[String] = [1, 2] // array literal has Element=Int, array has Element=String
This also shows that there is no implicit type conversion between Int
and String
, even though Int
conform to CustomStringConvertible
.
In your case, you are trying to assign an array literal consisting of Attributes
to an array of String
. This is a type mismatch. This is the reason it fails.
If you state protocol conformance, the following line will work:
var attributesList: [CustomStringConvertible] {
return [
Attributes.eventDate,
Attributes.eventName,
Attributes.eventType,
Attributes.country
]
}
// note that we have an array of CustomStringConvertible protocol,
// each element here is still of Attributes type
// a type conforming to a protocol can be cast to an instance
// of that protocol automatically
// The above initialisation works simply because the following
// also works without any further action required
// let a:CustomStringConvertible = Attributes.country
If you really want a list of string values, you need to map this to a string explicitly:
var attributesList1: [String] {
return [
Attributes.eventDate,
Attributes.eventName,
Attributes.eventType,
Attributes.country
].map { $0.description }
}
var attributesList2: [String] {
return [
Attributes.eventDate.description,
Attributes.eventName.description,
Attributes.eventType.description,
Attributes.country.description
]
}
Related Topics
Ambiguous Use of Recover Error While Using Promisekit
Lldb for Swift: Access Computed Property or Perform Function Call in Type Summary Python Script
How to Disable a Button of a Nstoolbar of Macos X in Swift
Is There a Difference Between Global Initialization Vs Viewdidload Initialization in Swift
How to Specify The Name of The Output Executable
<Unknown>:0: Error: Opening Import File for Module 'swift': Not a Directory
Implement an Equatable Void (None) Type
Swift Optionals Best Practices
How to Send Multiple Buttons in Button.Addtarget Action? Swift3
Convert Ble Current Time to Date
How to Reconnect Akplayer and Akmixer After Audiokit.Stop()
How to Create Nested Dictionary Elements in Swift
Swift How to Make List Start at The Top with No Padding
Upload Multiple Images to Ftp Server in iOS
Cropping Cgrect from Avcapturephotooutput (Resizeaspectfill)
List Scroll Freeze on Catalyst Navigationview