深入浅出 Rust 笔记 Series 1

系列导航

这是我在阅读范长春的《深入浅出 Rust》时做的笔记,绝大部分内容源自此书,另有小部分内容源自 Rust 圣经。这两份资料是我入门 Rust 的主要材料。

第一部分 基础知识

chap 04 函数

4.1 简介

  • 函数可以当成头等公民(first class value)被复制到一个值中,这个值可以像函数一样被调用。示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    fn add2((x,y) : (i32,i32)) -> i32 {
    x + y
    }

    fn main() {
    let p = (1, 3);
    // func 是一个局部变量
    let func = add2;
    // func 可以被当成普通函数一样被调用
    println!("evaluation output {}", func(p));
    }
  • 每一个函数都具有自己单独的类型,但是这个类型可以自动转换到 fn 类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    fn main() {
    // 先让 func 指向 add1
    let mut func = add1;
    // 再重新赋值,让 func 指向 add2
    func = add2;
    }

    error[E0308]: mismatched types
    --> test.rs:11:12
    |
    11 | func = add2;
    | ^^^^ expected fn item, found a different fn item
    |
    = note: expected type `fn((i32, i32)) -> i32 {add1}`
    found type `fn((i32, i32)) -> i32 {add2}`
  • 虽然 add1 和 add2 有同样的参数类型和同样的返回值类型,但它们是不同类型,所以这里报错了。修复方案是让 func 的类型为通用的 fn 类型即可:

    1
    2
    3
    4
    // 写法一,用 as 类型转换
    let mut func = add1 as fn((i32,i32))->i32;
    // 写法二,用显式类型标记
    let mut func : fn((i32,i32))->i32 = add1;
  • Rust 的函数体内也允许定义其他 item,包括静态变量、常量、函数、trait、类型、模块等。当你需要一些 item 仅在此函数内有用的时候,可以把它们直接定义到函数体内,以避免污染外部的命名空间。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    fn test_inner() {
    static INNER_STATIC: i64 = 42;
    // 函数内部定义的函数
    fn internal_incr(x: i64) -> i64 {
    x + 1
    }
    struct InnerTemp(i64);
    impl InnerTemp {
    fn incr(&mut self) {
    self.0 = internal_incr(self.0);
    }
    }
    // 函数体,执行语句
    let mut t = InnerTemp(INNER_STATIC);
    t.incr();
    println!("{}", t.0);
    }

4.2 发散函数

  • Rust 支持一种特殊的发散函数(Diverging functions),它的返回类型是感叹号!。

  • 发散类型 ! 的最大特点就是,它可以被转换为任意一个类型。

  • 在 Rust 中,有以下这些情况永远不会返回,它们的类型就是!

    • panic!以及基于它实现的各种函数/宏,比如 unimplemented!、unreachable!;
    • 死循环 loop{};
    • 进程退出函数 std::process::exit 以及类似的 libc 中的 exec 一类函数。

4.3 main 函数

  • 与其他编程语言也因此为 main 函数设计了参数和返回值类型相比,Rust 的设计稍微有点不一样,传递参数和返回状态码都由单独的 API 来完成,示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    fn main() {
    for arg in std::env::args() {
    println!("Arg: {}", arg);
    }
    std::process::exit(0);
    }

    编译,执行并携带几个参数,可以看到:
    $ test -opt1 opt2 -- opt3
    Arg: test
    Arg: -opt1
    Arg: opt2
    Arg: --
    Arg: opt3
  • 用 std::env::var() 以及 std::env::vars() 函数读取环境变量

    • var() 函数可以接受一个字符串类型参数,用于查找当前环境变量中是否存在这个名字的环境变量,vars() 函数不携带参数,可以返回所有的环境变量
    • 此前,Rust 的 main 函数只支持无参数、无返回值类型的声明方式,即 main 函数的签名固定为:fn main()->()。但是,在引入了?符号作为错误处理语法糖之后,就变得不那么优雅了,因为?符号要求当前所在的函数返回的是 Result 类型,这样一来,问号就无法直接在 main 函数中使用了。为了解决这个问题,Rust 设计组扩展了 main 函数的签名,使它变成了一个泛型函数,这个函数的返回类型可以是任何一个满足 Terminationtrait 约束的类型,其中()、bool、Result 都是满足这个约束的它们都可以作为 main 函数的返回类型。关于这个问题,可以参见第 33 章

