12|类型系统:Rust的类型系统有什么特点?

5,406 阅读7分钟

正式开始

  1. 在机器码的世界中,指令仅仅和立即数或者内存打交道,内存中存放的数据都是字节流
  2. 类型系统完全是一种工具

类型系统基本概念与分类

  1. 类型:是对值的区分,它包含了值在内存中的长度、对齐以及值可以进行的操作等信息
  2. 类型系统其实就是,对类型进行定义、检查和处理的系统

分类

  1. 按定义后类型是否可以隐式转换,可以分为强类型和弱类型

  2. 按类型检查的时机,在编译时检查还是运行时检查,可以分为静态类型系统和动态类型系统

    a. 对于静态类型系统,还可以进一步分为显式静态和隐式静态

多态

  1. 在使用相同的接口时,不同类型的对象,会采用不同的实现

  2. 对于动态类型系统,多态通过**鸭子类型(duck typing)**实现

  3. 对于静态类型系统,多态可以通过**参数多态(parametric polymorphism)、特设多态(adhoc polymorphism)和子类型多态(subtype polymorphism)**实现

    a. 参数多态是指,代码操作的类型是一个满足某些约束的参数,而非具体的类型。

    b. 特设多态是指同一种行为有多个不同实现的多态。比如加法,可以 1+1,也可以是 “abc” + “cde”、matrix1 + matrix2、甚至 matrix1 + vector1。在面向对象编程语言中,特设多态一般指函数的重载

    c. 子类型多态是指,在运行时,子类型可以被当成父类型使用

  4. 在 Rust 中,参数多态通过泛型来支持、特设多态通过 trait 来支持、子类型多态可以用 trait object 来支持

image.png

Rust类型系统

  1. 在定义时, Rust 不允许类型的隐式转换,也就是说,Rust 是强类型语言;
  2. 在检查时,Rust 使用了静态类型系统,在编译期保证类型的正确
  3. 从内存的角度看,类型安全是指 代码只能按照被允许的方法和被允许的权限,访问它被授权访问的内存

Rust 中除了 let / fn / static / const 这些定义性语句外都是表达式,而一切表达式都有类型

在 Rust 中,对于一个作用域,无论是 if / else / for 循环还是函数,最后一个表达式的返回值就是作用域的返回值,如果表达式或者函数不返回任何值,那么它返回一个 unit() 。unit 是只有一个值的类型,它的值和类型都是 ()

数据类型

  1. Rust 的原生类型包括字符、整数、浮点数、布尔值、数组(array)、元组(tuple)、切片(slice)、指针、引用、函数等

image.png

  1. 在原生类型的基础上,Rust 标准库还支持非常丰富的组合类型

image.png

  1. 类型分类

image.png

类型推导

  1. 在一个作用域之内,Rust 可以根据变量使用的上下文,推导出变量的类型
  2. collect 是 Iterator trait 的方法,它把一个 iterator 转换成一个集合
  3. 在泛型函数后使用 :: 来强制使用类型 T,这种写法被称为 turbofish
  4. 有些情况下,即使上下文中含有类型的信息,也需要开发者为变量提供类型,比如常量和静态变量的定义

用泛型实现参数多态

泛型数据结构

泛型结构 Vec<T>

pub struct Vec<T, A: Allocator = Global> {
    buf: RawVec<T, A>,
    len: usize,
}

pub struct RawVec<T, A: Allocator = Global> {
    ptr: Unique<T>,
    cap: usize,
    alloc: A,
}
/*
Vec有两个参数
一个是 T,是列表里的每个数据的类型
另一个是 A,它有进一步的限制 A: Allocator ,也就是说 A 需要满足 Allocator trait
A 这个参数有默认值 Global,它是 Rust 默认的全局分配器
*/

枚举类型 Cow

生命周期标注也是泛型的一部分,一个生命周期 'a 代表任意的生命周期,和 T 代表任意类型是一样的

/*
这里对B 的三个约束分别是:
1. 生命周期 'a
2. 长度可变 ?Sized
3. 符合 ToOwned trait
*/
pub enum Cow<'a, B: ?Sized + 'a> where B: ToOwned,
{
    // 借用的数据
    Borrowed(&'a B),
    // 拥有的数据
    // 对 B 做了一个强制类型转换,转成 ToOwned trait
    // 然后访问 ToOwned trait 内部的 Owned 类型
    Owned(<B as ToOwned>::Owned),
}

含义

Cow(Clone-on-Write)在返回数据的时候,提供了一种可能:

