【问题标题】:Returning the associatedtype of an opaque return type返回不透明返回类型的关联类型
【发布时间】:2019-12-08 21:35:15
【问题描述】:

我有一个带有关联类型的简单协议,以及一个返回此类型数组的协议扩展。

protocol Foo {
    associatedtype Unit
}

extension Foo {
    var allTheFoos: [Unit] {
        return []
    }
}

然后我有一个在计算属性中返回 some Foo 的结构,以及另一个返回 allTheFoos 数组的计算属性。

struct FakeFoo: Foo {
    typealias Unit = Int
}

struct FooFactory {
    var myFoo: some Foo {
        return FakeFoo()
    }

    /* WHICH RETURN TYPE WILL
       PLEASE THE SWIFT GODS?!
     */
    var allTheFoos: [Foo.Unit] {
        return myFoo.allTheFoos
    }
}

allTheFoos 的返回类型与 Xcode 对 myFoo.allTheFoos 调用的自动完成类型建议相匹配,但可以理解的是,这会产生:

// var allTheFoos: [Foo.Unit] {}
ERROR: Associated type 'Unit' can only be used with a concrete type or generic parameter base

我的问题是:什么返回类型会让 Xcode 开心?

以下是我的尝试,以及相应的错误

// var allTheFoos: [some Foo.Unit] {}
ERROR: 'some' types are only implemented for the declared type of properties and subscripts and the return type of functions
// func allTheFoos() -> some [Foo.Unit]
ERROR: Associated type 'Unit' can only be used with a concrete type or generic parameter base
// func allTheFoos<U: Foo.Unit>() -> [U]
ERROR: Associated type 'Unit' can only be used with a concrete type or generic parameter base
ERROR: Cannot convert return expression of type '[(some Foo).Unit]' to return type '[U]'
// func allTheFoos<U>() -> [U] where U: (some Foo).Unit
ERROR: 'some' types are only implemented for the declared type of properties and subscripts and the return type of functions

仅供参考:我首先在计算属性中执行此操作的原因是为了在某些 SwiftUI 代码中保持简洁。

感谢您提供的任何帮助!

===========更新============

我在示例代码中遗漏了一些重要的内容,因此提供一些上下文:该代码用于单位转换应用程序,因此可以将摄氏度转换为开尔文,将千克转换为磅,以及将其他任何东西转换为其他任何东西。

protocol Unit: Equatable {
    var suffix: String { get }
}

struct Value<UnitType: Unit> {
    let amount: Double
    let unit: UnitType

    var description: String {
        let formatted = String(format: "%.2f", amount)
        return "\(formatted)\(unit.suffix)"
    }
}

值被限制为单位类型,因此无法将摄氏度转换为升。

因此,我们有一个Conversion 协议将所有相似的单元存储在一起:

protocol Conversion {
    associatedtype UnitType: Unit

    var allUnits: [UnitType] { get }
    func convert(value: Value<UnitType>, to unit: UnitType) -> Value<UnitType>
}

extension Conversion {
    func allConversions(for value: Value<UnitType>) -> [Value<UnitType>] {
        let units = self.allUnits.filter { $0 != value.unit }
        return units.map { convert(value: value, to: $0) }
    }
}

因此,温度转换的示例如下:

struct Temperature: Conversion {
    enum Units: String, Unit, CaseIterable {
        case celsius, farenheit, kelvin

        var suffix: String {
            switch self {
            case .celsius:   return "˚C"
            case .farenheit: return "˚F"
            case .kelvin:    return "K"
            }
        }
    }

    var allUnits: [Units] { return Units.allCases }

    func convert(value: Value<Units>, to unit: Units) -> Value<Units> {
        /* ... */
    }
}

最后,实际出现问题的应用代码在这里:

struct MyApp {
    var current: some Conversion {
        return Temperature()
    }

    // ERROR: Associated type 'UnitType' can only be used with a concrete type or generic parameter base
    var allConversions: [Value<Conversion.UnitType>] {
        // This value is grabbed from the UI
        let amount = 100.0
        let unit = current.allUnits.first!
        let value = Value(amount: amount, unit: unit)

        return current.allConversions(for: value)
    }
}

【问题讨论】:

  • 这是不可能的。虽然作品中的一些功能可能会在未来几年使部分实现成为可能(特别是可能与不透明返回类型一起使用的广义存在),但目前尚不清楚您真正想要在这里发生什么以及是否会发生可能的。调用代码是什么样的?我们可以从那里开始,设计一些可能不需要关联类型的东西(我认为你不是真的在这里)。
  • 谢谢@RobNapier - 我现在意识到我试图简化问题并将其发布在这里,随后删除了很多关于我的意图的重要代码。马上更新。

标签: swift generics associated-types opaque-result-type


【解决方案1】:

看看你是如何实现AnyValue的,我想你想要的只是:

var allConversions: [String] {
    let units = self.allUnits.filter { $0 != value.unit }
    return units.map { convert(value: value, to: $0) }.description
}

或者类似的东西。与您所描述的匹配的所有算法都只是“转换->字符串”。如果是这种情况,您真正想要的只是 CustomStringConvertible。

【讨论】:

    【解决方案2】:

    设法使用一些类型擦除解决了这个问题:

    struct AnyValue {
        let description: String
    
        init<U: Unit>(_ value: Value<U>) {
            self.description = value.description
        }
    }
    

    允许:

    var allConversions: [AnyValue] {
        // This value is grabbed from the UI
        let amount = 100.0
        let unit = current.allUnits.first!
        let value = Value(amount: amount, unit: unit)
    
        return current.allConversions(for: value).map(AnyValue.init)
    }
    

    但是,这感觉像是一种笨拙的解决方案(并且引入了不透明的返回类型来避免这种解决方案)。有没有更好的办法?

    【讨论】:

    • 问题不在于笨重;问题是这将用于什么。 allConversions 可以对结果做什么? allConversions 的目的是什么? (我会先研究 Foundation 中现有的 Measurement 系统,看看你在解决什么问题。它不是完美的 Swift,肯定有改进它的方法,但它是解决这个问题的合适起点。)如果您的目标只是创建字符串,这里有更简单的解决方案。
    • 对于给定的值(比如 100˚C),我想立即获得该维度的所有其他可能值(如 ˚F 和 K)。对于这个用例,您的String 示例效果很好,但我仍然很想看看在some Conversion 的约束下这在概念上是否可行。你是对的,与Measurement 有很多重叠,我没有使用它的唯一原因是因为我在这里探索不透明的结果类型。我可以使用它,但由于所有相应的 Dimensions 都只是静态属性,我认为需要一些方便的枚举来使事情可以迭代。
    • 然而,这不是不透明结果类型的用途。不透明结果类型的意义在于隐藏编译器已知但调用者不知道的特定具体类型。我认为您正在尝试将 some 视为存在主义(类型橡皮擦)。这就是它现在的工作方式。这就是为什么你不能拥有[some P]。所有元素都必须是某种 specific 类型(不是P)。在您的示例中,myFoo 的类型不是“任何 P”。它是“myFoo 返回的精确类型,在编译时已知,但调用者只能将其视为P。”
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-05-11
    • 2021-04-15
    • 1970-01-01
    相关资源
    最近更新 更多