如何在Swift的新Result类型中正确使用泛型类型?

时间:2019-07-01 18:52:29

标签: swift generics

我正在尝试使用Swift的新Result类型,以使.success关联值的类型是通用的。下面相当人为的示例代码可以工作,但是有没有一种方法可以简化类型转换,以便编译器可以推断出T的正确类型?

enum FetchError : Error {
  case unknownKey
}

enum FetchKey {
  case getWidth
  case getName
}

func fetchValue<T>(_ key:FetchKey) -> Result<T, FetchError> {
  switch key {

  case .getName:
    // Ideally I would like to just use: return .success("Johnny Appleseed")
    return Result<String, FetchError>.success("Johnny Appleseed") as! Result<T, FetchError>

  case .getWidth:
    return Result<Double, FetchError>.success(25.0) as! Result<T, FetchError>

  @unknown default:
    return .failure(.unknownKey)
  }
}

// This explicit type declaration is also required.
let r:Result<String, FetchError> = fetchValue(.getName)
print(r)

3 个答案:

答案 0 :(得分:2)

对于有关类型转换的问题,您绝对可以简化:

case .getName:
    return .success("Johnny Appleseed" as! T)

如果可以将请求错误的类型视为编程错误(并且应该崩溃),并且结果永远不会来自外部来源,则可以。如果数据可能来自外部来源,那么您永远都不应因错误而崩溃。

在这种情况下,我们应该对这种错误建模:

enum FetchError : Error {
    case unknownKey
    case invalidType
}

然后,通过添加一个执行(可能失败的)类型转换的函数,可以使语法非常接近您想要的语法:

func fetchValue<Value>(_ key:FetchKey) -> Result<Value, FetchError> {

    func checkType(_ value: Any) -> Result<Value, FetchError> {
        guard let value = value as? Value else { return .failure(.invalidType) }
        return .success(value)
    }

    switch key {
    case .getName: return checkType("Johnny Appleseed")
    case .getWidth: return checkType(25.0)
    @unknown default: return .failure(.unknownKey)
    }
}

也就是说,我这样做是为了避免必需的类型注释的丑陋:

func fetch<Value>(_: Value.Type, forKey key: FetchKey) -> Result<Value, FetchError> { ... }

let r = fetch(String.self, forKey: .getName)

这遵循Codable的模式。


下面以几种不同的方式将整个解决方案集中在一个地方:

返回结果

enum FetchError : Error {
    case unknownKey
    case invalidType
}

enum FetchKey {
    case width
    case name
}

func fetch<Value>(_: Value.Type, forKey key: FetchKey) -> Result<Value, FetchError> {
    func checkType(_ value: Any) -> Result<Value, FetchError> {
        guard let value = value as? Value else { return .failure(.invalidType) }
        return .success(value)
    }

    switch key {
    case .name: return checkType("Johnny Appleseed")
    case .width: return checkType(25.0)
    @unknown default: return .failure(.unknownKey)
    }
}

有罚球

我认为,如果您抛出而不是将结果包装到Result中,这会变得更好。这意味着您可以更轻松地将checkType提升到一个位置,并且非常接近您想要的语法。

func fetch<Value>(_: Value.Type, forKey key: FetchKey) throws -> Value {
    func checkType(value: () throws -> Any) throws -> Value {
        guard let value = try value() as? Value else { throw FetchError.invalidType }
        return value
    }

    return try checkType {
        switch key {
        case .name: return "Johnny Appleseed"
        case .width: return 25.0
        @unknown default: throw FetchError.unknownKey
        }
    }
}

带有可选项

如果您不太在意错误,可以使用Optional来简化。

func fetch<Value>(_: Value.Type, forKey key: FetchKey) -> Value? {
    func _fetch() -> Any? {
        switch key {
        case .name: return "Johnny Appleseed"
        case .width: return 25.0
        @unknown default: return nil
        }
    }

    return _fetch() as? Value
}

答案 1 :(得分:1)

您要创建的函数具有动态类型的泛型。这是不可能的,因为Swift编译器需要在编译时知道每种方法/变量的类型。

假设你有

func whatToFetch() -> FetchKey {
 // Randomly returns one of the FetchKey cases
}

let r = fetchValue(whatToFetch())

在进行调用之前,编译器无法知道r的类型。您的示例代码之所以有效,是因为您的强制强制执行完全对齐。但是在正确的实现中,您不必指定通用函数内部的类型T


在您的情况下,似乎并不是真的需要泛型,您可以取消协议:


enum FetchKey {

    case someStringValueKey
    case someDoubleValueKey
    case someModelValueKey
}

protocol FetchResult {}

extension String: FetchResult {}
extension Double: FetchResult {}
extension SomeModel: FetchResult {}

func fetch(_ key: FetchKey) -> Result<FetchResult,Error> {
    switch key {
    case .someStringValueKey:
        return .success("")
    case .someDoubleValueKey:
        return .success(1.0)
    case .someModelValueKey:
        return .success(SomeModel())
    }
}

let wtf = whatToFetch()
let r = fetch(wtf)

switch r {
case .success(let value):
    switch wtf {
    case .someStringValueKey:
        guard let stringValue = value as? String else { return }
    case .someDoubleValueKey:
        guard let doubleValue = value as? Double else { return }
    case .someModelValueKey:
        guard let someModelValue = value as? SomeModel else { return }
    }
case .failure(let error):
    print("Better do more than just print on production code")
}

答案 2 :(得分:1)

尝试使用不合格的通用fetchValue<T>来完成您想做的事情可能无法正常工作。原因是在通用函数中,T是由调用方指定的,而不是由函数指定的。您实际上是在说:“向fetchValue询问您想要的任何T,它将为您带来结果。”

设置类型时缺少链接。最主要的提示是使用as!。这表明您在假设类型关系,而您并未告诉编译器。

那是什么意思?好吧,请注意,使用您的代码,let r: Result<URLConnection, FetchError> = fetchValue(.getName)也可以编译-但随后在运行时崩溃!

一种解决方案是,像Emil’s solution一样,使用伞类型来收集所有可能的结果类型。用这种方法,您可以删除结果的类型,并要求调用者动态提取它。调用者必须处理结果可能是任何类型的可能性,而不一定是他们期望的那种类型。在处理动态结构化数据(例如JSON文档)时,这是一种常见模式。

第二种方法是执行Rob suggests:让调用者指定类型,但将不正确的类型转换为错误。

解决此问题的第三种方法是找到一种将键与类型系统中的结果类型相关联的方法,即告诉编译器getNameString,{ {1}}→getWidth。不幸的是,对于AFAIK,无法对单个枚举大小写进行此操作,因此您需要将键编码为枚举以外的其他形式。

这里是一种方法:

Double

您应该使用哪种方法?

  • 每个键是否总是返回您事先知道的单个一致类型(例如,name始终是字符串)?
    • :使用我上面的方法
    • :如果提取成功,但是返回的类型不是呼叫者要求的类型,您希望如何报告?
      • 结果,结果失败,呼叫者看不到该值:请使用Rob的方法
      • 作为结果。成功,呼叫者必须弄清楚值的类型:使用Emil的方法