为什么 Swift 关联类型的协议需要做为泛型约束使用(译)

一、OC 协议:发消息

OC 的协议本质是消息的集合。例如,UITableViewDataSource 协议有请求 sections 个数和一个 sections 中的 rows 的个数。

二、Swift 协议:消息 + 关联类型

Swift 协议也是消息的集合。Swift 的协议同时也包含关联类型。协议中的关联类型是类型占位符,实现协议的时候来填充具体的类型。

三、关联类型:简化协议的实现

关联类型是一个强大的工具,让我们更方便的实现协议。

例子:Swift Equatable 协议

例如,Swift Equatable 协议有一个比较两个值是否相等的函数。

static func ==(lhs: Self, rhs: Self) -> Bool

这个函数使用了 Self 这个关联类型。Self 的具体类型是由协议的具体实现来确定。假设有一个类型下面的结构体,那么这个时候 Self 就是 Name。

struct Name: Equatable {
    let value: String
}

这个时候,协议的具体实现如下:

static func ==(lhs: Name, rhs: Name) -> Bool

综上:Equatable 使用关联类型 Self 来约束比较相同类型的值的 == 函数。

四、对比:OC NSObjectProtocol

NSObjectProtocol 也有一个 isEqual(_:)方法,但是因为是 OC 的协议,不能用 Self 类型。具体的定义如下:

func isEqual(_ object: Any?) -> Bool        

OC 的协议无法约束参数类型为指定关联类型,因此所有遵守协议的类型都能用来比较。通常在实现中会先检测参数的类型与消息的接收者类型是否一致。

func isEqual(_ object: Any?) -> Bool {
    guard let other = object as? Name else { return false } 
    
    // 开始检查值是否相等
}

每一个 isEqual(_:) 的实现,在每次调用的时候都要做这样的检查。Equatable 协议不会做这样的检测,通过 Self 关联类型保证了对象满足条件。

五、代价

Protocol ‘SomeProtocol’ can only be used as a generic constraint because it has Self or associated type requirements.

因为需要实现 Self 或者其他关联类型,该协议只能用做泛型约束。

关联类型是个强大的工具。所付出的代价就是

error: protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements

协议中使用关联类型,必须用做泛型。

泛型也是一个占位符,调用泛型函数的时候,必须填充对应的类型。

泛型关联类型的调用和实现两个方面对比来看:

  • 关联类型:实现方指定类型,调用方不指定。当你实现一个使用关联类型的函数的时候,你需要填充对应的类型,所以你知道实际的类型。调用方不知道你具体采用的类型。

  • 泛型:调用方指定类型,实现方不指定。当你实现一个使用泛型的函数,不需要知道调用方具体采用的类型。你可以使用约束限制类型,但是你必须处理所有满足约束的类型。调用方指定具体的类型,但是你的代码必须处理传递的任意类型。

例子: 调用 Equatable == 强制使用泛型
func checkEquals(left: Equatable, right: Equatable) -> Bool {
    return left == right
}

Swift 编译器调出如下错误:

error: MyPlaygroundSwift.playground:27:24: error: protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements
func checkEquals(left: Equatable, right: Equatable) -> Bool {
                       ^

error: MyPlaygroundSwift.playground:27:42: error: protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements
func checkEquals(left: Equatable, right: Equatable) -> Bool {
为什么不使用泛型,checkEquals 不起作用?

如果 swift 允许这样做会怎么样,我们来做个实验。

假如有两个遵守 Equatable 的类型,Name 和 Age。代码可能会是这样。

let name = Name(value: "")
let age = Age(value: 0)
let isEquals = checkEquals(name, age)

这样做没有任何意义,从以下两个方面来看:

  • 行为:怎样运行这段代码?怎样实现 checkEquals 中的 == 调用。只有 (Names, Names) 和 (Age, Age) 才有意义,因为 Equatable 定义为 ==(Self, Self)。只调用 Name 或者 Age 的 == 会打破类型安全。

  • 意义:有什么意义? Equatable 协议不仅仅是个类型,它还和另一个类型 Self 有关。如果你写checkEquals(left: Equatable, right: Equatable),只是在讨论 Equatable,它的关联类型Self被忽略。不能只是明确Equtable,必须明确Equtable where Self is (some type)

这非常蛋疼,但是很重要,checkEquals 看起来会工作。你想比较两个 Equtable 类型。但是Equtable是一个不完整的类型,它其实是equtable for some type

checkEquals(left: Equatable, right: Equatable)中左边是一个equtable for some type,右边也是一个equtable for some type。左右两边并不是equatable for the same typeEquatable ==需要左右同一个类型,所以上面的情况checkEquals不起作用。

使 checkEquals 处理所有的 Equatable + Self

checkEqualsEquatable where Self is (some type)不知道具体的some type。所以,必须处理所有的Equatable 和 Self 类型checkEquals所有的类型TTEquatable和它的关联类型

具体的代码如下:

func checkEquals(left: T, right: T) -> Bool {
    return left == right
}

现在,类型 T是一个Equatable类型,T的关联类型Self,实现了checkEquatable方法。使用 Swift 的泛型为具体的类型实现了一个配方,不需要写具体的checkEquals(left: Name, right: Name)checkEquals(left: Age, right: Age)。最后需要提取泛型函数来重构代码。

例子:调用 NSObjectProtocol 的 isEqual(_:) 不需要泛型

使用NSObjectProtocolcheckEquals不需要使用泛型。

import Foundation

func checkEquals(
  left: NSObjectProtocol,
  right: NSObjectProtocol
) -> Bool {
  return left.isEqual(right)
}

写起来很简单,这种情况下还是允许我们以下调用:

let isEqual = checkEquals(name, age)

Name 可以和 Age 比较?�当然不能,isEqual结果是false。Name.isEqual(_:) 会判断对象是不是 Name 类型。不像Equabable==,所以每个isEqual(:)方法必须处理类型一致性。

六、权衡

关联类型使 Swift 的协议比 OC 的更加强大。

OC 的协议捕获了对象和它的调用者间的关系。调用者能够使用协议发消息,消息的具体行为由遵守协议的对象来实现。

Swift 协议也能够捕获一个类型和多个关联类型之间的关系。Equatable协议通过Self关联一个类型。SetAlgebra协议将实现关联到类型Element

通过对比Equatable==NSObjectProtocolisEqual(:),能够发现 Swift 实现协议比较简单,但是在使用协议的时候比较复杂。强大的实现也会使代码变得很复杂,所以在使用协议的时候,需要权衡使用关联类型的价值和处理他们的复杂程度。

希望通过这篇文章帮助你认识使用的协议和 API。如果你觉得有用,可以关注我们的高级 Swift 训练营。

七、刨根问底:Self 是关联类型么?

Self 的行为很像关联类型,不同于其他关联类型,不需要指定 Self 关联的类型,Self 会自动关联到实现协议的类型。

但是协议中的错误提示是“有 Self 或者关联类型”,听起来很像是不同的类型。

为了找到答案,我找到了 Swift 编译器中 AST 的源码,具体的文档在这里

每个协议有一个隐式构造的关联类型 Self,描述了遵守协议的类型。

所以,Self 是一个关联类型

八、感谢原作者Jeremy Sherman,原文地址

https://www.bignerdranch.com/blog/why-associated-type-requirements-become-generic-constraints/

你可能感兴趣的:(为什么 Swift 关联类型的协议需要做为泛型约束使用(译))