Skip to content

Rust 基础

语言特点

内存所有权

String 的内存问题

变量在离开作用域后就被自动释放
将 String 赋值给其它变量,意味着从栈上拷贝了它的指针、长度和容量,并没有复制指针指向的堆上数据。
Rust 的操作方式称为移动而不是浅拷贝。

对于只在栈上的数据进行拷贝。如果一个类型实现了 Copy trait,在旧的变量赋值给其它变量后仍然可用。

向函数传递值,会使变量进入函数的作用域,在离开作用域时占用的内存会被释放。
函数的返回值也会转移所有权,将变量的值移出给调用的函数。

引用与借用

将对象的引用作为参数,而不是获取值的所有权。
&String s pointing at String s1

引用默认不可变,使用可变引用 &mut,对一个变量同时最多只能有一个可变引用;拥有不可变引用时不可创建可变引用。(重叠作用域)

切片类型

引用集合中一段连续的元素序列,而不用引用整个集合。

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

泛型

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

Traits

定义特定类型可能与其它类型共享的功能。
类似于接口。

pub fn notify(item1: impl Summary, item2: impl Summary) {}

pub fn notify<T: Summary>(item1: T, item2: T) {}

通过 where 简化

fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {}

fn some_function<T, U>(t: T, u: U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{}

生命周期

数据比引用有更长的生命周期,则是有效的引用。

生命周期标注表示多个引用的泛型生命周期参数如何相互关系。
不满足约束的值将被借用检查器拒绝。

#![allow(unused)]
fn main() {
    fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
        if x.len() > y.len() {
            x
        } else {
            y
        }
    }
}
// 返回值的生命周期必须与 x, y 一致

静态生命周期,能够存活于整个程序期间。

let s: &'static str = "I have a static lifetime.";

智能指针

Treating Smart Pointers Like Regular References with the Deref Trait

引用计数,允许数据有多个所有者,记录总共由多少个所有者,当没有所有者时清理数据。智能指针相比引用,拥有它们指向的数据。

智能指针实现了 DerefDropDeref 使实例表现像引用一样,Drop 可以自定义离开作用域时的代码。

  • Box<T> 用于在堆上分配值
  • Rc<T> 引用计数类型,其数据可有多个所有者
  • Ref<T>RefMut<T>,只读引用和可变引用

Deref 允许重载解引用运算符 *
Drop 运行清理代码

Rc<T> 通过不可变引用,在程序的多个部分之间只读地共享数据。

use std::rc::Rc;

let a = Rc::new();
let b = Rc::clone(&a);
let c = Rc::clone(&a);

println!("{}", Rc::strong_count(&a));

Rc::clone 只会增加引用计数,不会进行深拷贝。

RefCell<T> 能够在外部值被认为是不可变的情况下修改内部值

use std::cell::RefCell;

struct MockMessenger {
    sent_messages: RefCell<Vec<String>>,
}

impl MockMessagenger {
    fn new() -> MockMessenger {
        MockMessenger { sent_messages: RefCell::new(vec![])}
    }
}

impl Messenger for MockMessenger {
    fn send(&self, message: &str) {
        self.sent_messages.borrow_mut().push(String::from(message));
    }

}

避免引用循环:将 Rc<T> 变为 Weak<T>
调用 Rc::downgrade 会得到 Weak<T> 类型的智能指针。
弱引用并不属于所有权关系,弱引用的循环会在强引用计数为 0 时被打断。

为了使用指向的值,调用 Weak<T>upgrade 方法,返回 Option<Rc<T>>

异步

Rust Futures > Async Rust: What is a runtime? Here is how tokio works under the hood

Rust 并不提供用于执行 Futures 和 Streams 的上下文。如果不进行嵌入式开发,选择 tokio 即可。

异步的核心是 event loops 或称作 processors
每个 event-loop 有自己的任务队列,tokio 中的 processor 如果任务队列为空可以从其它 processor 处抢夺。

tokio::spawn(async move{
    for port in MOST_COMMON_PORTS_100 {
        let _ = input_tx.send(*port).await;
    }
});

可能导致阻塞的耗时任务 tokio::task::spawn_blocking
(后台的无限循环任务应该使用 Thread)

let is_code_valid = spawn_blocking(move || crypto::verify_password(&code, &code_hash)).await?;

