简体   繁体   English

Swift 类型是否可以通过从通用函数的参数中“拉出”类型值来推断?

[英]Is it possible for a Swift type to be inferred by "pulling out" a Type value from a generic function's parameter?

Introduction介绍

(Apologies if the title is confusing, but I explain the question better here!) (如果标题令人困惑,我深表歉意,但我在这里更好地解释了这个问题!)

I'm building a.networking library that can perform JSON decoding on its responses.我正在构建一个可以对其响应执行 JSON 解码的网络库。

Host apps adopting this library will create enums conforming to NetLibRoute .采用此库的主机应用程序将创建符合NetLibRoute的枚举。 All that currently does is enforce the presence of asURL :当前所做的只是强制存在asURL

public protocol NetLibRoute {
    var asURL: URL { get throws }
}

In a host app, I have a routing system that enforces API structure at the compiler-level (via enums and associated values) for each endpoint, like this:在主机应用程序中,我有一个路由系统,它在编译器级别(通过枚举和关联值)为每个端点强制执行 API 结构,如下所示:

enum Routes: NetLibRoute {
    case people(Int?)
    // Other routes go here, e.g.:
    // case user(Int)
    // case search(query: String, limit: Int?)
    
    var asURL: URL {
        let host = "https://swapi.dev/"
        
        let urlString: String
        
        switch self {
        case let .people(personID):
            if let personID {
                urlString = host + "api/people/\(personID)"
            } else {
                urlString = host + "api/people/"
            }
        // Build other URLs from associated values
        }
        
        return URL(string: urlString)!
    }
}

I also want each enum to be associated with a certain Codable type.我还希望每个枚举都与特定的 Codable 类型相关联。 I can do that, of course, by modifying the Route protocol declaration to also require a type conforming to Decodable :当然,我可以这样做,方法是修改Route协议声明,使其也需要符合Decodable的类型:

protocol NetLibRoute {
    var asURL: URL { get throws }
    var decodedType: Decodable.Type { get } // This
}

And a matching computed property in my Routes enum:以及我的Routes枚举中匹配的计算属性:

var decodedType: Decodable.Type {
    switch self {
    case .people(_):
        return Person.self
    // And so on
    }
}

The Problem问题

Currently, my.networking code has a declaration something like this:目前,my.networking 代码有这样的声明:

public static func get<T>(route: NetLibRoute,
                          type: T.Type) async throws -> T where T: Decodable {
    // performing request on route.asURL
    // decoding from JSON as T or throwing error
    // returning decoded T
}

Which lets me call it like this:这让我这样称呼它:

let person = try await NetLib.get(route: Routes.people(1), type: Person.self)

However, this redundancy (and potential human error from mismatching route and type) really irks me.然而,这种冗余(以及路由和类型不匹配导致的潜在人为错误)真的让我很恼火。 I really want to be able to only pass in a route, and have the resulting type be inferred from there.真的希望能够只传递一条路线,并从那里推断出结果类型。

Is there some way to get the compiler to somehow check the NetLibRoute enum and check its decodedType property, in order to know what type to use?有没有办法让编译器以某种方式检查 NetLibRoute 枚举并检查其decodedType属性,以便知道要使用什么类型?

Ultimately, I want this.networking function to take one parameter (a route) and infer the return type of that route (at compile-time, not with fragile runtime hacks or ! s), and return an instance of the type.最终,我希望 this.networking function 接受一个参数(一条路由)并推断该路由的返回类型(在编译时,而不是使用脆弱的运行时 hack 或! s),并返回该类型的一个实例。

Is this possible?这可能吗?


Potential Alternatives?潜在的替代品?

I'm also open to alternative solutions that may involve moving where the get function is called from.我也愿意接受替代解决方案,这些解决方案可能涉及移动调用get function 的位置。

For example, calling this get function on a route itself to return the type:例如,在路由本身上调用此get function 以返回类型:

let person = try await Routes.people(1).get(type: Person.self) // Works, but not optimal
let person = try await Routes.people(1).get() // What I want

Or even on the type itself, by creating a new protocol in the library, and then extending Decodable to conform to it:或者甚至在类型本身上,通过在库中创建一个新协议,然后扩展 Decodable 以符合它:

public protocol NetLibFetchable {
    static var route: NetLibRoute { get }
}

