Generic function taking a type name in Swift

2019-01-06 20:31发布

问题:

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.

回答1:

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.flatMap{ $0 as? T }.first
}

Here we're using flatMap(_:) 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.flatMap { $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")


回答2:

What you probably need to do is create a protocol that looks something like this:

protocol SomeProtocol {
    init()
    func someProtocolMethod()
}

And then add T.Type as a parameter in your method:

func f<T: SomeProtocol>(t: T.Type) -> T {
    return T()
}

Then assuming you have a type that conforms to SomeProtocol like this:

struct MyType: SomeProtocol {
    init() { }
    func someProtocolMethod() { }
}

You can then call your function like this:

f(MyType.self).someProtocolMethod()

Like others have noted, this seems like a convoluted way to do what you want. If you know the type, for example, you could just write:

MyType().someProtocolMethod()

There is no need for f.