Rust 泛型及关联类型

与其用它语言一样Rust也支持泛型,这里记录仪一下泛型的用法、特别是关联类型associated type.
泛型系统
每种语言支持泛型的程度不一样,根据<<An Extended Comparative Study of Language
Support for Generic Programming>>的描述,语言对泛型的支持基本可以用下面的表来评评估:

image.png

依据这个表,我们可以归纳一下Rust的泛型功能。
rust 的泛型类型参数必须写在<>里,所以根据尖括号很容易区分类型参数,并且类型参数的写法遵循 [camel case]https://en.wikipedia.org/wiki/CamelCase)。

  1. multi-type concepts
    类型参数可以出现在struct, enum 以fn中。


    image.png

泛型也可以出现在method上.


image.png

Rust支持使用多个泛型参数,代表两个不同的类型:
make_tuplepair<T, U>(a: T, b: U) -> (T, U) {
(a, b)
}
struct paIr<T,U>{
x:T,
y:U
}
无论是单参数,还是多参数,都可以增加对参数的约束:
fn find_max<T : PartialOrd> (list : &[T]) -> T {
let mut max = &list[0];
for &i in list.iter() {
if i > max {
max = &i;
}
}
max
}
要使用比较操作'>',T必须实现PartialOrd(trait),否则系统在编译的时候会报错。
fn some_function<T:Display, U:clone>(t: T, u: U) -> i32
{
....
}

  1. Multiple constaints
    Rust允许泛型参数具有多个约束,以上面过find_max为例,我们将对list[0]引用改为move(去掉&),这时候我们需要增加额外的约束。


    image.png

    当泛型参数有多个约束时,通常使用where语句的方式表示:


    image.png
  2. Associated type access
    rust的关联类型与trait的泛型有关,允许trait 内部定义新类型。
    关联类型使用Type在trait内部定义一个占位符,具体实现时声明占位符的类型。我们最常见的写法如下:


    image.png

    type 定义的Item只是一个类型占位符,在具体实现时声明Item的具体类型。
    另外,与泛型相比较,可以提高代码的可读性,如下实现一个contain的类型, 需要在实现方法上注明具体类型,同时比较复杂的包含关系是,需要声明所有的泛型参数:


    image.png

    但如果改用关联类型,可读性就会大大提高:
    image.png

    但是关联类型与泛型还是存在重要的不同,来源于其声明方式:
    //关联类型

    impl Contains for Container {
    }
    //泛型
    impl<i32,i32> Contains for Container {
    }
    关联类型的方式只能声明一次,而泛型可以声明多次,泛型可以对不同的具体类型进行声明,以上名的关联类型具体来看看泛型的声明:


    image.png
  3. Constraints on associated type
    这个Rust 中的关联类型也可以加限制,通过声明需要实现的trait来限制 , 参考 关联类型定义
    An associated type declaration declares a signature for associated type definitions. It is written as type, then an identifier, and finally an optional list of trait bounds.

    image.png

    如上图, 在关联类型的具体实现时,需要type的具体类型满足同时实现Debug的限制。

  4. Retroactive modeling

  1. type ailase
    rust 通过Type 关键字来声明一个类型的别名:
    语法:type Name = ExistingType;

type Meters = u32;
type Kilograms = u32;

let m: Meters = 3;
let k: Kilograms = 3;

assert_eq!(m, k);
类型别名的写法,类似于关联类型,实际上也确实有关联算是特殊一种类型别名(Associated types are type aliases associated with another type
)

  1. Separate compilation

  2. Implicit argument deduction

除了通过上面的不同标准来熟悉泛型在Rust中的应用,下面还还总结了一些没有在标准的功能,以及一些很特别的用法。
a. 泛型默认类型


image.png

这里Add<RHS = Self>是默认泛型类型参数,表示如果不显示指定泛型类型,就默认泛型类型为Self。
当使用泛型类型参数时,可以为泛型指定一个默认的具体类型。如果默认类型就足够的话,这消除了为具体类型实现 trait 的需要。为泛型类型指定默认类型的语法是在声明泛型类型时使用 <PlaceholderType=ConcreteType>。

b. rust 文档中提到的一种container的写法
trait Container {
type E;
fn empty() -> Self;
fn insert(&mut self, elem: Self::E);
}
impl<T> Container for Vec<T> {
type E = T;
fn empty() -> Vec<T> { Vec::new() }
fn insert2(&mut self, x: T) { self.push(x); }
}


image.png

上面这个容器的写法, Type 定义的关联类型在impl的时候仍是不知道的, 这样感觉扩展了关联类型的使用范围。

