Swift 泛型之条件性符合协议

2021-04-08 13:37:31 浏览数 (1)

Swift 泛型条件性符合(Conditional conformances) 表示泛型类型只有在其类型参数满足某些要求时才符合特定协议的概念。

例如,Array只在其元素本身实现了Equatable协议时才符合Equatable协议,这可以通过以下Equatable上的条件性符合来表示:

代码语言:javascript复制
extension Array: Equatable where Element: Equatable {
  static func ==(lhs: Array<Element>, rhs: Array<Element>) -> Bool { ... }
}

条件性符合解决了泛型系统可组合性中的一个漏洞。继续上面的数组示例,总是可以在两个Equatable类型的数组上使用==运算符,例如,[Int]==[Int]将比较成功。但是,如下情况却不行:可等式类型的数组的数组不能进行比较(例如,[[Int]]=[[Int]]将无法编译),因为即使符合Equatable协议的类型组成的数组他有==运算符,数组本身也并不符合Equable协议。

在构建泛型适配器类型时,条件性符合尤其强大,泛型适配器类型旨在反映其类型参数的功能。例如,考虑Swift标准库集合的“lazy”功能:使用序列(sequence)的lazy成员生成符合序列协议的lazy适配器,而使用集合的lazy成员生成符合集合协议的lazy适配器。在swift3中,唯一的建模方法是使用不同的类型。例如,Swift标准库有四个类似的泛型类型来处理惰性集合:LazySequenceLazyCollectionLazyBidirectionalCollectionLazyRandomAccessCollection。Swift标准库使用lazy属性的重载来决定以下各项:

代码语言:javascript复制
extension Sequence {
  var lazy: LazySequence<Self> { ... }
}

extension Collection {
  var lazy: LazyCollection<Self> { ... }
}

extension BidirectionalCollection {
  var lazy: LazyBidirectionalCollection<Self> { ... }
}

extension RandomAccessCollection {
  var lazy: LazyRandomAccessCollection<Self> { ... }
}

这种方法会导致大量的重复,并且不能很好地扩展,因为每个功能更强的类型都必须重新实现(或者以某种方式转发实现)功能较弱的版本的所有API。有了条件性符合,就可以提供一个泛型包装器类型,它的基本需求满足最小公分母(例如,Sequence),但是它可以用类型参数来扩展它们的功能(例如,当类型参数符合Collection时,LazySequence就符合Collection,以此类推)。

基础运用

让我们从基础开始——如何声明对协议的条件性符合。假设我们正在开发一款具有可以将多种类型(可以是关卡,收藏品,敌人等)转换为得分的游戏。为了统一处理所有这些类型,我们定义了一个ScoreConvertible 协议:

代码语言:javascript复制
protocol ScoreConvertible {
    func computeScore() -> Int
}

使用上述协议时,很常见的一件事就是要处理值数组。在这种情况下,我们希望能够轻松地对包含ScoreConvertible值的数组的所有元素的总得分求和。给Array扩展的一种方法是在扩展的条件中的要求Element遵守ScoreConvertible,如下所示:

代码语言:javascript复制
extension Array where Element: ScoreConvertible {
    func computeScore() -> Int {
        return reduce(0) { result, element in
            result   element.computeScore()
        }
    }
}

上面的方法非常适用于一维数组,例如在汇总Level对象数组的总分时:

代码语言:javascript复制
let levels = [Level(id: "water-0"), Level(id: "water-1")]
let score = levels.computeScore()

但是,一旦我们开始处理更复杂的数组(例如,如果我们使用嵌套数组将关卡分组为世界),就会开始遇到问题。由于Array本身实际上并不符合ScoreConvertible协议,因此我们将无法为数组的数组计算总分。我们也不希望所有数组都符合ScoreConvertible,因为对于诸如[String][UIView]来说这是没有意义的。

这是条件性符合旨在解决的核心问题。现在,在Swift 4.1 以上,我们可以使得仅当它包含符合ScoreConvertible 协议的 Element 时,我们才使Array符合ScoreConvertible协议,就像这样:

代码语言:javascript复制
extension Array: ScoreConvertible where Element: ScoreConvertible {
    func computeScore() -> Int {
        return reduce(0) { result, element in
            result   element.computeScore()
        }
    }
}

这使得我们可以计算任意数量的包含符合ScoreConvertible协议的嵌套数组类型的总分:

