Using a Type Variable in a Generic
Asked Answered
D

5

26

I have this question except for Swift. How do I use a Type variable in a generic?

I tried this:

func intType() -> Int.Type {
    return Int.self
}

func test() {
    var t = self.intType()
    var arr = Array<t>() // Error: "'t' is not a type". Uh... yeah, it is.
}

This didn't work either:

var arr = Array<t.Type>() // Error: "'t' is not a type"
var arr = Array<t.self>() // Swift doesn't seem to even understand this syntax at all.

Is there a way to do this? I get the feeling that Swift just doesn't support it and is giving me somewhat ambiguous error messages.

Edit: Here's a more complex example where the problem can't be circumvented using a generic function header. Of course it doesn't make sense, but I have a sensible use for this kind of functionality somewhere in my code and would rather post a clean example instead of my actual code:

func someTypes() -> [Any.Type] {
    var ret = [Any.Type]()
    for (var i = 0; i<rand()%10; i++) {
        if (rand()%2 == 0){ ret.append(Int.self) }
        else {ret.append(String.self) }
    }
    return ret
}

func test() {
    var ts = self.someTypes()

    for t in ts {
        var arr = Array<t>()
    }
}
Doyenne answered 18/6, 2015 at 1:25 Comment(0)
L
36

Swift's static typing means the type of a variable must be known at compile time.

In the context of a generic function func foo<T>() { ... }, T looks like a variable, but its type is actually known at compile time based on where the function is called from. The behavior of Array<T>() depends on T, but this information is known at compile time.

When using protocols, Swift employs dynamic dispatch, so you can write Array<MyProtocol>(), and the array simply stores references to things which implement MyProtocol — so when you get something out of the array, you have access to all functions/variables/typealiases required by MyProtocol.

But if t is actually a variable of kind Any.Type, Array<t>() is meaningless since its type is actually not known at compile time. (Since Array is a generic struct, the compiler needs know which type to use as the generic parameter, but this is not possible.)

I would recommend watching some videos from WWDC this year:

I found this slide particularly helpful for understanding protocols and dynamic dispatch:

Londoner answered 18/6, 2015 at 2:52 Comment(1)
Mmm, so this is the heart of the problem. I guess I should watch those videos then!Doyenne
G
7

There is a way and it's called generics. You could do something like that.

class func foo() {
    test(Int.self)
}

class func test<T>(t: T.Type) {
    var arr = Array<T>()
}

You will need to hint the compiler at the type you want to specialize the function with, one way or another. Another way is with return param (discarded in that case):

class func foo() {
    let _:Int = test()
}

class func test<T>() -> T {
    var arr = Array<T>()
}

And using generics on a class (or struct) you don't need the extra param:

class Whatever<T> {
    var array = [T]() // another way to init the array.
}

let we = Whatever<Int>()
Geostatics answered 18/6, 2015 at 2:10 Comment(2)
This would fix my example, but it doesn't work in general. What if you have a variable-size array of types you want to pass in? I actually have that, but I wanted to post a simple example. I'll add an example where generics won't work.Doyenne
Still a good answer though. This is helpful in the case that I know enough during compile-time. But I'm receiving types over a network connection and trying to build objects from them, so I'm looking for the totally general solution.Doyenne
S
5

jtbandes' answer - that you can't use your current approach because Swift is statically typed - is correct.

However, if you're willing to create a whitelist of allowable types in your array, for example in an enum, you can dynamically initialize different types at runtime.

First, create an enum of allowable types:

enum Types {
    case Int
    case String
}

Create an Example class. Implement your someTypes() function to use these enum values. (You could easily transform a JSON array of strings into an array of this enum.)

class Example {
    func someTypes() -> [Types] {
        var ret = [Types]()
        for _ in 1...rand()%10 {
            if (rand()%2 == 0){ ret.append(.Int) }
            else {ret.append(.String) }
        }
        return ret
    }

Now implement your test function, using switch to scope arr for each allowable type:

    func test() {
        let types = self.someTypes()

        for type in types {
            switch type {
            case .Int:
                var arr = [Int]()
                arr += [4]

            case .String:
                var arr = [String]()
                arr += ["hi"]
            }
        }
    }
}

As you may know, you could alternatively declare arr as [Any] to mix types (the "heterogenous" case in jtbandes' answer):

var arr = [Any]()

for type in types {
    switch type {
    case .Int:
        arr += [4]

    case .String:
        arr += ["hi"]
    }
}

print(arr)
Spoony answered 18/6, 2015 at 3:17 Comment(0)
G
3

I would break it down with the things you already learned from the first answer. I took the liberty to refactor some code. Here it is:

func someTypes<T>(t: T.Type) -> [Any.Type] {
    var ret = [Any.Type]()
    for _ in 0..<rand()%10 {
        if (rand()%2 == 0){ ret.append(T.self) }
        else {
            ret.append(String.self)
        }
    }
    return ret
}


func makeArray<T>(t: T) -> [T] {
    return [T]()
}

func test() {
    let ts = someTypes(Int.self)
    for t in ts {
        print(t)
    }
}

This is somewhat working but I believe the way of doing this is very unorthodox. Could you use reflection (mirroring) instead?

Geostatics answered 18/6, 2015 at 2:45 Comment(3)
This is interesting and creative, but I don't think it is usable. Say you add var arr = makeArray(t) in test()'s for loop. arr will be of type Array<protocol<>.Type>, which can't be cast to anything.Spoony
Strangely, this fails whenever I try to put anything dynamic in there instead of Int.self. Like this: var type:Any.Type ; if (rand()%2 == 0){ type = Int.self } ; else { type = String.self } ; let ts = someTypes(type);Doyenne
And I don't like the error messages I'm getting for this. It's like Swift is making up the rules as it goes.Doyenne
C
0

Its possible so long as you can provide "a hint" to the compiler about the type of... T. So in the example below one must use : String?.

func cast<T>(_ value: Any) -> T? {
    return value as? T
}

let inputValue: Any = "this is a test"
let casted: String? = cast(inputValue) 
print(casted) // Optional("this is a test")
print(type(of: casted)) // Optional<String>

Why Swift doesn't just allow us to let casted = cast<String>(inputValue) I'll never know.


One annoying scenerio is when your func has no return value. Then its not always straightford to provide the necessary "hint". Lets look at this example...

func asyncCast<T>(_ value: Any, completion: (T?) -> Void) {
    completion(value as? T)
}

The following client code DOES NOT COMPILE. It gives a "Generic parameter 'T' could not be inferred" error.

let inputValue: Any = "this is a test"
asyncCast(inputValue) { casted in
    print(casted) 
    print(type(of: casted))
}

But you can solve this by providing a "hint" to compiler as follows:

asyncCast(inputValue) { (casted: String?) in
    print(casted) // Optional("this is a test")
    print(type(of: casted)) // Optional<String>
}
Carcanet answered 11/2, 2020 at 13:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.