Swift通用问题扩展API

huangapple go评论72阅读模式
英文:

Swift Generic Issue extending API

问题

在Swift中,您需要能够以通用方式处理由API提供的不同的Token<T>。请注意,我了解您在使用Swift 5.8。

API定义如下(仅显示相关部分):

public struct Token<T> : Codable, Equatable, Hashable { }

public struct Application : Equatable, Hashable {
    public let token: ApplicationToken?
}

public struct ActivityCategory : Equatable, Hashable {
    public let token: ActivityCategoryToken?
}

public struct WebDomain : Equatable, Hashable {
    public let token: WebDomainToken?
}

public typealias ApplicationToken = Token<Application>
public typealias ActivityCategoryToken = Token<ActivityCategory>
public typealias WebDomainToken = Token<WebDomain>

// API提供的数据:
struct ActivitySelection {
    public var applicationTokens: Set<ApplicationToken>
    public var categoryTokens: Set<ActivityCategoryToken>
    public var webDomainTokens: Set<WebDomainToken>
}

您尝试的扩展如下:

// 我的自定义ActivityType:
enum ActivityType {
    case Applications
    case Categories
    case WebDomains
}

// 我的API扩展
extension ActivitySelection {
    func tokensFor<T>(activity: ActivityType) -> [Token<T>] {
        let array: [Token<T>]
        switch activity {
            case .Applications:
                array = Array(self.applicationTokens) as! [Token<T>]
            case .Categories:
                array = Array(self.categoryTokens) as! [Token<T>]
            case .WebDomains:
                array = Array(self.webDomainTokens) as! [Token<T>]
        }
        // 在返回之前进行排序,这里为简单起见省略了排序
        return array
    }
}

let selection = ActivitySelection() // 返回带有数据的实例

然后,当您尝试使用扩展时:

let tokens = selection.tokensFor(activity: .Applications)

您遇到了以下错误:XCode Error: Generic parameter 'T' could not be inferred

为解决此问题,您可以尝试以下更改:

extension ActivitySelection {
    func tokensFor(activity: ActivityType) -> [Any] {
        let array: [Any]
        switch activity {
            case .Applications:
                array = Array(self.applicationTokens)
            case .Categories:
                array = Array(self.categoryTokens)
            case .WebDomains:
                array = Array(self.webDomainTokens)
        }
        // 在返回之前进行排序,这里为简单起见省略了排序
        return array
    }
}

这将返回一个包含Token<T>的通用数组,但请注意,这会丢失类型信息。这是因为Swift的类型推断可能无法推断出通用类型参数T,因此我们使用了[Any]来容纳不同类型的Token。您需要在使用这个数组时小心处理类型。

这是一种通过泛型来处理不同类型的方法,但需要在使用时小心处理类型不一致的情况。如果您有其他更复杂的需求,可能需要考虑使用协议或更高级的类型擦除技巧来处理。

英文:

In Swift I need to be able to generically work with the various Token<T> given to me by an API.

Note that I'm Using Swift 5.8.

The API defines the following (only showing relevant parts):

public struct Token<T> : Codable, Equatable, Hashable { }

public struct Application : Equatable, Hashable {
    public let token: ApplicationToken?
}

public struct ActivityCategory : Equatable, Hashable {
    public let token: ActivityCategoryToken?
}

public struct WebDomain : Equatable, Hashable {
    public let token: WebDomainToken?
}

public typealias ApplicationToken = Token<Application>
public typealias ActivityCategoryToken = Token<ActivityCategory>
public typealias WebDomainToken = Token<WebDomain>


// Data the API gives me:
struct ActivitySelection {
    public var applicationTokens: Set<ApplicationToken>
    public var categoryTokens: Set<ActivityCategoryToken>
    public var webDomainTokens: Set<WebDomainToken>
}

I tried doing this:

// My custom ActivityType:
enum ActivityType {
    case Applications
    case Categories
    case WebDomains
}

// My API Extension
extension ActivitySelection {
    func tokensFor<T>(activity:ActivityType) -> [Token<T>] {
        let array: [Token<T>]
        switch activity {
            case .Applications:
                array = Array(self.applicationTokens) as! [Token<T>]
            case .Categories:
                array = Array(self.categoryTokens) as! [Token<T>]
            case .WebDomains:
                array = Array(self.webDomainTokens) as! [Token<T>]
        }
        // sort before returning here removed for simplicity
        return array
    }
}

let selection = ActivitySelection() // returns with data

Then when I try to use my extension:

let tokens = selection.tokensFor(activity: .Applications)

I get this error:

  • XCode Error: Generic parameter 'T' could not be inferred

If I try this:

    func tokensFor<T>(activity:Activities) -> [Token<T>] {
        let array: [Token<T>]
        switch activity {
            case .Applications:
                array = Array(self.applicationTokens)
            case .Categories:
                array = Array(self.categoryTokens)
            case .WebDomains:
                array = Array(self.webDomainTokens)
        }
        // sort before returning here
        return array
    }

I get:

  • Cannot assign value of type '[ApplicationToken]' (aka 'Array<Token<Application>>') to type '[Token<T>]'
  • Cannot assign value of type '[ActivityCategoryToken]' (aka 'Array<Token<ActivityCategory>>') to type '[Token<T>]'
  • Cannot assign value of type '[WebDomainToken]' (aka 'Array<Token<WebDomain>>') to type '[Token<T>]'

I've also tried it this way:

    func tokensFor(activity:Activities) -> [Token<Any>] {
        let array: [Token<Any>]
        switch activity {
            case .Applications:
                array = Array(self.applicationTokens)
            case .Categories:
                array = Array(self.categoryTokens)
            case .WebDomains:
                array = Array(self.webDomainTokens)
        }
        // sort before returning here
        return array
    }

But I get errors:

  • Cannot assign value of type '[ApplicationToken]' (aka 'Array<Token<Application>>') to type '[Token<Any>]'
  • Cannot assign value of type '[ActivityCategoryToken]' (aka 'Array<Token<ActivityCategory>>') to type '[Token<Any>]'
  • Cannot assign value of type '[WebDomainToken]' (aka 'Array<Token<WebDomain>>') to type '[Token<Any>]'

How can I make this work? I thought generics was the solution to this very problem.

Since Token<T> is already a generic type, how do I use that to work with the 3 different token types generically?

答案1

得分: 3

以下是您要翻译的内容:

使用enum不起作用的原因是枚举值只能在运行时知道:

let tokens = tokensFor(activity: randomlyGenerateAnActivity())

如果randomlyGenerateAnActivity随机选择了三种情况之一,tokens的编译时类型将是什么?编译器无法知道。

一种解决方法是创建这样一种类型,即一个非泛型类型,包装一个Token<T>,用与Token<T>中涉及T的所有内容相似的东西替换,只不过将T替换为Any。由于您不想显示Token<T>具有哪些成员,我无法为此提供任何代码示例。

另一种解决方法是使tokensFor接受元类型作为参数。

// 使Application、ActivityCategory和WebDomain符合此协议
public protocol TokenType {
    var token: Token<Self>? { get }
}

extension ActivitySelection {
    func tokensFor<T: TokenType>(_ tokenType: T.Type) -> [Token<T>] {
        let array: [Token<T>]
        if tokenType == Application.self {
            array = Array(self.applicationTokens) as! [Token<T>]
        } else if tokenType == ActivityCategory.self {
            array = Array(self.categoryTokens) as! [Token<T>]
        } else if tokenType == WebDomain.self {
            array = Array(self.webDomainTokens) as! [Token<T>]
        } else {
            fatalError("Invalid Token Type")
        }
        // 返回之前删除了排序以简化
        return array
    }
}

现在,tokensFor将始终返回在编译时已知的类型。

请注意,您可以在协议中添加一个静态属性,而不是使用if...else if来检查类型,该属性返回相应的键路径:

// 在三种类型中的每一个实现这个属性
static var activitySelectionKeyPath: KeyPath<ActivitySelection, Set<Token<Self>>> { get }

然后,在tokensFor中,您可以从self[keyPath: T.activitySelectionKeyPath]获取数组。

由于您在SwiftUI视图中显示令牌,您应该创建一个泛型View,并将该泛型参数传递给tokensFor

public struct SomeGenericView<T: TokenType>: View {
    let activitySelection = ActivitySelection()
    
    public var body: some View {
        ForEach(activitySelection.tokensFor(T.self), id: \.something) { token in
            Text("\(token)")
        }
    }
}

然后,您可以创建三个这种SomeGenericView

// 在父视图中...
public var body: some View {
    SomeGenericView<Application>()
    SomeGenericView<ActivityCategory>()
    SomeGenericView<WebDomain>()
}

希望这对您有所帮助。

英文:

The reason why using an enum doesn't work is that enum values could only be known at runtime:

let tokens = tokensFor(activity: randomlyGenerateAnActivity())

If randomlyGenerateAnActivity randomly chooses one of the three cases, what would be the compile-time type of tokens? The compiler cannot know.

One solution would be to make such a type, a non-generic type that wraps a Token&lt;T&gt;, replacing everything in Token&lt;T&gt; that involves T with something similar, except T is replaced with Any. Since you don't want to show what members Token&lt;T&gt; has, I can't give any code examples for this.

