【问题标题】:Protocol function with generic type具有泛型类型的协议函数
【发布时间】:2016-11-09 08:28:20
【问题描述】:

我想创建如下协议:

protocol Parser {
    func parse() -> ParserOutcome<?>
}

enum ParserOutcome<Result> {
    case result(Result)
    case parser(Parser)
}

我想让解析器返回特定类型的结果或另一个解析器。

如果我在Parser 上使用关联类型,那么我不能在enum 中使用Parser。如果我在parse() 函数上指定了泛型类型,那么我无法在没有泛型类型的实现中定义它。

我怎样才能做到这一点?


使用泛型,我可以这样写:

class Parser<Result> {
    func parse() -> ParserOutcome<Result> { ... }
}

enum ParserOutcome<Result> {
    case result(Result)
    case parser(Parser<Result>)
}

这样,Parser 将由结果类型参数化。 parse() 可以返回 Result 类型的结果,或任何类型的解析器,该解析器将输出 Result 类型的结果,或由相同 Result 类型参数化的另一个解析器。

然而,据我所知,对于关联类型,我将始终有一个 Self 约束:

protocol Parser {
    associatedtype Result

    func parse() -> ParserOutcome<Result, Self>
}

enum ParserOutcome<Result, P: Parser where P.Result == Result> {
    case result(Result)
    case parser(P)
}

在这种情况下,我不能再使用任何类型的解析器来返回相同的 Result 类型,它必须是相同类型的解析器。

我希望使用Parser 协议获得与使用泛型定义相同的行为,并且我希望能够在类型系统的范围内做到这一点,而无需引入新的盒装类型,只需就像我可以使用正常的通用定义一样。

在我看来,在 Parser 协议中定义 associatedtype OutcomeParser: Parser,然后返回由该类型参数化的 enum 可以解决问题,但如果我尝试以这种方式定义 OutcomeParser,我会收到错误:

类型不能将自身作为要求引用

