Reputation: 5119
I've run into a bit of an issue with generics in Swift. I have a generic struct:
struct MyStruct<T> { ... }
And I want to store it on a collection (in this case a dictionary):
var myStructDict = [MyKeyType: MyStruct]()
You'll notice I didn't specify the type of T
for MyStruct
. That is because I'd like to store any MyStruct
, independent of the type T.
Fine, you say, use Any
for T
([MyKeyType: MyStruct<Any>]
). Yes, but I'd also like to keep the original type information of T
of each struct when I get the struct from the dictionary so that when a function that takes a MyStruct
is called, it is called with the right type of T
.
Here's an example:
// Setup
var myStructDict: [String: MyStruct<Any>]
func f(v: MyStruct<String>) { ... }
func f(v: MyStruct<Int>) { ... }
// Set value
let s1 = MyStruct<String>(...)
myStructDict["key"] = unsafeTypeCast(s, MyStruct<Any>.self)
// Get value
let s2 = myStructDict["key"]
// Call function
f(s2) // I want the function that takes a `MyStruct<String>` to be called
I guarantee that there'll be a function for every type T
will take.
I could use a switch statement with every type allowed, like so:
switch s1 {
case let v as MyStruct<String>: f(v)
...
}
But that is not at all a good idea, because Any
, well..., is any type.
Upvotes: 3
Views: 1397
Reputation: 126177
Generic types don't unify in Swift. MyStruct<Int>
and MyStruct<String>
are completely different types, as much so as Int
and String
are, no matter how similar they look. This is one of the attributes of a type preserving generics system (as opposed to a type erasing generics system like you find in, say, Java).
Different kinds of generics systems can be useful for different things. Type preserving generics are best when you start out specialized and work your way to more generic code — i.e. write specific types that conform to protocols and then generic functions or types that make use of those protocols, or use generics to create wrapper types that can contain values of other types without caring what those types are.
On the other hand, use cases where you need to work back from a generic type to the specialized thing inside, or try to create generalizations of related generic types, can be less than straightforward.
What you're looking for needs two parts:
MyStruct<String>
and MyStruct<Int>
have a common type ancestor, so you can declare said type as the element type in a dictionaryMyStruct
once you pull it out of the dictionaryDon't generalize the type parameters, generalize the whole type: That is, don't make a dictionary of [MyKeyType: MyStruct<Any>]
, make a dictionary of [MyKeyType: MyValueType]
, where MyValueType
is a protocol to which both MyStruct<Int>
and MyStruct<String>
conform. (You could use Any
for your value type, but then you'd get things in your dictionary that aren't MyStruct
s.) For example:
protocol MyType {
func doNothing()
}
struct MyStruct<T>: MyType {
let thing: T
func doNothing() {}
}
let a = MyStruct(thing: "Hello")
let b = MyStruct(thing: 1)
let dict: [String: MyType] = ["a": a, "b": b]
There's an oddity here in the doNothing
function. We can't let the member thing
be the protocol requirement that separates MyValueType
from Any
, because that member's type is generic — and a protocol with associated-type requirements can't be used as a concrete type (e.g. as the declaration of a dictionary's element type). Presumably your real MyStruct
does something more than just hold a generic thing
, so perhaps you can use some other distinctive feature of the type to create a protocol that only it conforms to.
I guarantee that there'll be a function for every type
T
will take.
Say that all you want, but the compiler has its fingers in its ears and isn't listening. In other words, this is a "guarantee" that you can't enforce in the language, so Swift won't let you do things that proceed from that assumption.
Instead, you'll need a dispatch mechanism for reifying the specialization from the generic type through a cast before calling your functions f
:
func f(v: MyStruct<String>) { print("String \(v)") }
func f(v: MyStruct<Int>) { print("Int \(v)") }
func f(v: MyType) {
switch v {
case let str as MyStruct<String>:
f(str)
case let num as MyStruct<Int>:
f(num)
default:
fatalError("unsupported type")
break
}
}
The default case is where your "guarantee" comes into play. There's no way to make this switch
exhaustive as far as the language is concerned, so you need a runtime test to make sure you haven't tried to call f
on more types than you claim to have defined.
So, indeed, your guess of needing a switch
was correct. But it is a good idea, because it (or something similar) is your only way of disambiguating your generic types. You can at least restrict it to needing to handle your MyType
protocol instead of Any
though.
Upvotes: 8