rust 类型系统

 

列宁指出:世界上除了运动着的物质之外,什么也没有。同样的,rust的世界中,除了运动(trait)着的类型之外,什么也没有。类型之间通过运动(trait)的互相作用和互相依赖,构成了一个整体,即类型系统

列宁指出:世界上除了运动着的物质之外,什么也没有。同样的,rust的世界中,除了运动(trait)着的类型之外,什么也没有。类型之间通过运动(trait)的互相作用和互相依赖,构成了一个整体,即类型系统

rust 的类型系统

在理解rust的类型系统之前,先来回顾一下唯物主义中关于物质的概念,看看它能不能帮助我们理解类型系统。

唯物主义的世界观认为“世界除了运动着的物质以外,什么也没有”,“运动是物质的根本属性”。

那么这两句话对于我们理解类型系统有什么用呢?类比于rust的世界,可以说“rust中除了运动着的类型以外,什么也没有,运动(trait)是类型的根本属性”。

什么是类型

类型本质就是:指定某种解码方式的一组比特位,类就是一组比特位,型就是某种解码方式

  • “类” 就是一组比特位(一个或多个字节)
    • 计算机世界最小的单元就是 bit
    • 8个bit构成1个字节
    • 多个字节又构成不同的储存形式
  • “型”就是某种编码方式
    • 型是对不同储存的编码方式
    • 比如对于1个字节的内容用 i32, charfloat 方式编码后的含义是不同的
      • printf("%d", 0x41)
      • printf("%c", 0x41)
      • printf("%f", 0x41)
  • 类型
    • rust通过类型表达确定的内容
    • 在rust中表达为struct

什么是运动

哲学中,运动是物质存在的状态,指一切变化和过程。

那么在rust中,运动由trait来表达,这样我们可以理解成:在rust中,trait是类型的存在状态,也是类型的变化过程。在rust中,运动具体表现是:组合、运算和转换

什么是rust的类型系统

哲学中,系统是由相互作用、相互依赖的若干组成部分结合而成的,具有特定功能的有机整体。对于rust类型系统,就是rust的类型通过它的运动(组合、运算和转换)与其他的类型相互作用而形成了一个整体(即最后实现的程序)

所以,rust的世界由物质(类型,通过struct构成)和运动(组合、运算和转换,通过trait构成)

rust 的类型

程序的运行就是CPU把一定数量的比特位解码成特定意义的信息,然后对信息进行处理的过程。

既然rust中的类型是一组用来表达信息的比特位,那么在程序运行中,CPU会根据类型的大小,用合适的解码方式来解读这些比特位。并按照trait指定的处理方式来处理类型

所以类型主要涉及的是比特位的大小

类型的大小

rust在将源码编译成二进制机器码的过程中,要确定特定的类型会占据多少内存,这时就会遇到两种情况:

  1. 确定大小的类型 在编译期就能确定类型的大小,rust就可以直接分配内存地址和空间,这是rust常见的类型

  2. 动态大小的类型 只能在运行期间才知道,无法在事件发生之前就确定储存空间大小的类型。比方说表达当天做了核酸检测的人员信息的类型(比特位),因为事先不知道会有多少人做检测,所以这个类型占用的比特位是事先不能确定的。在事情发展过程中,这个类型会动态的改变大小。

由于rust要求在编译期确定类型占用内存空间的大小,而同一类型的所有值都必须使用等量的内存,那么对于运行期间才能确定的类型,就必须要占据等量的空间,但它们却因运行时的实际情况占有不同的大小,所以rust在编译期无法创建出动态大小类型的变量。对于这种情况可以用引用类型指向动态大小类型,引用类型储存了数据的起始位置和长度,它本身在编译期是确定大小的

str 类型

str 类型是典型的动态大小类型,比如:

let s1: str = "Hello world";
let s2: str = "This is string";

这里是无法编译通过的,因为对于同一类型str,rust要求相同类型占有相同内存空间。但s1s2显然占有不同大小的空间。

当我们使用了引用类型,则可以解决这个问题:

let s1: &str = "hello world";
let s2: &str = "This is string";

因为&str引用类型占用的是16个uszie大小的内存空间,其内容为(ptr, len),所以&str就是编译期确定大小的类型。而它指向的动态类型str在运行期可以分配在堆上。

零大小类型 ZST

enum void{}
struct Foo;

上面的类型,在编译期确定的大小都是零,运行时不占用内存

迭代技巧:

let v: Vec<()> = vec![(); 10];
for i in v:
  println!("{:?}", i);

底类型

这种类型用来表达“无”的概念。它可以等同于任何类型。比如loopbreakcontinue以及panic!()这种没有返回值的类型

泛型

泛型就是泛型参数,指用参数变量代替具体类型或属性,在编译时再给变量赋予具体的类型

泛参可以:

  • 用于函数

    fn foo<T>(x: T) -> T { x }
    
  • 用于结构体

    struct Point<T> {x: T, y: T}
    
  • 用于 trait

    impl<T> Point<T> {
      fn new(x:T, y:T) -> Point<T> {
          Point {x, y}
      }
    }
    

rust 的 trait

trait 定义了为达成类型的运动(组合、运算和转换)所必需的行为集合,所以它就是一种功能/行为集合。定义trait的语法是

pub trait TraitName {
  ...
}

定义好trait后,可以给类型装上trait行为集合

impl TraitName for TypeName {
  ...
}

trait 的特点

孤儿规则

impl TraitName for TypeName 中的TraitNameTypeName必须有一个在当前的rust源码中定义

  • trait可以有默认实现

trait限定

限定的是泛型的类型:<T: Add<Output=T>>

限定泛型 T 的类型是实现了 Add 的类型,而且只能是同类相加的trait

trait可以用于返回值

  fn return_summarizalbe() -> impl Summary {...}

表明函数返回的是实现了Summary的类型

trait可以用作参数

  pub fn func(item: impl TraitName) {
    ...
  }

item必须是已经实现TraitName的类型,也可以说item的类型是实现了TraitName的类型

trait约束

trait约束是对类型而言的,上面的impl TriatNametrait约束的一种语法糖。去糖的完整形式如下:

  pub fn func<T>(item: T)
  where T: TraitName {
    ...
  }
  • 注意trait约束和impl TraitName的区别

    pub fn func(item1: impl TraitName, iterm2: impl TraitName) {...}
    

    item1item2可以是不同的类型,只要它们都实现了TriatName

    pub fn func<T: TraitName>(item1: T, item2: T) {...}
    

    这里的item1item2就必须是相同的类型

  • +指定多个trait

    pub fn func(item: impl Display + TriatName) {...}
    

    item 必须是实现了两个trait的类型

实现另一个trait

为实现了某个triat的类型有条件的实现另一个trait

impl<T: Display> ToString for T {...}

为实现了Display的类型实现ToString

与trait有关的概念

关联类型

trait中有type定义的类型,表示的是和某个行为相关联的类型

pub trait Add<RHS=Self> {
  type Output;
  fn add(self, rhs: RHS) -> Self::Output;
}

trait的继承

trait Triat3: Triat1 + Triat2 {...}

trait对象

指向trait的指针,包括虚表和类型实例。运行时确定具体类型,通过转换(&x as &Foo)来取得trait对象的数据类型

triat对象的结构体:

pub struct TraitObject {
  pub data: *mut (),
  pub vtable: *mut(),
}

triat对象的要求

可以作trait对象的trait必须同时满足

  1. traitSelf?Sized

traitSelf代表的是实现该trait的类型。因为有很多类型都可以实现该trait,它们的定义又各不相同,所以对应的大小也不相同,这样Self的大小就编译器不确定的<Self: ?Sized>。注意,这里?Sized也是一种trait,所以<Self: ?Sized>就相当于trait1: trait2的意思

trait BadRule1:Sized{
    fn foo(&self);
}