4.4 const fn

  • 函数可以用 const 关键字修饰,这样的函数可以在编译阶段被编译器执行,返回值也被视为编译期常量。const 函数是在编译阶段执行的,因此相比普通函数有许多限制,并非所有的表达式和语句都可以在其中使用。

chap 05 trait

  • Rust 语言中的 trait 是非常重要的概念。在 Rust 中,trait 这一个概念承担了多种职责。

  • trait 本身既不是具体类型,也不是指针类型,它只是定义了针对类型的、抽象的“约束”。不同的类型可以实现同一个 trait,满足同一个 trait 的类型可能具有不同的大小。因此,trait 在编译阶段没有固定大小,目前我们不能直接使用 trait 作为实例变量、参数、返回值。

  • impl trait 只是一个语法糖。impl Trait ​是静态分发。用于指定未命名但是具体存在的类型(可以表达一个不用装箱的匿名类型,以及它所满足的基本接口),其实现了指定的 Trait​,可用作函数参数类型和返回值类型。跟泛型函数的主要区别是:泛型函数的类型参数是函数的调用者指定的;而 impl trait 的具体类型是函数的实现体指定的(还不太理解)。

    image

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
    }

    // https://rust-book.cs.brown.edu/ch10-02-traits.html#trait-bound-syntax
    // The impl Trait syntax works for straightforward cases
    // but is actually syntax sugar for a longer form known as a trait bound;
    // it looks like this:


    pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
    }
    • impl dyn Trait​:给 Trait Object ​增加方法。参考

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      trait Animal {
      fn walk(&self) {
      println!("walk");
      }
      }

      impl dyn Animal {
      fn talk(&self) {
      println!("talk");
      }
      }

      struct Person;

      impl Animal for Person {}

      fn demo() -> Box<dyn Animal> {
      let p = Person;
      Box::new(p)
      }

      fn main() {
      let p = Person;
      p.walk();

      let p1 = demo();
      p1.walk();
      p1.talk();
      }
  • 参考

5.1 成员方法

  • Self 类型:所有的 trait 中都有一个隐藏的类型 Self(大写 S),代表当前这个实现了此 trait 的具体类型。Rust 中 Self(大写 S)和 self(小写 s)都是关键字,大写 S 的是类型名,小写 s 的是变量名。

  • self 参数同样也可以指定类型,当然这个类型是有限制的,必须是包装在 Self 类型之上的类型。对于第一个 self 参数,常见的类型有 self:Self、self:&Self、self:&mut Self 等类型。对于以上这些类型,Rust 提供了一种简化的写法,我们可以将参数简写为 self、&self、&mut self。self 参数甚至可以是 Box 指针类型 self:Box<Self>。self 参数只能用在第一个参数的位置。请注意“变量 self”和“类型 Self”的大小写不同。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    trait T {
    fn method1(self: Self);
    fn method2(self: &Self);
    fn method3(self: &mut Self);
    }
    // 上下两种写法是完全一样的
    trait T {
    fn method1(self);
    fn method2(&self);
    fn method3(&mut self);
    }
  • trait 中定义的函数,也可以称作关联函数(associated function)。函数的第一个参数如果是 Self 相关的类型,且命名为 self(小写 s),这个参数可以被称为“receiver”(接收者)。具有 receiver 参数的函数,我们称为“方法”(method),可以通过变量实例使用小数点来调用。没有 receiver 参数的函数,我们称为“静态函数”(static function),可以通过类型加双冒号::的方式来调用。在 Rust 中,函数和方法没有本质区别

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    struct Circle {
    radius: f64,
    }

    trait Shape {
    fn area(self: &Self) -> f64;
    }

    impl Shape for Circle {
    // Self 类型就是 Circle
    // self 的类型是 &Self,即 &Circle
    fn area(&self) -> f64 {
    // 访问成员变量,需要用 self.radius
    std::f64::consts::PI * self.radius * self.radius
    }
    }


    let c = Circle { radius : 2f64};
    // 第一个参数名字是 self,可以使用小数点语法调用
    println!("The area is {}", c.area()); // 这里用 c 或者 &c 都可以。Deref ?
  • 针对一个类型,我们可以直接对它 impl 来增加成员方法,无须 trait 名字。可以看作是为该类型 impl 了一个匿名的 trait。用这种方式定义的方法叫作这个类型的“内在方法”(inherent methods)

    1
    2
    3
    4
    5
    6
    7
    struct Circle {
    radius: f64,
    }

    impl Circle {
    fn get_radius(&self) -> f64 { self.radius }
    }

