翻译自:https://thomashartmann.dev/blog/on-generics-and-associated-types/
泛型与关联类型
和其他我学过的语言相比较,Rust有一些令人费解的概念。借用,所有权,借用检查这些概念大家应该已经都听说过了,我自己曾花费数小时在生命期问题上,最终不得不放弃抗争,转而采用Clone来解决。
关联类型虽然不是什么令人抓狂的概念,但我还是尝试了很多工作来试图正确的理解它,或者说至少我认为我自己理解了。
TL;DR:
一个关于何时使用泛型何时使用关联类型的粗略答案是:如果针对特定类型的trait有多个实现(例如From)则使用泛型,否则使用关联类型(例如Iterator 和 Deref)。
本文目标和限制
本文的目的是解释泛型和关联类型的相似与不同之处。特别是针对trait,因为关联类型主要用于trait。
此外,虽然我们在讨论关联类型,但是我们不会涉及泛关联类型(generic associated types)。如果你对这一主题感兴趣,可以参考下RFC。
如果读完本文,你还是不太理解我所说的,建议阅读下Rust Book的 高级Traits章节,特别是关于关联类型。
最后,阅读本文需要你有一些编程经验(Rust),以及基本的泛型编程思想。关于Rust中的泛型,可以参考10.1 泛型。
定义
为了确保我们的理解一致,先来定义一些基本概念。
泛型(Generic Types)
在trait上下文中, 泛型又被称作类型参数(type parameters),用于在具体实现trait时使用的类型。类型参数可以是完全开放的,或者受限于特定trait。
例如 std::convert::From trait, 其中的T泛型参数表明接受任何类型,你可以把任何类型T转换为目标类型,只要你实现了相应的转换方法。
受限(也叫bounded)泛型是指, trait X 声明只有实现了 trait Y的类型才能用于trait X,我们后续会看到例子。
关联类型(Associated Types)
关联类型,如同其名称所暗示,是指关联至某个trait的类型。当你定义该trait时,类型未指定,这一点和泛型很相似。同时你也可以对类型增加trait限制。
一个使用关联类型trait的重要例子是:Iterator。它有一个关联类型Item以及一个函数next。next返回Option。你可以用泛型实现同样的功能,但是后续我们会解释使用关联类型可以在某些情况下带来额外好处。
语法
更进一步之前,我们来浏览下这些概念的语法。我们尽量采用较少的抽象。此处定义两个traits:Generic和Associated,分别使用泛型和关联类型,并且观察使用trait限定和默认类型。
基础traits
使用类型参数化(type-parameterized)的trait:
trait Generic {
fn get(&self) -> T;
}
使用关联类型的trait:
trait Associated {
type T;
fn get(&self) -> Self::T;
}
注意观察两种定义的不同,类型T如何从泛型参数变为了trait自身定义的一部分,在关联类型中,我们无法直接像泛型一样直接使用T,而是使用Self::T。
加上trait限制
如果我们想对泛型参数或者关联类型加以特定trait限制定义,可以使用Rust常用的:表达式(bounds)。
例如限定类型必须实现了core::fmt::Display trait:
trait Generic {
fn get(&self) -> T;
}
// or using the `where` keyword
trait Generic
where
T: Display,
{
fn get(&self) -> T;
}
同样的,对于关联类型:
trait Associated {
type T: Display;
fn get(&self) -> Self::T;
}
默认类型
Rust一个很酷的特性是可以指定泛型的默认类型,通常使用默认类型,某些特殊情况使用重载类型。参看:默认泛型。
举例如下:
// basic trait, no constraint
trait Generic {
// ...
}
// with constraint
trait Generic {
// ...
}
// or using the `where` clause
trait Generic
where
T: Display,
{
// ...
}
经过尝试,我们无法在where中使用默认类型(= String)。
对于关联类型,目前在Rust stable环境中还没有对默认类型的支持。但是nightly build可以使用如下宏:#![feature(associated_type_defaults)]来启用。
我们来看下在关联类型中的使用:
#![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,我们可以实现任意数量类型。
举例来看,假设你有一个类型定义:MyNumeric。你可以在此类型上实现 From, From, From等多种数据转换。这使得泛型在处理仅是类型参数不同的trait时特别有用。
关联类型,从另一方面来说,仅允许单个实现,因为一个类型仅能实现一个trait一次,这可以用来限制实现的数量。
Deref trait有一个关联类型:Target,用于解引用到目标类型。如果可以解引用到多个不同类型,会使人相当迷惑(对编译类型推导也很不友好)。
因为一个trait仅能被类型实现一次,关联类型带来了表达上的优势。使用关联类型意味着你不必对所有额外类型增加类型标注,这可以被认为是一个工程优势,具体见:RFC.
总结和进一步阅读
简而言之,当你想类型A能够对一个特定trait实现多种实现(基于不同类型参数),使用泛型。例如From。
如果仅实现特定trait一次,使用关联类型,例如Iterator和Deref。
如果你想了解更多的关于关联类型所能解决的问题,我推荐你阅读 RFC和Rust书中关联类型。Add trait 同时使用了泛型(默认)和关联类型,也值得一读。另外Stack overflow问答也包含一个详细的解释和例子。
领取专属 10元无门槛券
私享最新 技术干货