Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic function taking a type name in Swift

Tags:

In C#, it's possible to call a generic method by specifying the type:

public T f<T>() {    return something as T }  var x = f<string>() 

Swift doesn't allow you to specialize a generic method when calling it. The compiler wants to rely on type inference, so this is not possible:

func f<T>() -> T? {     return something as T? }  let x = f<String>() // not allowed in Swift 

What I need is a way to pass a type to a function and that function returning an object of that type, using generics

This works, but it's not a good fit for what I want to do:

let x = f() as String? 

EDIT (CLARIFICATION)

I've probably not been very clear about what the question actually is, it's all about a simpler syntax for calling a function that returns a given type (any type).

As a simple example, let's say you have an array of Any and you create a function that returns the first element of a given type:

// returns the first element in the array of that type func findFirst<T>(array: [Any]) -> T? {     return array.filter() { $0 is T }.first as? T } 

You can call this function like this:

let array = [something,something,something,...]  let x = findFirst(array) as String? 

That's pretty simple, but what if the type returned is some protocol with a method and you want to call the method on the returned object:

(findFirst(array) as MyProtocol?)?.SomeMethodInMyProtocol() (findFirst(array) as OtherProtocol?)?.SomeMethodInOtherProtocol() 

That syntax is just awkward. In C# (which is just as strongly typed as Swift), you can do this:

findFirst<MyProtocol>(array).SomeMethodInMyProtocol(); 

Sadly, that's not possible in Swift.

So the question is: is there a way to accomplish this with a cleaner (less awkward) syntax.

like image 807
Philippe Leybaert Avatar asked May 13 '16 17:05

Philippe Leybaert


People also ask

Which of the following are generic types in Swift?

Swift 4s 'Arrays' and 'Dictionary' types belong to generic collections. With the help of arrays and dictionaries the arrays are defined to hold 'Int' values and 'String' values or any other types.


1 Answers

Unfortunately, you cannot explicitly define the type of a generic function (by using the <...> syntax on it). However, you can provide a generic metatype (T.Type) as an argument to the function in order to allow Swift to infer the generic type of the function, as Roman has said.

For your specific example, you'll want your function to look something like this:

func findFirst<T>(in array: [Any], ofType _: T.Type) -> T? {   return array.lazy.compactMap { $0 as? T }.first } 

Here we're using compactMap(_:) in order to get a sequence of elements that were successfully cast to T, and then first to get the first element of that sequence. We're also using lazy so that we can stop evaluating elements after finding the first.

Example usage:

protocol SomeProtocol {   func doSomething() }  protocol AnotherProtocol {   func somethingElse() }  extension String : SomeProtocol {   func doSomething() {     print("success:", self)   } }  let a: [Any] = [5, "str", 6.7]  // Outputs "success: str", as the second element is castable to SomeProtocol. findFirst(in: a, ofType: SomeProtocol.self)?.doSomething()  // Doesn't output anything, as none of the elements conform to AnotherProtocol. findFirst(in: a, ofType: AnotherProtocol.self)?.somethingElse() 

Note that you have to use .self in order to refer to the metatype of a specific type (in this case, SomeProtocol). Perhaps not a slick as the syntax you were aiming for, but I think it's about as good as you're going to get.

Although it's worth noting in this case that the function would be better placed in an extension of Sequence:

extension Sequence {   func first<T>(ofType _: T.Type) -> T? {     // Unfortunately we can't easily use lazy.compactMap { $0 as? T }.first     // here, as LazyMapSequence doesn't have a 'first' property (we'd have to     // get the iterator and call next(), but at that point we might as well     // do a for loop)     for element in self {       if let element = element as? T {         return element       }     }     return nil   } }  let a: [Any] = [5, "str", 6.7] print(a.first(ofType: String.self) as Any) // Optional("str") 
like image 96
Hamish Avatar answered Dec 21 '22 22:12

Hamish