because BadRule1 requires `Self: Sized, this trait cannot be made into an object…

上面的例子含义是,BadRule1只能为Sized类型实现,或者说要为某个类型实现BadRlue1必须先实现Sizedtrait对象是动态分发,编译器不能确定类型的大小。在运行中动态调用trait对象的过程中,如果遇到了Unsize类型,在调用相应方法时,就会引发段错误

  1. trait中所有方法必须是对象安全的

  2. 满足下面条件之一,就是对象安全

    这里分发的是trait对象指向的具体类型实体,所以必须确定类型的大小(原因见附录)

    • 方法受Self:Sized的约束

      trait GoodRule21a{
        fn foo<T>(&self,t:T) where Self:Sized;
      }
      
      trait GoodRule21b{
        fn foo<T>(t:T) -> Self where Self:Sized;
        fn foo2(&self,i:i32) -> Self where Self:Sized;
      }
      
    • 方法签名同时满足下面条件

      • 不能包含任何泛参

        trait BadRule221{
            fn foo<T>(&self,t:T);
        }
        

        this trait cannot be made into an object…, .because method foo has generic type parameters

      • 第一个参数必须为Self或可以解引用为Self类型

        trait BadRule222{
            fn foo();
        }
        

        this trait cannot be made into an object…, .because associated function foo has no self parameter

      • Self不能出现在第一个参数之外的地方,特别是返回值

        trait BadRule223{
            fn foo(&self) -> Self;
        }
        

        this trait cannot be made into an object…, .because method foo references the Self type in its return type

    • trait中不包含关联常量

        trait BadRule3 {
            const S :i32 = 100;
            fn foo(&self);
        }
      
  3. 标签 trait

  • Sized:标识编译器可以确定大小的类型

  • Unsized:标识动态大小类型,编译期不确定大小
  • Copy

      #[lang = "copy"]
      pub trait Copy: Clone {
        // 空
      }
    
  • Send:跨线程安全通信的类型,跨线程传递所有权
  • Sync:跨线程安全传递共享引用

    • 原生数据类型默认都实现了SendSync标签

      #[lang = "send"]
      pub unsafe trait Send {
        // 空
      }
      
      unsafe impl Send for .. { }
      impl<T: ?Sied> !Send for *const T { }
      

rust 的类型转换

Deref 解引用

如果TDeref<Target=U>,那么*T就能自动转换成U

x.deref()手动调用x的解引用

as 操作符

  1. 长类型转短类型会截断

  2. 完全限定语法

     <S as A>::test()
     <S as B>::test()
    
  3. 与子类型的互换

    &'static str类型是&'a str的子类型。'a是泛型标记

  4. FromInto两个trait

    • T::from(u) 生成 T 类型
    • u.into()转换成T类型,其中U要满足U: Into<T>

trait 的不足

孤儿规则的局限性

1.设计trait时,会影响下游的使用者

  impl<T:Foo> Bar for T {}

为所有的 T 实现 Bar 特性

  1. 下游分支使用NewType模式将远程类型包装成本地类型

  2. 某些本地类型放到窗口中,会变成远程类型

比如:Rc<T>Option<T>,因为这些窗口是在标准库中定义的,所以类型变成了远程(非本地)类型

为了满足在子crate中为Box<Int>这种类型扩展trait,rust为Box<T>做了特殊处理,脱离孤儿规则的限制

在Rust内部使用了 #[fundamental]属性标识

  #[fundamental]
  pub struct Box<T: ?Sized>(Unique<T>);

代表了 Box<T> 类型不受孤儿规则的限制

代码利用效率不高

重叠规则:不能为重叠类型实现同一个trait。重叠类型如下:

impl<T> AnyTrait for T {...}
impl<T> AnyTrait for T where T: Copy {...}
impl<T> AnyTrait for i32 {...}

这里 T 包含了 T where T: Copy,后者又包含了 i32。所以这三个类型是重叠的,为了保证trait一致性,Rust不允许编译

但是有一些被包含的类型,可以不用实现某些特性,这就引起了性能问题。比如

impl<R, T> AddAssign<R> for T 
  where T: Add<R> + Clone 
{...}

为所有的 T 实现 AddAssign,但是要求所有的 T 必须实现 Clone。因为重叠规则,我们不能为没有Clone的类型重新实现AddAssign,所以为了实现更好的性能,标准库是为每个具体的类型各自实现一遍AddAssign

理想的情况应该是对所有的T where T: Clone + Add<R>实现AddAssign,对于没有Clone的类型,可以重新实现AddAssign

现在Rust引入了特化(专门化)标识,比如:

#![feature(specialization)]
struct Diver<T> {
  inner: T,
}

# trait的默认实现
trait Swimmer {
  fn swim(&self) {
    println!("swimming")
  }
}

impl<T> Swimmer for Diver<T> {}
impl Swimmer for Diver<&'static str> {
  fn swim(&self) {
    println!("drowning, help!")
  }
}

通过specialization给泛型类型Diver<T>打好特化(专用化)的标签,说明Diver<T>这个类型的trait特性可以面向具体类型而专门化的。也就是说Diver<str>Diver<String>对于类型strString有专门化的trait

如果triat没有默认实现,需要通过关键字defaultSwimmer的具体实现处标识

#![feature(specialization)]
struct Diver<T> {
  inner: T,
}

# 没有trait的默认实现
trait Swimmer {
  fn swim(&self);
}

impl<T> Swimmer for Diver<T> {
  # 没有trait的默认实现,就在具体实现处增加default关键字
  # 默认impl块中的方法不可被专用化
  default fn swim(&self) {
    println!("swimming")
  }
}

impl Swimmer for Diver<&'static str> {
  fn swim(&self) {
    println!("drowning, help!")
  }
}

抽象表达能力有待改进

迭代器的缺陷:在迭代元素时,只能按值迭代,只有重新分配内在,不能通过引用来复用原始数据。

比如std::io::Lines读一行数据分配一个新的String,而不能重复使用内在缓存区,影响了性能。

因为迭代器的实现基于关联类型,但它不支持泛型。不支持泛型就无法支持引用类型(引用类型要求标注生命周期,它是泛型)

Rust的泛型关联类型(GAT: Generic Associated Type)

trait StreamingIterator {
  # 'a就是泛型参数
  type Item<'a>;
  fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}

Item<'a>是类型构造器(ACT: Associated type constructor)

附录

为什么 trait 对象要求 Self 是 sized

给定任何具体类型,你总是可以说它是有大小的还是没有大小的。然而,对于泛型,就会出现一个问题–某些类型参数是否有大小?

fn generic_fn<T>(x: T) -> T { ... }

如果T是无大小的,那么这样的函数定义是不正确的,你不能在不知道其大小的情况下制造一个局部变量,所以不能直接传递无大小的值。如果它是有大小的,那么一切都OK。

在 Rust 中,所有的泛型参数默认都是确定大小的(在函数、结构和 traits 中)。它们都隐式的由 Sized 限定(Sized 是一个标记大小类型的trait)

fn generic_fn<T: Sized>(x: T) -> T { ... }

这是因为在绝大多数情况下,泛型都是大小确定的。但有时,你会希望选择无固定大小,这可以通过 ?Sized 限定来实现。

fn generic_fn<T: ?Sized>(x: &T) -> u32 { ... }

现在,generic_fn可以像generic_fn("abcde")一样被调用,T将被实例化为str,而str是没有大小的,但这没关系–这个函数接受了对T的引用,所以没有什么坏事发生。

然而,还有一个地方的大小问题很重要。Rust中的trait总是为某种类型实现的。

trait A {
    fn do_something(&self);
}

struct X;
impl A for X {
    fn do_something(&self) {}
}

然而,这只是出于方便和实用的目的。我们可以定义trait,使其只取一个类型参数,而不指定实现trait的类型。

trait A<T> {
    fn do_something(t: &T);
}

struct X;
impl A<X> {
    fn do_something(t: &X) {}
}

这就是Haskell类型类的工作方式,事实上,这也是Rust中较低层次的traits的实际实现方式。

Rust中的每个trait都有一个隐式类型参数,称为Self,它指定了这个trait的实现类型。它总是在trait的主体中可用。

trait A {
    fn do_something(t: &Self);
}

这就涉及到大小的问题了。自身参数Self是否有大小?

事实证明,不是的,在 Rust 中,Self 默认情况下是没有大小的。每个trait都对Self有一个隐式的?Sized约束。需要这样做的原因之一是,有很多trait可以为没有大小的类型实现,并且仍然可以工作。

例如,任何只包含引用Self并返回Self的方法的trait都可以为非大小类型实现。你可以在 RFC 546 中阅读更多关于动机的内容。

当你只定义trait和它的方法的签名时,大小不是问题。因为在这些定义中没有实际的代码,编译器不会假设任何事情。然而,当你开始编写使用这个trait的通用代码时(其中包括默认方法,因为它们采用了隐含的Self参数),你应该考虑到大小问题。因为Self默认大小不是固定的,所以默认的trait方法不能按值返回Self,也不能按值把它作为参数。因此,你要么需要指定Self默认是大小固定的:

trait A: Sized { ... }

或者你可以指定一个方法只有在Self是大小固定的时候才能被调用:

trait WithConstructor {
    fn new_with_param(param: usize) -> Self;

    fn new() -> Self
      where Self: Sized {
        Self::new_with_param(0)
    }
}