【问题标题】:Generic type not preserved when called within another generic function在另一个泛型函数中调用时不保留泛型类型
【发布时间】:2021-11-11 18:23:31
【问题描述】:

[更新了一个不那么做作的例子]

我正在尝试扩展一个通用函数来为特定类型提供不同的行为。当我直接调用该函数时,这按预期工作。但是如果我从另一个泛型函数中调用它,则不会保留原始泛型类型并且我会得到默认行为。我对 Swift 有点陌生,所以我可能在这里遗漏了一些明显的东西。

我的代码如下所示:

protocol Task {
    associatedtype Result
}

struct SpecificTask: Task {
    typealias Result = String
    
    // other taks related properties
}

enum TaskRunner<T: Task> {
    static func run(task: T) throws -> T.Result {
        // Task not supported
        throw SomeError.error
    }
}

extension TaskRunner where T == SpecificTask {
    static func run(task: T) throws -> T.Result {    
        // execute a SpecificTask
        return "Some Result"
    }
}

func run<T: Task>(task: T) throws -> T.Result {
    // Additional logic related to running the task
    return try TaskRunner.run(task: task)
}

print(try TaskRunner.run(task: SpecificTask())) // Prints "Some Result"
print(try run(task: SpecificTask()))            // Throws SomeError

我需要顶层run函数调用底层run()函数的SpecificTask版本,但是调用的是泛型版本的函数

【问题讨论】:

  • 我建议你做一个不那么做作的例子,这样你的实际问题就可以得到解决(除非你只是想了解这背后的原因)。
  • 谢谢!我用一个不那么做作的例子更新了我的帖子。

标签: swift generics


【解决方案1】:

您正在尝试使用泛型重新发明类继承。这不是泛型的用途,它们也不是那样工作的。泛型方法是静态分派的,这意味着代码是在编译时而不是运行时选择的。重载永远不应该改变函数的行为(这是你在这里尝试做的)。 where 子句中的覆盖可用于提高性能,但不能用于创建动态(运行时)调度。

如果你必须使用继承,那么你必须使用类。也就是说,您所描述的问题最好使用通用任务而不是协议来解决。例如:

struct Task<Result> {
    let execute: () throws -> Result
}

enum TaskRunner {
    static func run<Result>(task: Task<Result>) throws -> Result {
        try task.execute()
    }
}

let specificTask = Task(execute: { "Some Result" })

print(try TaskRunner.run(task: specificTask)) // Prints "Some Result"

注意这如何消除“不支持任务”的情况。它现在不是运行时错误,而是编译时错误。您不能再错误地调用它,因此您不必检查这种情况。

如果你真的想要动态调度,这是可能的,但你必须将它实现为动态调度,而不是重载。

enum TaskRunner<T: Task> {
    static func run(task: T) throws -> T.Result {
        
        switch task {
        case is SpecificTask:
            // execute a SpecificTask
            return "Some Result" as! T.Result   // <=== This is very fragile
        default:
            throw SomeError.error
        }
    }
}

由于as! T.Result,这很脆弱。如果将 SpecificTask 的结果类型更改为 String 以外的类型,它会崩溃。但重要的一点是case is SpecificTask,它是在运行时确定的(动态调度)。如果你需要task,我假设你需要,你可以用if let task = task as? SpecificTask 交换它。

在走这条路之前,我会重新考虑设计,看看这将如何真正被调用。由于 Result 类型是通用的,因此您不能在循环中调用任意任务(因为所有返回值都必须匹配)。所以这让我想知道什么样的代码实际上可以调用run

【讨论】:

  • 这非常有用。我认为最后我需要做的就是将 TaskRunner 变成一个协议,定义 Task 特定的实现,并按照你的建议在它上面添加一个动态调度层。它消除了“未知”的情况和脆弱的强制铸造的需要。谢谢!
猜你喜欢
  • 2021-06-01
  • 1970-01-01
  • 2023-01-12
  • 2021-10-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-25
相关资源
最近更新 更多