Another solution would be to make tokensFor take a metatype as a parameter.

// make Application, ActivityCategory, and WebDomain conform to this
public protocol TokenType {
    var token: Token&lt;Self&gt;? { get }
}

extension ActivitySelection {
    func tokensFor&lt;T: TokenType&gt;(_ tokenType: T.Type) -&gt; [Token&lt;T&gt;] {
        let array: [Token&lt;T&gt;]
        if tokenType == Application.self {
            array = Array(self.applicationTokens) as! [Token&lt;T&gt;]
        } else if tokenType == ActivityCategory.self {
            array = Array(self.categoryTokens) as! [Token&lt;T&gt;]
        } else if tokenType == WebDomain.self {
            array = Array(self.webDomainTokens) as! [Token&lt;T&gt;]
        } else {
            fatalError(&quot;Invalid Token Type&quot;)
        }
        // sort before returning here removed for simplicity
        return array
    }
}

Now the type that tokensFor will return is always known at compile time.

Note that instead of using if...else if to check the type, you could also add a static property in the protocol that returns the corresponding key path:

// implement this property in each of the three types
static var activitySelectionKeyPath: KeyPath&lt;ActivitySelection, Set&lt;Token&lt;Self&gt;&gt;&gt; { get }

Then you can just get the array in tokensFor from self[keyPath: T.activitySelectionKeyPath].

<hr>

Since you are displaying the tokens in a SwiftUI view, you should make a generic View, and pass that generic parameter into tokensFor:

public struct SomeGenericView&lt;T: TokenType&gt;: View {
    let activitySelection = ActivitySelection()
    
    public var body: some View {
        ForEach(activitySelection.tokensFor(T.self), id: \.something) { token in
            Text(&quot;\(token)&quot;)
        }
    }
}

Then you can make three of this SomeGenericView:

// in a parent view...
public var body: some View {
    SomeGenericView&lt;Application&gt;()
    SomeGenericView&lt;ActivityCategory&gt;()
    SomeGenericView&lt;WebDomain&gt;()
}

答案2

得分: 1

这个签名不表示你认为的意思:

func tokensFor<T>(activity: ActivityType) -> [Token<T>] {

这意味着调用者可以选择任何可能存在的类型(String、Int、他们刚刚定义的随机结构、UIViewController,或任何类型),而该函数承诺返回包装该类型的 Token。而这一切将在编译时静态确定。这不是 Token 的工作方式,也不是你的意思。

相反,目标是在运行时返回仅在运行时已知的 Token 类型。这是协议的标准用法。

protocol TokenType {
    // 这里通常放置调用者需要在返回的令牌上调用的任何方法。
}

extension Token: TokenType {
    // 这可能是空的,但如果 Token 需要一些新方法以符合 TokenType,请在这里添加它们。
}

extension ActivitySelection {
    // 现在返回一个“在运行时已知的任何类型的 Token”数组
    func tokensFor(activity: ActivityType) -> [any TokenType] {
        let array: [any TokenType]
        switch activity {
            case .Applications:
                array = Array(self.applicationTokens)
            case .Categories:
                array = Array(self.categoryTokens)
            case .WebDomains:
                array = Array(self.webDomainTokens)
        }
        // 返回之前进行排序的部分已被简化
        return array
    }
}
英文:

This signature does not mean what you think it means:

func tokensFor&lt;T&gt;(activity:ActivityType) -&gt; [Token&lt;T&gt;] {

This says that the caller can pick any type that can possibly exist (a String, Int, random struct they just defined, UIViewController, anything), and this function promises to return a Token wrapping that type. And this will all be decided statically, at compile-time. That's not how Token works, and it's not what you mean.

Instead, the goal is to return a Token type that is only known at runtime. That is the standard use for a protocol.

protocol TokenType {
    // Here you would generally put any methods the caller needs to be 
    // able to call on the returned token.
}

extension Token: TokenType {
    // This can probably be empty, but if Token needs some new methods to
    // conform to TokenType, add them here.
}

extension ActivitySelection {
    // Now return an Array of &quot;any kind of Token, known at runtime&quot;
    func tokensFor(activity: ActivityType) -&gt; [any TokenType] {
        let array: [any TokenType]
        switch activity {
            case .Applications:
                array = Array(self.applicationTokens)
            case .Categories:
                array = Array(self.categoryTokens)
            case .WebDomains:
                array = Array(self.webDomainTokens)
        }
        // sort before returning here removed for simplicity
        return array
    }
}

huangapple
  • 本文由 发表于 2023年5月15日 09:30:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/76250368.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定