代码语言:javascript复制
let worlds = [
    [Level(id: "water-0"), Level(id: "water-1")],
    [Level(id: "sand-0"), Level(id: "sand-1")],
    [Level(id: "lava-0"), Level(id: "lava-1")]
]

let totalScore = worlds.computeScore()

当我们在代码基础上迭代时,拥有这种级别的灵活性真是太棒了!

递归设计

条件一致性的最大好处是允许我们以更递归的方式设计代码和系统。通过嵌套类型和集合(如上面的示例所示),我们可以自由地以更灵活的方式构造对象和值。

Swift标准库中这种递归设计的一个最明显的好处是,包含Equatable类型的集合现在也可以自己进行Equatable。与上面的示例类似,我们现在可以自由地检查嵌套集合的相等性,而无需编写任何额外的代码。

代码语言:javascript复制
func didLoadArticles(_ articles: [String : [Article]]) {
    // 我们现在可以比较包含Equatable的嵌套集合
    // 只需使用 == 或 != 运算符。
    guard articles != currentArticles else {
        return
    }

    currentArticles = articles
    ...
}

尽管能够完成上述操作非常简单,但同样重要的是要记住,这样的相等性检查会隐藏复杂性,因为检查两个集合是否相等是一个O(n)操作。

应用实例 - 多重请求

现在让我们看一个更高级的例子,在这个例子中,我们将使用条件性符合来创建一个好的API来处理多个网络请求。我们将首先为请求定义一个协议,该协议可以返回包含任何ResponseResult类型,如下所示:

代码语言:javascript复制
protocol Request {
    associatedtype Response

    typealias Handler = (Result<Response>) -> Void

    func perform(then handler: @escaping Handler)
}

假设我们正在为一本杂志构建一个应用程序,让我们的用户可以阅读不同类别的文章。为了能够加载给定类别的项目数组,我们定义了符合上述请求协议的ArticleRequest类型:

代码语言:javascript复制
struct ArticleRequest: Request {
    typealias Response = [Article]

    let dataLoader: DataLoader
    let category: Category

    func perform(then handler: @escaping Handler) {
        let endpoint = Endpoint.articles(category)

        dataLoader.load(from: endpoint) { result in
            // 这里我们将结果<Data>值解码为错误或模型数组
            handler(result.decode())
        }
    }
}

就像我们在前面的示例中希望能够对多个ScoreConvertible值的总分求和一样,假设我们希望有一种简单的方法以同步方式执行多个请求。例如,我们可能希望一次加载多个类别的文章,然后得到一个包含所有组合结果的字典。

你也许能猜到这是怎么回事。通过条件性符合当字典的值符合Request协议时我们使Dictionary也符合Request协议,我们就可以用一种非常好的递归方式再次解决这个问题。

我们将使用GCD的 DispatchGroup 来同步我们的请求组并生成聚合结果,如下所示:

代码语言:javascript复制
extension Dictionary: Request where Value: Request {
    typealias Response = [Key : Value.Response]

    func perform(then handler: @escaping Handler) {
        var responses = [Key : Value.Response]()
        let group = DispatchGroup()

        for (key, request) in self {
            group.enter()

            request.perform { response in
                switch response {
                case .success(let value):
                    responses[key] = value
                    group.leave()
                case .error(let error):
                    handler(.error(error))
                }
            }
        }

        group.notify(queue: .main) {
            handler(.success(responses))
        }
    }
}

有了上述扩展,我们现在可以通过使用字典字面量轻松创建请求组:

代码语言:javascript复制
extension TopArticlesViewController {
    func loadArticles() {
        let requests: [Category : ArticleRequest] = [
            .news: ArticleRequest(dataLoader: dataLoader, category: .news),
            .sports: ArticleRequest(dataLoader: dataLoader, category: .sports)
        ]

        requests.perform { [weak self] result in
            switch result {
            case .success(let articles):
                for (category, articles) in articles {
                    self?.render(articles, in: category)
                }
            case .error(let error):
                self?.handle(error)
            }
        }
    }
}

我们现在可以使用一个统一的实现来组合多个请求,而不必为请求和集合的各种组合编写单独的实现.

参见 Swift - Evolution SE-0143 实例译自 John Sundell 的 Conditional conformances in Swift

0 人点赞