5.2 静态方法

  • 没有 receiver 参数的方法(第一个参数不是 self 参数的方法)称作“静态方法”。静态方法可以通过 Type::FunctionName()的方式调用。需要注意的是,即便我们的第一个参数是 Self 相关类型,只要变量名字不是 self,就不能使用小数点的语法调用函数。

    • 在标准库中就有一些这样的例子。Box 的一系列方法 Box::into_raw(b:Self)Box::leak(b:Self),以及 Rc 的一系列方法 Rc::try_unwrap(this:Self)Rc::downgrade(this:&Self),都是这种情况。它们的 receiver 不是 self 关键字,这样设计的目的是强制用户用 Rc::downgrade(&obj)的形式调用,而禁止 obj.downgrade()形式的调用。这样源码表达出来的意思更清晰,不会因为 Rc 里面的成员方法和 T 里面的成员方法重名而造成误解问题(这又涉及 Deref trait 的内容,读者可以把第 16 章读完再回看这一段)。

5.3 扩展方法

  • 可以利用 trait 给其他的类型添加成员方法,就像 C# 里面的“扩展方法”一样。哪怕这个类型不是在当前的项目中声明的,我们依然可以为它增加一些成员方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    trait Double {
    fn double(&self) -> Self;
    }
    impl Double for i32 {
    fn double(&self) -> i32 { *self * 2 }
    }
    fn main() {
    // 可以像成员方法一样调用
    let x : i32 = 10.double();
    println!("{}", x);
    }
  • 在声明 trait 和 impl trait 的时候,Rust 规定了一个 Coherence Rule(一致性规则)或称为 Orphan Rule(孤儿规则):impl 块要么与 trait 的声明在同一个的 crate 中,要么与类型的声明在同一个 crate 中。

    • 也就是说,如果 trait 来自于外部 crate,而且类型也来自于外部 crate,编译器不允许你为这个类型 impl 这个 trait。它们之中必须至少有一个是在当前 crate 中定义的。
    • 上游开发者在给别人写库的时候,尤其要注意,一些比较常见的标准库中的 trait,如 Display Debug ToString Default 等,应该尽可能地提供好。否则,使用这个库的下游开发者是没办法帮我们把这些 trait 实现的。