  1. 要么返回一个借用的数据(只读)
  2. 要么返回一个拥有所有权的数据(可写)

Cow 泛型约束参数

  1. 对于拥有所有权的数据 B ,第一个是生命周期约束。

    a. 这里 B 的生命周期是 'a,所以 B 需要满足 'a,用 B: 'a 来表示。

    b.当 Cow 内部的类型 B 生命周期为 'a 时,Cow 自己的生命周期也是 'a。

  2. B 还有两个约束:?Sized 和 “where B: ToOwned”

    a. ?Sized 是一种特殊的约束写法,? 代表可以放松问号之后的约束。

    b. 由于 Rust 默认的泛型参数都需要是 Sized,也就是固定大小的类型,所以这里 ?Sized 代表用可变大小的类型

    c. ToOwned 是一个 trait,它可以把借用的数据克隆出一个拥有所有权的数据

泛型约束的方式

在表述泛型参数的约束时,Rust 允许两种方式

  1. 一种类似函数参数的类型声明,用 “:” 来表明约束,多个约束之间用 + 来表示;
  2. 另一种是使用 where 子句,在定义的结尾来表明参数的约束
  3. 两种方法都可以,且可以共存

泛型函数

  1. 在声明一个函数的时候,我们可以不指定具体的参数或返回值的类型,而是由泛型参数来代替

  2. 对于泛型函数,Rust 会进行单态化(Monomorphization)处理,也就是在编译时,把所有用到的泛型函数的泛型参数展开,生成若干个函数

    a. 单态化的好处是,泛型函数的调用是静态分派

    b. 编译速度很慢,一个泛型函数,编译器需要找到所有用到的不同类型,一个个编译

小结

image.png

好用链接

  1. 鸭子类型 概念
  2. 参数多态 概念
  3. 特设多态 概念
  4. 子类型多态
  5. Rust 原生类型
  6. Iterator trait
  7. 内存分配 Global
  8. Rust 默认的全局分配器
  9. ToOwned trait
  10. 单态化 概念
  11. 里氏替换原则
  12. 各个语言实现和处理泛型

精选问答

  1. 下面这段代码为什么不能编译通过?你可以修改它使其正常工作么?

use std::io::{BufWriter, Write};
use std::net::TcpStream;

#[derive(Debug)]
struct MyWriter<W> {
    writer: W,
}

impl<W: Write> MyWriter<W> {
    pub fn new(addr: &str) -> Self {
        let stream = TcpStream::connect("127.0.0.1:8080").unwrap();
        Self {
            writer: BufWriter::new(stream),
        }
    }

    pub fn write(&mut self, buf: &str) -> std::io::Result<()> {
        self.writer.write_all(buf.as_bytes())
    }
}

fn main() {
    let writer = MyWriter::new("127.0.0.1:8080");
    writer.write("hello world!");
}

代码报错的主要原因是

  1. 实现 new 方法时,对泛型的约束要求要满足 W: Write,而 new 的声明返回值是 Self,也就是说 self.wirter 必须是 W: Write 类型(泛型)
  2. 但实际返回值是一个确定的类型 BufWriter<TcpStream>,这不满足要求

修改方法有这么几个思路

  1. 修改 new 方法的返回值
impl<W: Write> MyWriter<W> { 
    pub fn new(addr: &str) -> MyWriter<BufWriter<TcpStream>> { 
        let stream = TcpStream::connect(addr).unwrap(); 
        MyWriter {
            writer: BufWriter::new(stream), 
        } 
    }
} 

fn main() { 
    let mut writer = MyWriter::<BufWriter<TcpStream>>::new("127.0.0.1:8080"); 
    writer.write("hello world!"); 
}
  1. 对确定的类型 MyWriter<BufWriter>实现 new 方法
impl MyWriter<BufWriter<TcpStream>> { 
    pub fn new(addr: &str) -> Self { 
        let stream = TcpStream::connect(addr).unwrap(); 
        Self {
            writer: BufWriter::new(stream), 
        }
    } 
}
    
fn main() { 
    let mut writer = MyWriter::new("127.0.0.1:8080"); 
    writer.write("hello world!"); 
}
  1. 修改 new 方法的实现,使用依赖注入
impl<W: Write> MyWriter<W> { 
    pub fn new(writer: W) -> Self { 
        Self {
            writer, 
        }
    }
} 
fn main() { 
    let stream = TcpStream::connect("127.0.0.1:8080").unwrap(); 
    let mut writer = MyWriter::new(BufWriter::new(stream)); 
    writer.write("hello world!"); 
}