我从55个Swift标准库协议中学到了什么?
-
本文由CocoaChina译者@ALEX吴浩文翻译
-
作者: Greg Heo
Swift团队使用协议的方法,给了我们哪些使用协议的提示?
好的。55个Swift标准库公有协议,18分钟,让我们开始吧。
首先我只想问:为什么是协议?为什么面向协议编程?如果我们回到过去那段年少无知少不更事的面相对象编程时期,我们很多人最初学习的是Objective-C,这意味着我们免受多继承的专横。又或者你是这个房间里另一半喜欢C++的人,那么我们并没有受过多继承的启示,我们稍后将对其进行讨论。
单继承中,层次结构是线性的:你有父辈、子辈以及孙子辈一系列的继承树。当你回到树的顶端,所有的一切有一个单独的父辈。这使得层次干净,但同时你的确失去了合理使用多继承所带来的优势。在Swift中不能继承枚举和结构类型,只有类可以。这意味着你有时需要弄得跟麻花一样来让你的类型有意义。这样最终能得到真正通用的超类。然后一级一级下来,如果你可以想象更多的级在你从图中获得一个叶子结点之前,你才能得到一个真正可以实例化和使用的类。
"你有时需要弄得跟麻花一样来让你的类型有意义"
所以通过协议,你可以使得类型系统更加有组合性,你可以理清继承的长链。当然你将要放弃一个长而高的继承链,为了一个有协议一致性的宽的链。但我认为取舍是值得的,希望在这个演讲结束后你也这么认为。
什么样的东西放进协议里有意义?我不打算谈我已经完成的很酷的协议,而是浏览一下苹果在Swift标准库中提供的协议。我们将浏览,也许你会学到一些你以前从未听过的协议。我们究竟能找到什么伟大的想法,接着也许可以获得一些灵感——在我们的代码里什么样的东西可以和协议一起用。基本上这个演讲的思路是Swift团队使用协议的方法,给我们一些可以怎么用协议的提示?
最终目标是让你开始思考协议,想出一些很酷的点子然后开源,然后我会在你GitHub的仓库上点星。
三种协议类别:"Can do","Is a"以及"Can be"
Swift标准库包括54个真正的共有协议。一开始这个演讲我开玩笑地想一个个说明它们,每个16秒来凑时间,但相反,我把它们分成三类。"Can do"协议,"Is a"协议,"Can be"协议。每次我们会看其中一个,看一些示例,再看看我们能够学到的东西。
"Can do"协议
首先我们先来看看"Can do"协议。这些描述的事情是类型可以做或已经做过的,它们也以 -able 结尾为语法,这使得当你浏览头文件时它们很容易被找到。第一个例子:一个类型遵守Hashable协议,意味着你可以得到这个类的整型散列值。这意味着你可以把它存储进一个集合,可以当作一个字典的键等等。还有Equatable和Comparable协议,这意味着你可以用Swift中各种相等和比较的运算符比较两个实例的值。这些都是很常见的协议,你可能已经在你自己的类型中实现了。你会注意到这些协议描述了你可以对类进行的操作。有比较、相等、散列。
"AbsoluteValuable(绝对有价值)。这听起来如此重要。正因为它以'valuable(价值)'结尾"
这里做一个补充说明,让我们来谈谈我认为的最好的命名协议,它值得一个特殊的过度动画:AbsoluteValuable(绝对有价值)。这听起来如此重要。正因为它以"valuable(价值)"结尾。但不幸的是它没有听起来那么重要。它只意味着支持绝对值函数。
还有一个小协议子集在这个"Can do"组中,和替代视图或替代表示形式有关。
让我们看看一个简单的例子RawRepresentable。这意味着该类可以表示成某种原始值,然后你可以把原始值转变回实际的实例。这听起来很像Swift里的枚举,有内置的原始值。所以所有有枚举的功能的类有一个初始化函数,它接受一个原始值,一旦你有一个枚举值就得到它原始值的版本。这些都建立在这个协议之上。你可以用自己的结构体和类做类似的事情,如果你喜欢。这里的想法是,东西内在的值是一样的,你只是改变它外在的表示。只因为,原始值和实例值之间存在一对一的映射。
同属此类。
接下来是CustomPlaygroundQuickLookable。这只是意味着你类型可以在playground中快速查看。同时这也意味着,你的类型是一样的,你不是将它转换为其他东西,而是为你的值提供另一种替代视图。在这种情况下,它可以在快速浏览中显示。
"所以我们有操作,我们有替代试图。从中我们可以学到什么呢?"
如果你自己的类型有操作,比如说你曾正在写下一代Instagram杀手级照片过滤APP,你可以添加一个过滤性的协议,然后让你的照片实例、图像去遵守它。假设之后,你的过滤APP火了,它真的流行起来,你想要扩展到视频。然而视频只是另一种形式的媒体。在理论上,你还可以应用过滤性的协议在视频、音频、3D照片上,无论将来出现了什么。
替代视图的例子呢?总存在从大照片创建缩略图的情况,你可以认为这是全尺寸照片的替代视图。再次,这实际上不是一个转换,它只是替代的表示形式。所以,你可以想象这样一个Thumbnailable协议,希望你能想出一个更好的名字,甚至音频版本的一个缩略图。缩略图就像一个低比特率版本的音频之类的。
这里的基本思想,是把你APP和代码中常见的操作抽象出来,协议化他们,如果有这个词。为什么你要这么做?一个好处是使得想法可重用。你有几种类型需要实现一些常见的操作,现在他们可以共享同一个有保证的公共的接口。你可以得到多态的好处,即使是在你的结构和枚举里。而且,这种复合的方法可以帮助你从操作中分割东西出来。我知道这里意见会不同,但我喜欢从这样的小块来建立一个类型,基于它们能做什么。这是第一类的协议、操作、替代视图。你可以建立的操作和视图的集合用于您的类型。
"Is a"协议
下一类是"Is a"协议,这些描述类型是什么样的。与"Can do"的协议相比,这些更基于身份。这意味着遵守多个协议,感觉很像Swift的多继承。您可以在标准库中找到这些协议,因为它们以"-type"结尾。它们占了整整一半标准库,54个中有35个左右的是这类的。让我们来看一个例子。CollectionType是个好协议。显然Array、Dictionary、Set遵守CollectionType。或许更令人惊讶的是,Ranges和String views。如果你有一个字符串,你可以获得它的UTF-8或UTF-16的表示形式。所以,它只是一系列的Unicode代码点。所以,它也遵守CollectionType。
还有一些协议包括的一些原语像IntegerTyp、FloatingPointType、BooleanType等等。这些协议更像分组。所以有几个整数类型。我们有无符号整数、有符号整数、16位整数等等。但它们都组合在一起,因为他们共同遵守整数类型的协议。如果你想出自己的整数类型,也许你想要一个4位整数或6位整数,有点非主流,那么为什么不遵守这个协议。但是,这不大可能发生。大多数这类协议你可能从来没有编写类型去遵守这些协议。例如你看BooleanType头文件的注释,实际上劝阻你创造更多的布尔类型,因为一个已足够。另一个例子是MirrorPathType,它的头文件有以下令人愉快的注释:"不要声明新的类遵守这个协议,它们不会像预期的那样工作。”
所以,正如你所看到的,这意味着许多这一类的协议,你可能是使用遵守这些的类型。我相信每个人都使用一个数组或一个整数,但你可能不会创建遵守它们的类型。虽然有一些你可能使用——我们有ErrorType,我们早些时候听Thomas说过,在Swift 2中新的错误处理模式用。还有SequenceType、GeneratorType,如果你正在构建集合化的东西且你想要支持迭代,可以看一看这些协议。
这就是"Is a"协议。协议被当作身份。我们可以从这个模式的协议中学到什么吗?再次,因为这些都是基于身份而不是操作,你可以在更大的类型分组中使用它们。回到规范化动物王国的例子:这是一个夸张的很长的类层次结构。底部甚至没有一种我们可以实例化的类型。在这个类层次结构上,每一步都比之前添加一些功能。因此,有了协议你可以让你的类型系统有更多的组合性。你有这个协议的清单,你可以构建和在不同类型中使用。比如猫叫和狗吠,更多是一种"Can do"风格的动物能做的事,而"两条腿"和"四条腿"则更多是一种身份类型。你会注意到两条腿和四条腿也有继承,因为协议可以像这样继承。
这意味着,一旦你设置好了这些协议,你可以建立起过去需要巨大的超类列表而现在只是一组协议的东西,包括继承如果你需要它。当你构建你的类型,你可以在这里选择身份和需要的功能。因为你的类型可以遵守多个协议,你可以一点点建立类型的功能,基于协议的一致性。
这就是第二类的标准库协议--"Is a"协议,与分组和身份有关。
"Can be"协议(11.28)
最后我们有"Can be"类型。这不是同一个东西的替代视图,正如我们已经看到的,这些都是直接转换。从类型X转换到Y。这些协议以"-Convertible"结尾。这意味着这个类型可以被转换到或者转换成别的东西。
让我们看看几个例子。我们有简单的初始化风格的,如FloatLiteralConvertible、IntegerLiteralConvertible、ArrayLiteralConvertible等等。如果你的类型遵守FloatLiteralConvertible那么这意味着你需要一个初始化函数,接受某种floatLiteral,默认情况下这是一个double值,然后构建你的类型。所以转换的方向是从一个浮点数到你的类型。
相比之下,有每个人的最好的朋友CustomStringConvertible之类的协议,又或如先前知道的Printable协议。它指定你的类型可以转换成一个字符串,所以转换变到另一个方向,是从你的类型中到一个字符串。
"一个Objective-C从业者看到,说'啊,那没什么。我码方法声明的时间比每次我码代码的时间都长。'"
这里再次补充说明,54个协议中名称最长的也在此类,ExtendedGraphemeClusterLiteralConvertible——41个字符。我相信那些来自Objective-C会说、或笑、就像:"啊,那没什么。我码方法声明的时间比每次我码代码的时间都长。"这就是用于转换的协议在这最后一组。我们可以从这样的协议中学到什么,除了要尽量保持你类型的名字短?
这是很明显的。如果你有类型,它可以成为其他类型,那就不要只添加一个函数,或添加一个被计算的属性,或添加一个初始化函数。考虑设置一个协议。记住你可以使用协议指定你的类型被转换到或转换成别的。所需的其他任何技术讨论的例子,除了动物,是员工数据库。如果你有对象表示人、普通员工、经理、承包商,那么这些人可能是一个单独的类型。如果承包商可以被雇佣而作为一个员工,或员工可以被晋升为经理,那这就是一种转换。你不想再加入人的名称、地址、电话号码和社会安全号等等。你想相对无缝地把一个承包商转换成员工。你可以用一个EmployeeConvertible协议。然后说承包商类型和应试者类型能遵守它。
这么做的好处是什么?为什么需要一个协议加上转换函数,而似乎仅仅一个函数不是更简单吗?再次,它一部分是组合的方法。一个应试者可以成为员工的事实,是类型是什么的一部分,但这并不特别。其他的人也可以成为员工。通过使用一个协议可以保证有一个共同的定义良好的接口来将一些人转换成一个员工。
还有一个好处是,代码漂亮的像文档一样。如果你浏览代码,或者项目里的其他人,你会看到"EmployeeConvertible",并且你已经熟悉它,它告诉了你这类型能做什么和接口是什么样子。你还可以在你的项目查找"EmployeeConvertible"这个词,然后在搜索结果中,就可以看到能成为员工的类型的列表。这就是"Can be"协议组合,处理你类型之间的转换。
四个广泛模式
所以我们看到三类来自标准库的协议,它们与能力、身份和转换有关。什么是广泛的模式,想想我们自己的代码?我们有四个:
-
操作--如果有一组通用的操作你必须在类型中执行,考虑抽象出来当作协议。
-
与替代视图有关--如果你的类型有替代视图,或另一种表示形式,不是一个完整的转换,想想它是否遵守公有协议。
-
代表身份--这是你做类似多继承的地方,或混合多种类型(Mix-ins)的类型。考虑身份和类型,把相似的类型用协议来分组。
-
最后我们有转换,无论是从一个类型转换成别的,还是一个类型被转换到,如果特定的转换在代码中多次发生,考虑抽象很常见的转换作为协议,这帮助你跟踪事物,并保持一致的接口。
我想,看到苹果把如此多的常用功能,如映射、过滤器、枚举函数、抽象到协议,使用的也只是普通的旧协议和协议扩展,这是一个很好的例子。一个未来将如何强大的灵感。苹果正遵循这个榜样。例如,如果你看看数组的定义,它遵守8个协议,字符串遵守12协议,等等。所以这里的想法是,你创建这些特征打包在协议里,然后你的你的代码库就都可以使用。
我认为以这种方式思考你的类型,可以帮助你在脑海里保持清晰和并把这些类型分类。所以我肯定鼓励在你自己的代码里尝试这些。仔细看看你的类型,聚焦于它们的共同点,看能不能用协议。
以上就是我全部要说的。协议万岁,谢谢!
Q&A (17:47)
Q1:你好,又是我--Dave Ungar。演讲让我思考我自己的代码,当不用协议时。例如我有一个结构体,它有10个函数。也许我应该用10个协议。具体来说来自标准库,并没有字符串的协议。因此我不得不创造一个,为了把一个Where子句转换成别的一般化的东西,参数可以是一个字符串。所以你能说说看,是何时不该使用协议,还是为什么当我需要时没有一个字符串协议?谢谢。
Greg:苹果就是这样,对吧?从不满足每个人的需求。但是我认为何时用何时不用,我正在使用的方法是根据普遍性来分类。如果我只有一个类型遵守这个协议,很大程度上我就留给这个类型去处理。如果这个类型做10件事那就做10件吧。但如果它甚至发生两次,只要不止一次,对我来说这就足够去说"好吧,这是普遍得足以发生两次",似乎是一个奇迹的重复,也许这将发生不只一次。所以这是一个主观判断的问题。但是我想说,不止发生一次是至少的要求。
观众:天哪。所以值得复制函数、声明和所有的东西去得到协议。
Greg:没错,就是这样。
Q2:你好。我想知道,你处理过泛型协议吗?如果有,你的策略是什么?
Greg:我认为我有这样一个例子,因为协议不能有泛型,但你可以取类型别名,正如你常看到的,你只是定义类型别名。如果你想,你可以称之为类型别名T。如果你想让它看起来像泛型。然后当你定义类型你只需要定义类型别名作为具体类型。这是我在协议里见过的方法。
观众:我认为,有时情况就变得有点困难,比如如果你声明一个变量,然后你说"我想让这个变量遵守这个协议。"如果这协议有一个类型别名,你不能这么做,因为编译器不喜欢它。
Greg:是这样,它更像是如果你定义你自己的类型,然后你可以给类型一个别名那么这也是一种选择。但我还没找到一个更好的在变量情况下的通用解决方案。
Q3:非常感谢你这次演讲,我很喜欢它。这样思考协议似乎让我更加清晰了。我的问题是,你说的有4次你可以使用协议的地方,或思考协议的地方,操作,替代视图,身份和转换。我的问题是,你能说清一下替代视图,替代试图和转换之前的区别。我通常思考的方式是,有时我可以将类型转换为替代视图,但是你似乎把它区分开来了,对吧?
Greg:好的,我听懂了你的问题。问题的一部分是因为协议的命名方式。也许它们的命名糟糕。再次,Printable曾是一个 -able 协议,现在却是CustomStringConvertible。这是临阵倒戈,对吧?但我认为,我看它是以替代视图的含义,事物本身并没有改变,只是外在的样子正在发生变化。从全尺寸图像到缩略图,或从枚举值到原始值,它们其实是一样的。只是你透过一个不同的镜头。然而转换协议更像是"我有一个整数,我将变为一个字符串。"这是CustomStringConvertible,对吧?你把它改变成了一个完全不同的类型,这不仅仅是一个有自己东西的不同的视图,它会有自己的生命周期。我就是这样看的。
-
本文仅用于学习和交流目的,转载请注明文章译者、出处以及本文链接。
-
感谢博文视点对本期翻译活动的支持