< 返回版块

pfcoder 发表于 2020-03-31 15:41

翻译自:https://thomashartmann.dev/blog/on-generics-and-associated-types/

泛型与关联类型

和其他我学过的语言相比较,Rust有一些令人费解的概念。借用,所有权,借用检查这些概念大家应该已经都听说过了,我自己曾花费数小时在生命期问题上,最终不得不放弃抗争,转而采用Clone来解决。

关联类型虽然不是什么令人抓狂的概念,但我还是尝试了很多工作来试图正确的理解它,或者说至少我认为我自己理解了。

TL;DR:

一个关于何时使用泛型何时使用关联类型的粗略答案是:如果针对特定类型的trait有多个实现(例如From<T>)则使用泛型,否则使用关联类型(例如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<T> trait, 其中的T泛型参数表明接受任何类型,你可以把任何类型T转换为目标类型,只要你实现了相应的转换方法。

受限(也叫bounded)泛型是指, trait X 声明只有实现了 trait Y的类型才能用于trait X,我们后续会看到例子。

关联类型(Associated Types)

关联类型,如同其名称所暗示,是指关联至某个trait的类型。当你定义该trait时,类型未指定,这一点和泛型很相似。同时你也可以对类型增加trait限制。

一个使用关联类型trait的重要例子是:Iterator。它有一个关联类型Item以及一个函数next。next返回Option<Self::Item>。你可以用泛型实现同样的功能,但是后续我们会解释使用关联类型可以在某些情况下带来额外好处。

语法

更进一步之前,我们来浏览下这些概念的语法。我们尽量采用较少的抽象。此处定义两个traits: Generic和Associated,分别使用泛型和关联类型,并且观察使用trait限定和默认类型。

基础traits

使用类型参数化(type-parameterized)的trait:

trait Generic<T> {
    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<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 Generic<T = String> {
    // ...
}

// with constraint
trait Generic<T: Display = String> {
    // ...
}

// or using the `where` clause
trait Generic<T = String>
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<T> trait,我们可以实现任意数量类型。

举例来看,假设你有一个类型定义:MyNumeric。你可以在此类型上实现 From<u8>, From<u16>, From<u32>等多种数据转换。这使得泛型在处理仅是类型参数不同的trait时特别有用。

关联类型,从另一方面来说,仅允许 单个实现,因为一个类型仅能实现一个trait一次,这可以用来限制实现的数量。

Deref trait有一个关联类型:Target,用于解引用到目标类型。如果可以解引用到多个不同类型,会使人相当迷惑(对编译类型推导也很不友好)。

因为一个trait仅能被类型实现一次,关联类型带来了表达上的优势。使用关联类型意味着你不必对所有额外类型增加类型标注,这可以被认为是一个工程优势,具体见:RFC.

总结和进一步阅读

简而言之,当你想类型A能够对一个特定trait实现多种实现(基于不同类型参数),使用泛型。例如From<T>。

如果仅实现特定trait一次,使用关联类型,例如Iterator和Deref。

如果你想了解更多的关于关联类型所能解决的问题,我推荐你阅读 RFCRust书中关联类型Add trait 同时使用了泛型(默认)和关联类型,也值得一读。另外Stack overflow问答也包含一个详细的解释和例子。

评论区

写评论
ScathonLin 2023-01-04 16:17

一直没想明白二者的区别和联系,感觉就是一个东西,读了这篇文章,终于知道为啥了,感谢博主分享!

andytesii 2020-04-01 00:43

这两天刚看过rust book这部分内容,再复习下,多谢分享

Mike Tang 2020-03-31 20:06

+1

1 共 3 条评论, 1 页