extension Decodable where Self: NetLibFetchable {
    public static func get<T>() async throws -> T where Self == T, T: Decodable {
    // Call normal get function using inferred properties
    return try await NetLib.get(route: route,
                                type: T.self)
}

Which indeed lets me call like this:这确实让我这样称呼:

let person = try await Person.get() // I can't figure out a clean way to pass in properties that the API may want, at least not without once again passing in Routes.people(1), defeating the goal of having Person and Routes.people inherently linked.

While this eliminates the issue of type inference, the route can no longer be customized at call-time, and instead is stuck like this:虽然这消除了类型推断的问题,但不能再在调用时自定义路由,而是像这样卡住:

extension Person: NetLibFetchable {
    public static var route: NetLibRoute {
        Routes.people(1) // Can't customize to different ID Ints anymore!
    }
}

Which makes this particular example a no-go, and leaves me at a loss.这使得这个特定的例子成为禁忌,让我不知所措。


Appreciation欣赏

Anyway, thank you so much for reading, for your time, and for your help.无论如何,非常感谢您的阅读、抽出时间和帮助。

I really want this library to be as clean as possible for host apps interacting with it, and your help will make that possible.我真的希望这个库对于与之交互的主机应用程序尽可能干净,您的帮助将使这成为可能。

I think the problem lays in this function.我认为问题出在这个function。

public static func get<T>(route: Route,
                      type: T.Type) async throws -> T where T: Decodable {
    // performing request on route.asURL
    // decoding from JSON as T or throwing error
    // returning decoded T
}

On the first hand, it uses concretions instead of abstractions.一方面,它使用具体而不是抽象。 You shouldn't pass a Route here, it should use your protocol NetLibRoute instead.你不应该在这里传递路由,它应该使用你的协议 NetLibRoute 代替。

On the other hand, I think that the type param is not needed.另一方面,我认为不需要类型参数。 Afaik you can get the Type to Decode with the var: Afaik,您可以使用 var 获取要解码的类型:

NetLibRoute.decodedType

Am I missing something on this matter?我在这件事上遗漏了什么吗?


Apart from that, I'd rather go with struct instead of enum when trying to implement the Routes (concretions).除此之外,我宁愿 go 在尝试实现路由时使用结构而不是枚举。 Enums cannot be extended.枚举不能扩展。 So you won't be allowing the creation of new requests in client side, only in the library.因此,您将不允许在客户端创建新请求,只能在库中创建。

I hope I've helped.我希望我有所帮助。

PS: Some time ago I made this repo . PS:前段时间我做了这个回购 Maybe that could help you (specially this class ).也许这可以帮助你(特别是这个class )。 I used Combine instead of async/await, but it's not relevant to what you need.我使用 Combine 而不是 async/await,但它与您的需求无关。

Are you wedded to the idea of using an enum ?您执着于使用enum的想法吗? If not, you can do pretty much what you want by giving each enum value its own type and using an associated type to do what you want.如果没有,您可以通过为每个枚举值赋予其自己的类型并使用关联类型来执行您想要的操作来执行您想要的操作。

public protocol NetLibRoute
{
    var asURL: URL { get throws }

    associatedtype Decoded: Decodable
}

struct Person: Decodable
{
    var name: String
}

struct Login: Decodable
{
    var id: String
}

struct People: NetLibRoute
{
    typealias Decoded = Person

    var id: Int

    var asURL: URL { return URL(filePath: "/") }

}

struct User: NetLibRoute
{
    typealias Decoded = Login

    var id: String

    var asURL: URL { return URL(filePath: "/") }
}

func get<N: NetLibRoute>(item: N) throws -> N.Decoded
{
    let data = try Data(contentsOf: item.asURL)
    return try JSONDecoder().decode(N.Decoded.self, from: data)
}

let thing1 = try get(item: People(id: 1))
let thing2 = try get(item: User(id: "foo"))

Where you might have had a switch before to do different things with different Route s you would now use a function with overloaded arguments.在您之前可能有一个开关用不同的Route做不同的事情的地方,您现在可以使用 function 和超载的 arguments。

func doSomething(thing: Person)
{
    // do something for a Person
}

func doSomething(thing: Login)
{
    // do something else for a Login
}

doSomething(thing: thing1)
doSomething(thing: thing2)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM