为什么 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<T: Equatable>(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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342