c. 幻影类型 Phantom Type
幻影类型,是不会存储任何数据,利用泛型约束的特点,用于编译期的检查等,不会影响运行期行为。 幻影类型和std::marker::PhantomData幻影数据一起使用,PhantomData作为标志符表示不存在的数据,只用来占位消费幻影类型。
// A phantom tuple struct which is generic over A with hidden parameter B.
struct PhantomTuple<A, B>(A,PhantomData<B>);

// A phantom type struct which is generic over A with hidden parameter B.
struct PhantomStruct<A, B> { first: A, phantom: PhantomData<B> }
如上,B是幻影类型,和PhantomData一切占位,实际不存储任何数据。

网上有一篇博客专门讲 Phantom Type, 提到了下面几种用法:

  • Compile time type check
    如下图,


    image.png
  • Unused lifetime parameters
    在写一些unsafe的代码(FFI,数据结构),我们常会遇到在定义struct的时候type或者lifetime和struct 是逻辑是关联的,但却不是struct字段的一部分。列入如下代码, 只包含unsafe 指针,由于语法规范没有a,所以直接在struct字段上声明‘a会报错:


    image.png

    但是我们确实希望iter具有'a的声明周期,如同声明。这时候我们可以通关添加幻影类型,来实现。


    image.png

    这样,Iter的lifetime是 ‘a 也就是不能超过 ‘a 的生命周期.
  • Unused type parameters
    这里和上边的情况类似这次是FFI场景下,Rust也是禁止在struct中定义未使用的类型参数,这种情况用PhantomData进行包裹.下图也是网上其它博客给出的例子。


    image.png

    如上图代码, resource_type 实际上是一个拥有未定义类型R的站位属性字段。

  • Ownership and the drop check
    虽然Rust中有各种规则保证声明周期管理,但事后有时候也不好用:
    下面这个代码片段服务编译通过,因为编译器服务确定inspector, days谁的声明周期更长,如果day的生命周期短,inspector = Inspector(&days);就会造成空指针。
    struct Inspector<'a>(&'a u8);

impl<'a> Drop for Inspector<'a> {
fn drop(&mut self) {
// 1. 因为无法确认days和inspector的生命周期到底谁长,如果days先被drop这里就会构成闲空指针
println!("I was only {} days from retirement!", self.0);
// 2. 现在的编译器还没有智能到能看出drop的方法实现即使不使用也不能通过编译
//println!("Just drop!");
}
}

fn main() {
let (inspector, days);
days = Box::new(1);
inspector = Inspector(&days);
}

rust里,struct内有生命周期参数的,要求使用生命周期的参数生命周期长于struct, 这样才不会造成空指针。
在上例中,Inspector生命有自己的drop方法和一个引用参数(&),而在main函数实例化时,我们看到这个引用参数是days。
上面的这份代码,编译的时候会报错,提示不能确定days的生命周期是否足够长,原因是这样的:

  1. let (inspector, days); inspector 在day的前面声明,根据rust的规则,一个scope的变量已声明的反序drop掉(当超出scope时)。所以,days会在inspector前面被drop掉。
  2. inspector = Inspector(&days);days作为引用参数传给了inspector, 这是后rust需要, days的声明周期长于inspector. 总结为:对于一个实现了Drop的范型类型对于它的范型参数必须严格的超过它
  3. 通过#[may_dangle]明确指明不会去使用借用的数据

3。inspector 声明了自己的drop trait的实现,drop函数会在inspector被drop的时候调用。 但是问题来了, 编译器无法判断是否drop函数中有引用days。
所以当执行完inspector = Inspector(&days); 到达scope的终点时:rust开始先drop days;rust 在开始调用inspector的drop方法,开始 drop inspector。在这个方法里,如果引用了days的值 println!("I was only {} days from retirement!", self.0); 和显然就产生了空指针。
为了避免这个错误,rust的编译器在发现某个struct有自己实现drop, 并引用了统一作用范围的参数,该参数优先收回时,会自动报编译错误。
直接改上面这段错误的方式:
方法一. 移除自己实现的drop方法。
方法二. 修改变量定义顺序, let (days,inspector ); 满足:对于一个实现了Drop的范型类型对于它的范型参数必须严格的超过它

对于下面一个Vec类型:
struct Vec<T> {
data: *const T, // *const for variance!
len: usize,
cap: usize,
}
虽然没有’a 声明周期参数,检查也不会报错,但是drop检查器会认为Vec 并不拥有T, 两者之间不存在约束(声明周期约束),这就会导致可能在drop函数中访问到已经被析构的数据导致悬空指针(例如自己实现drop并访问T). 这个问题可以通过PhantomData来解决这个问题:


image.png

另一重方式,是参考标准库中的Unique<T>工具类,它默认实现了上面的功能。
额外的参考:


image.png
  1. dropck
  2. phantom-data

推荐阅读更多精彩内容