泛型与关联类型
和其他我学过的语言相比较,Rust有一些令人费解的概念。借用,所有权,借用检查这些概念大家应该已经都听说过了,我自己曾花费数小时在生命期问题上,最终不得不放弃抗争,转而采用Clone来解决。 关联类型虽然不是什么令人抓狂的概念,但我还是尝试了很多工作来试图正确的理解它,或者说至少我认为我自己理解了。TL;DR:
一个关于何时使用泛型何时使用关联类型的粗略答案是:如果针对特定类型的trait有多个实现(例如From本文目标和限制
本文的目的是解释泛型和关联类型的相似与不同之处。特别是针对trait,因为关联类型主要用于trait。 此外,虽然我们在讨论关联类型,但是我们不会涉及泛关联类型(generic associated types)。如果你对这一主题感兴趣,可以参考下RFC。 如果读完本文,你还是不太理解我所说的,建议阅读下Rust Book的 高级Traits章节,特别是关于关联类型。 最后,阅读本文需要你有一些编程经验(Rust),以及基本的泛型编程思想。关于Rust中的泛型,可以参考10.1 泛型。定义
为了确保我们的理解一致,先来定义一些基本概念。泛型(Generic Types)
在trait上下文中, 泛型又被称作类型参数(type parameters),用于在具体实现trait时使用的类型。类型参数可以是完全开放的,或者受限于特定trait。 例如 std::convert::From关联类型(Associated Types)
关联类型,如同其名称所暗示,是指关联至某个trait的类型。当你定义该trait时,类型未指定,这一点和泛型很相似。同时你也可以对类型增加trait限制。 一个使用关联类型trait的重要例子是:Iterator。它有一个关联类型Item以及一个函数next。next返回Option语法
更进一步之前,我们来浏览下这些概念的语法。我们尽量采用较少的抽象。此处定义两个traits: Generic和Associated,分别使用泛型和关联类型,并且观察使用trait限定和默认类型。基础traits
使用类型参数化(type-parameterized)的trait:使用关联类型的trait:
trait Generic<T> {
fn get(&self) -> T;
}
注意观察两种定义的不同,类型T如何从泛型参数变为了trait自身定义的一部分,在关联类型中,我们无法直接像泛型一样直接使用T,而是使用Self::T。
trait Associated {
type T;
fn get(&self) -> Self::T;
}
加上trait限制
如果我们想对泛型参数或者关联类型加以特定trait限制定义,可以使用Rust常用的:表达式(bounds)。 例如限定类型必须实现了<font style="color:rgb(34, 34, 34);">core::fmt::Display</font>
trait:
同样的,对于关联类型:
trait Generic<T: Display> { fn get(&self) -> T; } // or using the `where` keyword trait Generic<T>
where T: Display, { fn get(&self) -> T; }
trait Associated {
type T: Display;
fn get(&self) -> Self::T;
}
默认类型
Rust一个很酷的特性是可以指定泛型的默认类型,通常使用默认类型,某些特殊情况使用重载类型。参看:默认泛型。 举例如下:// basic trait, no constraint trait Genericwhere
clause trait Generic
![feature(associated_type_defaults)] // simple trait Associated { type T = String; // … } // with constraint trait Associated { type T: Display = String; // … }
看起来和泛型用法很相似,而且你还可以更进一步:trait Associated { type T: Display = String; type U = Self::T; // … }
不知道这个feature什么时候能稳定下来,但确实是一个实用功能。共性
到目前为止,我们已经了解了定义和语法,接下来我们来探索下共性。 泛型和关联类型最重要的一点是都允许你延迟决定trait类型到实现阶段。即使二者语法不同,关联类型总是可以用泛型来替代实现,但反之则不一定。RFC中有个说明:”关联类型不会增加trait本身的表现力,因为你总是可以对trait增加额外的类型参数来达到同样目的”。但是,关联类型可以提供其他的好处。 既然关联类型总是可以被泛型来替代实现,那关联类型存在的意义是什么? 我们会解释下二者的不同,以及怎么选择。不同之处
我们已经看到,泛型和关联类型在很多使用场合是重叠的,但是选择使用泛型还是关联类型是有原因的。 泛型允许你实现数量众多的具体traits(通过改变T来支持不同类型),例如之前提到过的From关联类型,从另一方面来说,仅允许 单个实现,因为一个类型仅能实现一个trait一次,这可以用来限制实现的数量。
Deref trait有一个关联类型:Target,用于解引用到目标类型。如果可以解引用到多个不同类型,会使人相当迷惑(对编译类型推导也很不友好)。
因为一个trait仅能被类型实现一次,关联类型带来了表达上的优势。使用关联类型意味着你不必对所有额外类型增加类型标注,这可以被认为是一个工程优势,具体见:RFC.总结和进一步阅读
简而言之,当你想类型A能够对一个特定trait实现多种实现(基于不同类型参数),使用泛型。例如<font style="color:rgb(34, 34, 34);">From<T></font>
。
如果仅实现特定trait一次,使用关联类型,例如<font style="color:rgb(34, 34, 34);">Iterator</font>
和<font style="color:rgb(34, 34, 34);">Deref</font>
。
如果你想了解更多的关于关联类型所能解决的问题,我推荐你阅读 RFC和Rust书中关联类型。Add trait 同时使用了泛型(默认)和关联类型,也值得一读。另外Stack overflow问答也包含一个详细的解释和例子。