Swift 关联类型
关联类型系列文章
有时候我认为类型理论是故意弄的很复杂,以及所有的那些函数式编程追随者都只是胡说八道,仿佛他们理解了其中的含义。真的吗?你有一篇 5000 字的博客是写关于插入随机类型理论概念的吗?毫无疑问,没有。而且这种理论无法阐述你必须要关注它的原因,以及通过这种高大上的概念能解决什么样的问题。我想把你装进麻袋里,然后把麻袋扔到河里,最后把河丢到一个巨大的坑中。 我们在讨论什么?当然,关联类型。 当我第一次看到 Swift 泛型的实现时,关联类型 的用法的出现,让我感到很奇怪。 在这篇文章,我将通过类型概念和一些实践经验,这几乎都是我用自己的思考尝试解释这些概念(如果我犯了错误,请告诉我)。 泛型在 Swift 中,如果我想有一个抽象的类型(也就是创建一个泛型的东西),在类中的语法是这个样子: class Wat<T> { ... } 类似的,带泛型的结构体: struct WatWat<T> { ... } 或者带泛型的枚举: enum GoodDaySir<T> { ... } 但如果我想有一个抽象的协议: protocol WellINever { ? ?typealias T } 嗯哼? 基本概念protocol 和 class、struct 以及 enum 不同,它不支持泛型类型参数。代替支持抽象类型成员;在 Swift 术语中称作关联类型。尽管你可以用其它系统完成类似的事情,但这里有一些使用关联类型的好处(以及当前存在的一些缺点)。 协议中的一个关联类型表示:“我不知道具体类型是什么,一些服从我的类、结构体、枚举会帮我实现这个细节”。 你会很惊奇:“非常棒,但和类型参数有什么不同呢?”。这是一个很好的问题。类型参数强迫每个人知道相关的类型以及需要反复的指明该类型(当你在构建他们的时候,这会让你写很多的类型参数)。他们是公共接口的一部分。这些代码使用多种结构(类、结构体、枚举)的代码会确定具体选择什么类型。 通过对比关联类型实现细节的部分。它被隐藏了,就像是一个类可以隐藏内部的实例变量。使用抽象的类型成员的目的是推迟指明具体类型的时机。和泛型不同,它不是在实例化一个类或者结构体时指明具体类型,而且在服从该协议时,指明其具体类型。这让我们多了一种选择类型的方式。 有用的Scala 的创建者 Mark Odersky 在一次访谈中举了一个例子。在 Swift 术语中,如果没有关联类型的话,此时你有一个带有
如果 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) { ... } } 增加了类型参数的数量是很不爽的,但这并不是我们的唯一问题。我们到处泄露实现的细节,需要我们去重新指明具体的类型。 当我们从 更糟糕的是,一个买食物来喂养这些动物的方法变成这个样子了: 现在让我们看看如何用关联类型帮我们解决问题: 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 指向这个关联类型,只是通过查找 真实的例子讨论了一会关于动物的事情,让我们再来看看 Swift 中的
显然,我们需要元素 其他语言是怎么处理的呢?C# 并没有抽象类型成员。他首先处理这些是通过不支持任何东西而不是一个开放式的索引,这里的类型系统不会表明索引是否只能单向移动,是否支持随机存取等等。数字的索引就只是个整型,以及类型系统也只会表明这一信息。 其次,对于生成器 Swift 目的是做一个传统的编译系统(non-VM , non-JIT)编程语言,考虑到性能的需求,需要动态行为类型并不是一个好主意。编译器真的倾向于知道你的索引和生成器的类型,以便于它可以做一些奇妙的事情,比如代码嵌入(inlining)以及知道需要分配多少内存这样奇妙的事情。 丑陋的事实对于抽象类型成员,这儿有个大「坑」:Swift 不会完全地让你确定他们是变量还是参数类型,毕竟这是不必要的事情。只有在使用到泛型约束的时候,你才会用到带有关联类型的协议。 在我们的之前的 理论上,这些代码本应该可以工作的,只要泛型在这个方法上强迫动物吃商店销售的食物的约束,但实际上,当测试它的时候,我遇到了一些 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 = ... 注意:使用 这个语法将会让我可以声明持有关于一些动物中部分类型的一个变量,而这里的动物关联的 当前情况下,为了获得一个类型参数,你必须通过创建一个封装的结构体”擦除“其关联类型。进一步的警告:这很丑陋。 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 标准库会包括 我上面提到的这个 bug ,如果 总结就像我们之前看到的,关联类型允许在编译时提供多个具体的类型,只要该类型服从对应的协议,从而不会用一堆类型参数污染类型定义。对于这个问题,它们是一个很有趣的解决方案,用泛型类型参数表达出不同类型的抽象成员。 更进一步考虑,我在想,如果采取 Scala 的方案,简单的为 class、struct、enum 以及 protocol 提供类型参数和关联类型两个方法会是否更好一些。我还没有进行更深入的思考,所以还有一些想法就先不讨论了。对于一个新语言最让人兴奋的部分是——关注它的发展以及改进进度。 现在走的更远一些,并且向你的同事开始炫耀类似抽象类型成员的东西。之后你也可以称霸他们,讲一些很难理解的东西。 要远离麻袋。 还有河水。 没有坑,坑是令人惊奇的。
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |