专栏首页韦弦的偶尔分享Swift 泛型之条件性符合协议

Swift 泛型之条件性符合协议

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

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

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属性的重载来决定以下各项:

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 协议:

protocol ScoreConvertible {
    func computeScore() -> Int
}

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

extension Array where Element: ScoreConvertible {
    func computeScore() -> Int {
        return reduce(0) { result, element in
            result + element.computeScore()
        }
    }
}

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

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协议,就像这样:

extension Array: ScoreConvertible where Element: ScoreConvertible {
    func computeScore() -> Int {
        return reduce(0) { result, element in
            result + element.computeScore()
        }
    }
}

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

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。与上面的示例类似,我们现在可以自由地检查嵌套集合的相等性,而无需编写任何额外的代码。

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

    currentArticles = articles
    ...
}

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

应用实例 - 多重请求

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

protocol Request {
    associatedtype Response

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

    func perform(then handler: @escaping Handler)
}

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

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 来同步我们的请求组并生成聚合结果,如下所示:

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))
        }
    }
}

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

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 条评论
登录 后参与评论

相关文章

  • swift4.0语法杂记(精简版)

    一、swift简史 1、介绍 swift是苹果公司于2014年推出用于撰写OS和iOS应用程序的语言。它由苹果开发者工具部门总监“克里斯.拉特纳”在2010年...

    谦谦君子修罗刀
  • Swift 中风味各异的类型擦除

    Swift的总体目标是强大得足以用于低级(low-level)系统编程,又足够容易以便初学者学习,有时会导致非常有趣的情况——当 Swift 功能强大的类型系统...

    韦弦zhy
  • Swift4语法新特性 原

          随着iPhone X的来到,iOS11的发布,Swift语言也更新到了第4个版本。在Swift4中,无论是代码风格还是编程理念都更进一步的融合了许多...

    珲少
  • 【面试必备】Swift 面试题及其答案

    答案:optional类型被用来表示任何类型的变量都可以表示缺少值。在Objective-C中,引用类型的变量是可以缺少值得,并且使用nil作为缺少值。基本的数...

    编程怪才-凌雨画
  • Swift进阶一:Swift简介

    Swift语言引入了协议、协议的扩展、泛型等新特性,因此使用Swift语言可以很好地面向协议编程;Swift语言将函数和闭包提升为语言的一等公民,函数可以作为一...

    拉维
  • Swift 4.0 新特性

    WWDC 2017 带来了很多惊喜,在这次大会上,Swift 4 也伴随着 Xcode 9 测试版来到了我们的面前,虽然正式版要8月底9月初才会公布,但很多强大...

    xiangzhihong
  • Hacking with iOS: SwiftUI Edition - 里程碑:项目 7 - 9

    希望您觉得这些项目开始让您有所收获,不仅可以进一步提高您的SwiftUI技能,还可以教给您一些更高级的Swift。另外,当然,您还创建了两个新的SwiftUI项...

    韦弦zhy
  • Swift学习:泛型

    本篇将详细总结介绍Swift泛型的用法; Swift泛型代码让你能够根据自定义的需求,编写出适用于任意类型、灵活可重用的函数及类型。它能让你避免代码的重复,用...

    梧雨北辰
  • WWV 2018年十大必看视频

    我们汇集了十大WWDC 2018视频列表,涵盖了您需要知道的一切,包括Core ML,Siri Shortcuts,ARKit 2等等!

    iOSDevLog

扫码关注云+社区

领取腾讯云代金券