系统编程

Writing an OS in Rust

测试

单元测试

使用 #[cfg(test)] 标注只在 cargo test 才编译运行测试代码

pub fn add_two(a: i32) -> i32 {
    a + 2
}
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        assert_eq!(4, add_two(2));
    }
}

集成测试

tests/src/ 同级,其中每一个文件被当作独立的 crate 来编译

use adder;

#[test]
fn it_adds_two() {
    assert_eq!(4, adder::add_two(2));
}

通过指定测试函数的名称作为 cargo test 的参数来运行特定集成测试
也可以指定运行 tests 目录下 integration_test.rs 中的所有测试
cargo test --test integration_test

闭包类型

  • 闭包在被声明时,而非被调用时,会捕获外围词法作用域中的变量
  • 闭包应延长所捕获变量的 lifetime,不能短于闭包自身的 lifetime
  • 变量可通过 move, &mut& 的方式传入函数 - move 将获得环境的所有权 - &mut 将对环境造成可变引用 - & 将对环境造成不可变引用
  • std::ops 中定义了 FnOnce, FnMut, Fn - 所有的 Fn 一定能被实现为 FnMut, FnOnce - 所有的 FnMut 一定能实现为 FnOnce
  • Rust 的内置闭包只包含了环境,即 unboxed closure,因此闭包对象不需要携带函数指针
fn main() {
    println!("{}", std::mem::size_of_val(&main)); // 0

    let mut s = String::from("Hello");
    let mut c = || s.push('!');
    println!("{}", std::mem:size_of_val(&c)); // 8
}
  • Rust 会尽可能选择最宽松的调用,即 Fn > FnMut > FnOnce
  • 使用 move 关键字强制将所有捕获的变量移入闭包的环境
  • 书写高阶函数时对闭包的约束 - 需要纯函数时,书写 Fn - 需要函数保存内部状态时,如伪随机数,书写 FnMut - 类似创建线程的调用,选择 FnOnce
pub fn spawn<F, T>(f: F) -> JoinHandle<T> where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static,

FnOnce

闭包获取了外部数据的所有权,且只可调用一次。数据在调用过程中被移动或释放。

struct MyClosure {
    name: String
}

impl FnOnce for MyClosure {
    fn call_once(self) {
        drop(self.name)
    }
}

FnMut

闭包获取了外部数据的可变引用。

闭包需要是 mut,因为需要获取自身的可变引用。

let mut i: i32 = 0;
let mut f = || {
    i += 1;
}
struct MyClosure {
    i: &mut i32,
}

impl FnMut for MyClosure {
    fn call_mut(&mut self) {
        *self.i += 1;
    }
}

Fn

闭包获取了外部数据的只读引用。

struct MyClosure {
    i: &String,
}

impl Fn for MyClosure {
    fn call(&self) {
        println!("{}", self.msg);
    }
}

move 关键字

move - Rust
Closures: Anonymous Functions that Capture Their Environment

在闭包内,将通过引用或可变引用获取的变量转化为由值获取的变量。

如果闭包内使用了数据的引用,但闭包与引用数据的生命周期不同,使用 move 来明确获取数据所有权。

fn create_closure() -> impl Fn() {
    let msg = String::from("hello");
    let v: Vec<i32> = vec![1, 2];

    move || {
        println!("{}", msg);
        println!("{:?}", v);
    }
}
struct MyClosure {
    msg: String,
    v: Vec<i32>,
}

impl Fn for MyClosure {
    fn(&self) {
        println!("{}", self.msg);
        println!("{:?}", self.v);
    }
}

[!EXAMPLE]
惰性求值,只在需要结果时执行闭包,并会缓存结果值

struct Cacher<T>
  where T: Fn(u32) -> u32
{
  calculation: T,
  value: Option<u32>,
}

impl<T> Cacher<T>
  where T: Fn(u32) -> u32
{
  fn new(calculation: T) -> Cacher<T> {
      Cacher {
          calculation,
          value: None,
      }
  }

  fn value(&mut self, arg: u32) -> u32 {
      match self.value {
          Some(v) => v,
          None => {
              let v = (self.calculation)(arg);
              self.value = Some(v);
              v
          },
      }
  }
}
// 改用 HashMap 以支持不同的 arg