【问题讨论】:

  • 我自己真的无法为此写出答案,但在我看来,您可能正在寻找类型擦除。我知道@RobNapier 在其中一些答案中已经展示了它的优雅使用,也许你可以在那里找到一些要调查的东西。
  • 我正在寻找语言支持。如果它需要 hack,那么我宁愿不实现它。
  • 我不认为类型擦除被认为是一种 hack,而是一种技术(我自己还没有掌握 :)。我想我在某处读到 Swift stdlib 本身在某些地方使用了类型擦除。 (此外,由于我自己并没有真正掌握类型擦除,因此在这种情况下使用它可能完全不合适。
  • 嗯,确实,AnySequence 正在使用类型擦除,它在标准库中,并且被 Apple 明确记录为“类型擦除”。到目前为止,它仍然感觉像是一个 hack,但我正在研究它。
  • 类型擦除不是 hack。它们在 Swift 标准库中使用。你可以在这里阅读更多关于它们的信息 - natashatherobot.com/swift-type-erasure

标签: swift generics types swift-protocols associated-types


【解决方案1】:

完成这项工作所需的功能状态:

  • 递归协议约束 (SE-0157) 已实现 (Swift 4.1)
  • 协议中的任意要求 (SE-0142) 已实现 (Swift 4)
  • 通用类型别名 (SE-0048) 已实现 (Swift 3)

目前看来,如果不引入盒装类型(“类型擦除”技术),这是不可能实现的,并且正如 @ 的 Recursive protocol constraintsArbitrary requirements in protocols 部分所述,这是 Swift 的未来版本所考虑的东西987654326@(因为不支持generic protocols)。

当 Swift 支持这两个特性时,以下内容应该生效:

protocol Parser {
    associatedtype Result
    associatedtype SubParser: Parser where SubParser.Result == Result

    func parse() -> ParserOutcome<Result, SubParser>
}

enum ParserOutcome<Result, SubParser: Parser where SubParser.Result == Result> {
    case result(Result)
    case parser(P)
}

使用generic typealiases,子解析器类型也可以提取为:

typealias SubParser<Result> = Parser where SubParser.Result == Result

【讨论】:

  • 我在上面提到了类型擦除,但也许你可以适应recursive enumerations 来帮助你(关键字indirect case)。
  • @dfri,真的,我编辑了这个问题,但提到我希望能够在类型系统的范围内做到这一点,而无需引入新的盒装类型,就像我可以使用正常的通用定义。我确实考虑过indirect 枚举,但我认为这不适用于我的情况,因为Parser 本身不是枚举,而是需要包含逻辑的东西。
  • 是的,我尝试自己使用递归枚举来构造一个像上面那样的解析器,但是没有成功(我想我会添加它以防其他人可以更有创意地使用该构造.)
【解决方案2】:

我不会这么快就将类型擦除视为“hacky”或“绕过 [...] 类型系统”——事实上,我认为它们与 类型系统,以便在使用协议时提供有用的抽象层(如前所述,用于标准库本身,例如 AnySequenceAnyIndexAnyCollection)。

正如您自己所说,您在这里要做的就是有可能从解析器返回给定的结果,或者返回另一个处理相同结果类型的解析器。我们不关心该解析器的具体实现,我们只想知道它有一个parse() 方法,该方法返回相同类型的结果,或者另一个具有相同要求的解析器​​。

类型擦除非常适合这种情况,因为您需要做的就是引用给定解析器的parse() 方法,从而允许您抽象出该解析器的其余实现细节。重要的是要注意,您在这里并没有失去任何类型安全性,您对解析器的类型完全按照您的要求指定。

如果我们看一下类型擦除解析器的潜在实现,AnyParser,希望您能明白我的意思:

struct AnyParser<Result> : Parser {

    // A reference to the underlying parser's parse() method
    private let _parse : () -> ParserOutcome<Result>

    // Accept any base that conforms to Parser, and has the same Result type
    // as the type erasure's generic parameter
    init<T:Parser where T.Result == Result>(_ base:T) {
        _parse = base.parse
    }

    // Forward calls to parse() to the underlying parser's method
    func parse() -> ParserOutcome<Result> {
        return _parse()
    }
}

现在在您的ParserOutcome 中,您可以简单地指定parser 案例具有AnyParser&lt;Result&gt; 类型的关联值——即可以使用给定Result 泛型参数的任何类型的解析实现。

protocol Parser {
    associatedtype Result
    func parse() -> ParserOutcome<Result>
}

enum ParserOutcome<Result> {
    case result(Result)
    case parser(AnyParser<Result>)
}

...

struct BarParser : Parser {
    func parse() -> ParserOutcome<String> {
        return .result("bar")
    }
}

struct FooParser : Parser {
    func parse() -> ParserOutcome<Int> {
        let nextParser = BarParser()

        // error: Cannot convert value of type 'AnyParser<Result>'
        // (aka 'AnyParser<String>') to expected argument type 'AnyParser<_>'
        return .parser(AnyParser(nextParser))
    }
}

let f = FooParser()
let outcome = f.parse()

switch outcome {
case .result(let result):
    print(result)
case .parser(let parser):
    let nextOutcome = parser.parse()
}

您可以从这个示例中看到 Swift 仍在强制执行类型安全。我们正在尝试将BarParser 实例(与Strings 一起使用)包装在需要Int 泛型参数的AnyParser 类型已擦除包装器中,从而导致编译器错误。一旦将FooParser 参数化为使用Strings 而不是Int,编译器错误将得到解决。


事实上,由于AnyParser 在这种情况下仅充当单个方法的包装器,另一个潜在的解决方案(如果您真的讨厌类型擦除)是直接使用它作为您的ParserOutcome 的关联值。

protocol Parser {
    associatedtype Result
    func parse() -> ParserOutcome<Result>
}

enum ParserOutcome<Result> {
    case result(Result)
    case anotherParse(() -> ParserOutcome<Result>)
}


struct BarParser : Parser {
    func parse() -> ParserOutcome<String> {
        return .result("bar")
    }
}

struct FooParser : Parser {
    func parse() -> ParserOutcome<String> {
        let nextParser = BarParser()
        return .anotherParse(nextParser.parse)
    }
}

...

let f = FooParser()
let outcome = f.parse()

switch outcome {
case .result(let result):
    print(result)
case .anotherParse(let nextParse):
    let nextOutcome = nextParse()
}

【讨论】:

  • 不错!在您的第二个解决方案中,为什么我们不需要用indirect 标记parser 案例?当一个case的关联值是一个return类型与枚举相同的闭包时,它不是递归枚举吗?
  • @dfri 啊,有趣,我什至没有想到这一点。我怀疑这是因为函数是引用类型 - 因此不需要额外的间接级别来在枚举中存储 () -&gt; ParserOutcome&lt;Result&gt; 函数,因为存储引用的内存块是已知大小的,而不是依赖于枚举本身。
  • 啊,当然,一定是这样!
  • 感谢您的详尽解释。我确实理解 Swift 版本的类型擦除背后的想法,我也理解你不会失去类型安全的事实,但我说它是一个 hack 的意思是你不应该需要 i> 做所有这些事情是为了表达你的要求。 Swift 团队自己认识到这一点,并在谈到与 AnySequence 类型有关的递归协议约束时这么说。就像我一样,他们不喜欢 AnySequence 的现状,他们计划通过引入递归协议约束来修复它。
  • 另外,虽然我确实认识到解决方案很优雅,但事实仍然是它是一种解决方法。它确实使用类型系统来实现解决方法,但它仍然是一种解决方法。我不需要创建一个新的中间具体类型来表达类型约束,尤其是使用类型系统的不同特性(泛型)可以很好地表达的约束。
【解决方案3】:

我认为您想对 ParserOutcome 枚举使用通用约束。

enum ParserOutcome<Result, P: Parser where P.Result == Result> {
    case result(Result)
    case parser(P)
}

这样,您将无法将ParserOutcome 与不符合Parser 协议的任何内容一起使用。您实际上可以再添加一个约束以使其更好。添加Parser 结果的结果将与Parser 的关联类型相同的约束。

【讨论】:

  • 是的,谢谢,但是parse() 的返回类型的问题仍然存在:我如何定义它以便它可以采用任何类型的Parser,而不仅仅是Self
  • 哦,我现在明白了。当前版本的 Swift 不允许递归协议约束。我认为它已在 Swift 进化邮件列表中进行了讨论,但不会包含在 Swift 3.0 版本中。我认为现在类型擦除可能是你最好的选择。
  • 确实,现在看看类型擦除,它看起来确实很优雅,但它绝对是一个 hack(即使它是官方的),因为它解决了类型系统的限制。如果协议可以是通用的,那就太好了,但在这种情况发生之前(或直到实现递归引用),在类型擦除容器中装箱似乎是一种必要的解决方法。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-05-24
  • 1970-01-01
  • 2020-06-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多