做者:Russ Bishop,原文连接,原文日期:2015-01-05
译者:靛青K;校对:shanks;定稿:Ceehtml
我想要一个关联类型的圣诞礼物ios
Swift 关联类型git
Swift 关联类型(续)github
有时候我认为类型理论是故意弄的很复杂,以及全部的那些函数式编程追随者都只是胡说八道,仿佛他们理解了其中的含义。真的吗?你有一篇 5000 字的博客是写关于插入随机类型理论概念的吗?毫无疑问,没有。并且这种理论没法阐述你必需要关注它的缘由,以及经过这种高大上的概念能解决什么样的问题。我想把你装进麻袋里,而后把麻袋扔到河里,最后把河丢到一个巨大的坑中。swift
咱们在讨论什么?固然,关联类型。安全
当我第一次看到 Swift 泛型的实现时,关联类型 的用法的出现,让我感到很奇怪。app
在这篇文章,我将经过类型概念和一些实践经验,这几乎都是我用本身的思考尝试解释这些概念(若是我犯了错误,请告诉我)。编程语言
在 Swift 中,若是我想有一个抽象的类型(也就是建立一个泛型的东西),在类中的语法是这个样子:ide
class Wat<T> { ... }
相似的,带泛型的结构体:
struct WatWat<T> { ... }
或者带泛型的枚举:
enum GoodDaySir<T> { ... }
但若是我想有一个抽象的协议:
protocol WellINever { typealias T }
嗯哼?
protocol 和 class、struct 以及 enum 不一样,它不支持泛型类型参数。代替支持抽象类型成员;在 Swift 术语中称做关联类型。尽管你能够用其它系统完成相似的事情,但这里有一些使用关联类型的好处(以及当前存在的一些缺点)。
协议中的一个关联类型表示:“我不知道具体类型是什么,一些服从个人类、结构体、枚举会帮我实现这个细节”。
你会很惊奇:“很是棒,但和类型参数有什么不一样呢?”。这是一个很好的问题。类型参数强迫每一个人知道相关的类型以及须要反复的指明该类型(当你在构建他们的时候,这会让你写不少的类型参数)。他们是公共接口的一部分。这些代码使用多种结构(类、结构体、枚举)的代码会肯定具体选择什么类型。
经过对比关联类型实现细节的部分。它被隐藏了,就像是一个类能够隐藏内部的实例变量。使用抽象的类型成员的目的是推迟指明具体类型的时机。和泛型不一样,它不是在实例化一个类或者结构体时指明具体类型,并且在服从该协议时,指明其具体类型。这让咱们多了一种选择类型的方式。
Scala 的建立者 Mark Odersky 在一次访谈中举了一个例子。在 Swift 术语中,若是没有关联类型的话,此时你有一个带有eat(f:Food)
的方法的基类或者协议 Animal
,以后的 Cow
类的没有办法指定 Food
只能是 Grass
。你很清楚不能经过重载这个方法 - 协变参数类型(在子类中添加一个更明确的参数)在大多数的语言都是不支持的,而且是一种不安全的方式 ,当从基类进行类型转换的时候可能获得意料以外的值。
译者注:关于协变,您能够参考这篇文章 Friday Q&A 2015-11-20:协变与逆变 。
若是 Swift 的协议已经支持类型参数,那代码大概是这个样子:
protocol Food { } class Grass : Food { } protocol Animal<F:Food> { func eat(f:F) } class Cow : Animal<Grass> { func eat(f:Grass) { ... } }
很是棒。那当咱们须要再增长些东西呢?
protocol Animal<F:Food, S:Supplement> { func eat(f:F) func supplement(s:S) } class Cow : Animal<Grass, Salt> { func eat(f:Grass) { ... } func supplement(s:Salt) { ... } }
增长了类型参数的数量是很不爽的,但这并非咱们的惟一问题。咱们处处泄露实现的细节,须要咱们去从新指明具体的类型。var c = Cow()
的类型就变成了 Cow<Grass,Salt>
。一个 doCowThings 方法将变成 func doCowThings(c:Cow<Grass,Salt>)
。那若是咱们想让全部的动物都吃草呢?而且咱们没有方式代表咱们不关心 Supplement
类型参数。
当咱们从 Cow
中得到了建立特别的品种,咱们的类就会很白痴的定义成这样:class Holstein<Food:Grass, Supplement:Salt> : Cow<Grass,Salt>
。
更糟糕的是,一个买食物来喂养这些动物的方法变成这个样子了:func buyFoodAndFeed<T,F where T:Animal<Food,Supplement>>(a:T, s:Store<F>)
。这真的很丑很啰嗦,咱们已经没法把 F
和 Food
关联起来了。若是咱们重写这个方法,咱们能够这样写func buyFoodAndFeed<F:Food,S:Supplement>(a:Animal<Food,Supplement>, s:Store<Food>)
,但这并不会有做用 - 当咱们尝试传入一个 Cow<Grass, Salt>
参数,Swift 会抱怨 ’Grass’ is not identical to ‘Food’
(’Grass’ 和 ‘Food’ 不相同)。再补充一点,注意到这个方法并不关心 Supplement
,但这里咱们却不得不处理它。
如今让咱们看看如何用关联类型帮咱们解决问题:
protocol Animal { typealias EdibleFood typealias SupplementKind func eat(f:EdibleFood) func supplement(s:SupplementKind) } class Cow : Animal { func eat(f: Grass) { ... } func supplement(s: Salt) { ... } } class Holstein : Cow { ... } func buyFoodAndFeed<T:Animal, S:Store where T.EdibleFood == S.FoodType>(a:T, s:S){ ... }
如今的类型签名清晰多了。Swift 指向这个关联类型,只是经过查找 Cow
的方法签名。咱们的 buyFoodAndFeed
方法,能够清晰的表达商店卖的食物是动物吃的食物。事实上,Cow 须要一个特别的食物类型,而这个具体实现是在 Cow 类里面,但这些信息仍然要在在编译时肯定。
讨论了一会关于动物的事情,让咱们再来看看 Swift 中的 CollectionType
。
笔记: 做为一个具体实现,许多 Swift 协议都有带前导下划线的嵌套协议;好比
CollectionType -> _CollectionType
或者SequenceType -> _Sequence_Type -> _SequenceType
。简单来讲,当咱们讨论这些协议时,我即将打平这些层级。因此当我说CollectionType
有ItemType
、IndexType
和GeneratorType
关联类型时,你并不能在协议CollectionType
自己中找到这些。
显然,咱们须要元素 T
的类型,但咱们也须要这个索引和生成器(generator)/计数器 (enumerator)的类型,这样咱们才能够处理 subscript(index:S) -> T { get }
和 func generate() -> G<T>
。若是咱们只是使用类型参数,惟一的方法就是提供一个带泛型的 Collection
协议,在一个假想的 CollectionOf<T,S,G>
中指明 T
S
G
。
其余语言是怎么处理的呢?C# 并无抽象类型成员。他首先处理这些是经过不支持任何东西而不是一个开放式的索引,这里的类型系统不会代表索引是否只能单向移动,是否支持随机存取等等。数字的索引就只是个整型,以及类型系统也只会代表这一信息。
其次,对于生成器 IEnumerable<T>
会生成一个 IEnumerator<T>
。起初这个不一样看起来很是的微妙,但 C# 的解决方案是用一个接口(协议)直接的抽象覆盖掉这个生成器,容许它避免必须去声明特别的生成器类型,做为一个参数,像 IEnumerable<T>
。
Swift 目的是作一个传统的编译系统(non-VM , non-JIT)编程语言,考虑到性能的需求,须要动态行为类型并非一个好主意。编译器真的倾向于知道你的索引和生成器的类型,以便于它能够作一些奇妙的事情,好比代码嵌入(inlining)以及知道须要分配多少内存这样奇妙的事情。
惟一的方法就是,经过香肠研磨机在编译时便利出全部的泛型。若是你强迫将它推迟到运行时,这也就意味着你须要一些间接的、装箱和其余的相似比较好的技巧,但这些都是有门槛的。
对于抽象类型成员,这儿有个大「坑」:Swift 不会彻底地让你肯定他们是变量仍是参数类型,毕竟这是没必要要的事情。只有在使用到泛型约束的时候,你才会用到带有关联类型的协议。
在咱们的以前的 Animal
例子中,调用 Animal().eat
是不安全的,由于它只是一个抽象的 EdibleFood
,而且咱们不知道这个具体的类型。
理论上,这些代码本应该能够工做的,只要泛型在这个方法上强迫动物吃商店销售的食物的约束,但实际上,当测试它的时候,我遇到了一些 EXC_BAD_ACCESS
的崩溃,我不肯定这是状况是否是由于编译器的问题。
func buyFoodAndFeed<T:Animal,S:StoreType where T.EdibleFood == S.FoodType>(a:T, s:S) { a.eat(s.buyFood()) //crash! }
咱们没有办法使用这些协议做为参数或者变量类型。这只是须要考虑的更远一些。这是一个我但愿在将来 Swift 会支持的一个特性。我但愿声明变量或者类型时可以写成这样的代码:
typealias GrassEatingAnimal = protocol<A:Animal where A.EdibleFood == Grass> var x:GrassEatingAnimal = ...
注意:使用 typealias
只是建立一个类型别名,而不是在协议中的关联类型。我知道这可能有些让人感受困惑。
这个语法将会让我能够声明持有关于一些动物中部分类型的一个变量,而这里的动物关联的 EdiableFoof
是 Grass
。这种语法在这种状况下也颇有用:若是在协议中约束其关联类型,但这看起来你可能会进入一个不安全的位置,致使须要考虑的更多一些。若是你开始运行时,有一件事,你须要约束关联类型在这个编译器的定义的协议不能安全的约束任何带泛型的方法(见下文)。
当前状况下,为了得到一个类型参数,你必须经过建立一个封装的结构体”擦除“其关联类型。进一步的警告:这很丑陋。
struct SpecificAnimal<F,S> : Animal { let _eat:(f:F)->() let _supplement:(s:S)->() init<A:Animal where A.EdibleFood == F, A.SupplementKind == S>(var _ selfie:A) { _eat = { selfie.eat($0) } _supplement = { selfie.supplement($0) } } func eat(f:F) { _eat(f:f) } func supplement(s:S) { _supplement(s:s) } }
若是你曾考虑过为何 Swift 标准库会包括 GeneratorOf<T>:Generator
、SequenceOf<T>:Sequence
和 SinkOf<T>:Sink
...我想如今你知道了。
我上面提到的这个 bug ,若是 Animal
指明了 typealias EdibleFood:Food
以后,即便你给它定义了 typealias EdibleFood:Food
,这个结构体仍然是没法编译的。即便是在结构体中进行了清晰的约束, Swift 将会抱怨 F
不是 Food
。详情能够见 rdar://19371678 。
就像咱们以前看到的,关联类型容许在编译时提供多个具体的类型,只要该类型服从对应的协议,从而不会用一堆类型参数污染类型定义。对于这个问题,它们是一个颇有趣的解决方案,用泛型类型参数表达出不一样类型的抽象成员。
更进一步考虑,我在想,若是采起 Scala 的方案,简单的为 class、struct、enum 以及 protocol 提供类型参数和关联类型两个方法会是否更好一些。我尚未进行更深刻的思考,因此还有一些想法就先不讨论了。对于一个新语言最让人兴奋的部分是——关注它的发展以及改进进度。
如今走的更远一些,而且向你的同事开始炫耀相似抽象类型成员的东西。以后你也能够称霸他们,讲一些很难理解的东西。
要远离麻袋。
还有河水。
没有坑,坑是使人惊奇的。
本文由 SwiftGG 翻译组翻译,已经得到做者翻译受权,最新文章请访问 http://swift.gg。