5.4 完整函数调用语法

  • Fully Qualified Syntax 提供一种无歧义的函数调用语法,允许程序员精确地指定想调用的是那个函数。以前也叫 UFCS(universal function call syntax),也就是所谓的“通用函数调用语法”,包括成员方法和静态方法。

  • 具体写法为 T as TraitName::item

  • 如果一个类型同时实现了这两个 trait,那么如果我们使用 obj.method() 这样的语法执行方法调用的话,就会出现歧义,编译器不知道你具体想调用哪个方法,编译错误信息为“multiple applicable items in scope”。此时就有必要使用完整的函数调用语法来进行方法调用,只有这样写,才能清晰明白且无歧义地表达清楚期望调用的是哪个函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    trait Cook {
    fn start(&self);
    }
    trait Wash {
    fn start(&self);
    }
    struct Chef;
    impl Cook for Chef {
    fn start(&self) { println!("Cook::start");}
    }
    impl Wash for Chef {
    fn start(&self) { println!("Wash::start");}
    }
    fn main() {
    let me = Chef;
    // me.start(); // 编译错误

    // 应写为下面这种形式
    <Cook>::start(&me);
    <Chef as Wash>::start(&me);
    }
    • 由此也可以看到,所谓的“成员方法”也没什么特殊之处,它跟普通的静态方法的唯一区别是,第一个参数是 self,而这个 self 只是一个普通的函数参数而已。只不过这种成员方法也可以通过变量加小数点的方式调用。变量加小数点的调用方式在大部分情况下看起来更简单更美观,完全可以视为一种语法糖。
    • 通过小数点语法调用方法调用,有一个“隐藏着”的“取引用”步骤。虽然我们看起来源代码长的是这个样子 me.start(),但是真正传递给 start()方法的参数是&me 而不是 me,这一步是编译器自动做的。不论这个方法接受的 self 参数究竟是 Self、&Self 还是&mut Self,最终在源码上,我们都是统一的写法:variable.method()。而如果用 UFCS 语法来调用这个方法,就不能让编译器自动取引用了,必须手动写清楚。

5.5 trait 约束和继承

  • Rust 的 trait 的另外一个大用处是,作为泛型约束使用。

    1
    2
    3
    fn my_print<T : Debug>(x: T) {
    println!("The value is {:?}.", x);
    }

    上面这段代码中,my_print 函数引入了一个泛型参数 T,所以它的参数不是一个具体类型,而是一组类型。冒号后面加 trait 名字,就是这个泛型参数的约束条件。它要求这个 T 类型实现 Debug 这个 trait。

    • 泛型约束还有另外一种写法,即 where 子句

      1
      2
      3
      fn my_print<T>(x: T) where T: Debug {
      println!("The value is {:?}.", x);
      }
  • trait 继承

    1
    2
    trait Base { ... }
    trait Derived : Base { ... }

    这表示 Derived trait 继承了 Base trait。它表达的意思是,满足 Derived 的类型,必然也满足 Base trait。所以,我们在针对一个具体类型 impl Derived 的时候,编译器也会要求我们同时 impl Base。

    实际上,在编译器的眼中,trait Derived:Base{}​ 等同于 trait Derived where Self:Base{}​。这两种写法没有本质上的区别,都是给 Derived 这个 trait 加了一个约束条件,即实现 Derived trait 的具体类型,也必须满足 Base trait 的约束。

5.6 derive: 自动 impl 某些 trait

  • 语法:在你希望 impl trait 的类型前面写 #[derive(…)],括号里面是你希望 impl 的 trait 的名字。

  • 目前,Rust 支持的可以自动 derive 的 trait 有以下这些:

    • Debug, Clone, Copy, Hash
    • RustcEncodable RustcDecodable
    • PartialEq, Eq, ParialOrd, Ord
    • Default, FromPrimitive
    • Send, Sync
    • 这些 trait 都是标准库内部的较特殊的 trait,它们可能包含有成员方法,但是成员方法的逻辑有一个简单而一致的“模板”可以使用,编译器就机械化地重复这个模板,帮我们实现这个默认逻辑。当然也可以手动实现。

