I've been playing around with arrays of generic classes with different types. It's easiest to explain my problem with some sample code:
// Obviously a very pointless protocol...
protocol MyProtocol {
var value: Self { get }
}
extension Int : MyProtocol { var value: Int { return self } }
extension Double: MyProtocol { var value: Double { return self } }
class Container<T: MyProtocol> {
var values: [T]
init(_ values: T...) {
self.values = values
}
func myMethod() -> [T] {
return values
}
}
Now if I try to create an array of containers like so:
var containers: [Container<MyProtocol>] = []
I get the error:
Protocol 'MyProtocol' can only be used as a generic constraint because it has Self or associated type requirements.
To fix this I can use [AnyObject]
:
let containers: [AnyObject] = [Container<Int>(1, 2, 3), Container<Double>(1.0, 2.0, 3.0)]
// Explicitly stating the types just for clarity.
But now another 'problem' emerges when enumerating through containers
:
for container in containers {
if let c = container as? Container<Int> {
println(c.myMethod())
} else if let c = container as? Container<Double> {
println(c.myMethod())
}
}
As you can see in the code above, after determining the type of container
the same method is called in both cases. My question is:
Is there a better way to get the Container
with the correct type than casting to every possible type of Container
? Or is there something else I've overlooked?
There is a way - sort of - to do what you want - kind of. There is a way, with protocols, to eliminate the type restriction and still get the result that you want, kind of, but it isn't always pretty. Here is what I came up with as a protocol in your situation:
protocol MyProtocol {
func getValue() -> Self
}
extension Int: MyProtocol {
func getValue() -> Int {
return self
}
}
extension Double: MyProtocol {
func getValue() -> Double {
return self
}
}
Note that the value
property that you originally put in your protocol declaration has been changed to a method that returns the object.
That's not very interesting.
But now, because you've gotten rid of the value
property in the protocol, MyProtocol
can be used as a type, not just as a type constraint. Your Container
class doesn't even need to be generic anymore. You can declare it like this:
class Container {
var values: [MyProtocol]
init(_ values: MyProtocol...) {
self.values = values
}
func myMethod() -> [MyProtocol] {
return values
}
}
And because Container
is no longer generic, you can create an Array
of Container
s and iterate through them, printing the results of the myMethod()
method:
var containers = [Container]()
containers.append(Container(1, 4, 6, 2, 6))
containers.append(Container(1.2, 3.5))
for container in containers {
println(container.myMethod())
}
// Output: [1, 4, 6, 2, 6]
// [1.2, 3.5]
The trick is to construct a protocol that only includes generic functions and places no other requirements on a conforming type. If you can get away with doing that, then you can use the protocol as a type, and not just as a type constraint.
And as a bonus (if you want to call it that), your array of MyProtocol
values can even mix different types that conform to MyProtocol
. So if you give String
a MyProtocol
extension like this:
extension String: MyProtocol {
func getValue() -> String {
return self
}
}
You can actually initialize a Container
with mixed types:
let container = Container(1, 4.2, "no kidding, this works")
[Warning - I am testing this in one of the online playgrounds. I haven't been able to test it in Xcode yet...]
Edit:
If you still want Container
to be generic and only hold one type of object, you can accomplish that by making it conform to its own protocol:
protocol ContainerProtocol {
func myMethod() -> [MyProtocol]
}
class Container<T: MyProtocol>: ContainerProtocol {
var values: [T] = []
init(_ values: T...) {
self.values = values
}
func myMethod() -> [MyProtocol] {
return values.map { $0 as MyProtocol }
}
}
Now you can still have an array of [ContainerProtocol]
objects and iterate through them invoking myMethod()
:
let containers: [ContainerProtocol] = [Container(5, 3, 7), Container(1.2, 4,5)]
for container in containers {
println(container.myMethod())
}
Maybe that still doesn't work for you, but now Container
is restricted to a single type, and yet you can still iterate through an array of ContainterProtocol
objects.