5.9 总结

  • trait 其他用处:

    • trait 本身可以携带泛型参数;
    • trait 可以用在泛型参数的约束中;
    • trait 可以为一组类型 impl,也可以单独为某一个具体类型 impl,而且它们可以同时存在;
    • trait 可以为某个 trait impl,而不是为某个具体类型 impl;
    • trait 可以包含关联类型,而且还可以包含类型构造器,实现高阶类型的某些功能;
    • trait 可以实现泛型代码的静态分派,也可以通过 trait object 实现动态分派;
    • trait 可以不包含任何方法,用于给类型做标签(marker),以此来描述类型的一些重要特性
    • trait 可以包含常量。

chap 06 数组和字符串

6.1 数组

  • 数组类型的表示方式为[T; n]。其中 T 代表元素类型;n 代表元素个数;它必须是编译期常量整数。

    • 对于两个数组类型,只有元素类型和元素个数都完全相同,这两个数组才是同类型的。数组与指针之间不能隐式转换。同类型的数组之间可以互相赋值。
    • 多维数组:[[T: m]; n]
  • 数组切片:对数组取借用 borrow 操作,可以生成一个“数组切片”(Slice)。数组切片对数组没有“所有权”。可以把数组切片看作专门用于指向数组的指针,是对数组的另外一个“视图”。

    • 比如,有一个数组[T;n],它的借用指针的类型就是&[T;n]。它可以通过编译器内部魔法转换为数组切片类型&[T]。数组切片实质上还是指针,它不过是在类型系统中丢弃了编译阶段定长数组类型的长度信息,而将此长度信息存储为运行期的值。
  • 数组切片(Slice)是指向一个数组的指针,而它比指针又多了一点东西——它不止包含有一个指向数组的指针,切片本身还含带长度信息。Slice 是胖指针类型。胖指针的设计,避免了数组类型作为参数传递时自动退化为裸指针类型,丢失了长度信息的问题,保证了类型安全

  • 动态大小类型(Dynamic Sized Type,DST)。DST 指的是编译阶段无法确定占用空间大小的类型。为了安全性,指向 DST 的指针一般是胖指针。

    • 对于 DST 类型,Rust 有如下限制:

      • 只能通过指针来间接创建和操作 DST 类型,&[T]、Box<[T]> 可以,[T]不可以
      • enum 中不能包含 DST 类型,struct 中只有最后一个元素可以是 DST,其他地方不行,如果包含有 DST 类型,那么这个结构体也就成了 DST 类型。
  • 边界检查:在 Rust 中靠编译阶段静态检查是无法消除数组越界的行为的。若不确定使用的“索引”是否合法,应该使用 get()方法调用来获取数组中的元素,这个方法不会引起 panic!,它的返回类型是 Option

    • 一般情况下,Rust 不鼓励大量使用“索引”操作。正常的“索引”操作都会执行一次“边界检查”。从执行效率上来说,Rust 比 C/C++ 的数组索引效率低一点,因为 C/C++ 的索引操作是不执行任何安全性检查的,它们对应的 Rust 代码相当于调用 get_unchecked()函数。在 Rust 中,更加地道的做法是尽量使用“迭代器”方法。

6.2 字符串

Rust 的字符串涉及两种类型,一种是&str,另外一种是 String。

6.2.1 &str

  • str 是 Rust 的内置类型。&str 是对 str 的借用,也是一个胖指针。
  • Rust 的字符串内部默认是使用 utf-8 编码格式的。而内置的 char 类型是 4 字节长度的,存储的内容是 Unicode Scalar Value。所以,Rust 里面的字符串不能视为 char 类型的数组,而更接近 u8 类型的数组。
  • &str 类型是对一块字符串区间的借用,它对所指向的内存空间没有所有权,哪怕&mut str 也一样。

6.2.2 String

  • String 类型跟&str 类型的主要区别是,它有管理内存空间的权力。
  • String 实现了 Deref<Target=str> 的 trait。所以在很多情况下 &String 类型可以被编译器自动转换为&str 类型。
  • Rust 的 String 类型类似于 std::string,而 Rust 的&str 类型类似于 std::string_view。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!