3

Rust学习笔记

 2 years ago
source link: https://taodaling.github.io/blog/2021/11/21/rust%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Rust学习笔记

Published: November 21, 2021 by Daltao

本文只是对《Rust 程序设计语言》的读书笔记。

数据可能分配在栈上和堆上。一个分配在栈上的数据可以通过默认的浅拷贝完整克隆,也更为廉价。而需要共享的数据更适合分配在堆上,这样只需要在栈上保留一个指向堆位置的指针即可,这样可以避免大量的拷贝操作。如果想要同时拷贝堆上的数据,可以用clone方法。

栈上的数据在退栈的时候会被自动释放,但是堆上不同。堆上的数据由于可能被多个指针引用,不能轻易决定释放时机。

Rust中引入所有权的概念,一个数据的所有权被正好一个变量获得,当这个变量离开作用域的时候,它的数据也会被销毁。

    let s1 = String::from("hello");
    let s2 = s1; //s2接管s1的所有权,s1失去了所有权,不能再被使用

在栈上直接分配的数据实现了名为Copytraits,在赋值给另外一个变量的时候并不会失去所有权,而是将数据完整拷贝给了另外一个变量。

    let x = 5;
    let y = x; //x并不会失去所有权

以下类型的数据实现了Copytraits

  • 仅包含实现了Copytraits数据的元组
fn take_ownership(x: String){ //
    println!("{}", x);
}

fn main(){
    let s = String::from("hello");
    take_ownership(s); //这里s失去所有权
}

由于将数据作为函数参数传递也会使得变量失去所有权,因此我们必须将所有权从函数中传回。

fn take_ownership(x: String) -> String{ //
    println!("{}", x);
    x
}

fn main(){
    let s = String::from("hello");
    let s = take_ownership(s);
}

这样做很麻烦,尤其在参数比较多的情况下。我们可以用引用来优化这一过程。

fn borrow(x: & String){ 
    println!("{}", x);
}

fn main(){
    let s = String::from("hello");
    borrow(& s);// 这里s不会失去所有权
}

引用也分为mutimmutable两种。前者会对数据加写锁,后者会对数据加读锁(这里并不真的在运行期加锁,实际上都是编译期的工作,这里只是为了方便理解)。因此一个数据最多有一个mut类型的应用,或者多个immutable类型的引用。之所有这么设计是为了防止race condition。

fn main(){
    let mut s = String::from("hello"); //s得到所有权
    let mut_ref_s = &mut s; //获得一个mut引用
    mut_ref_s.push_str(" world"); //修改mut引用,由于这里是mut_ref_s的最后一次被使用,因此它的生命周期在此结束
    let immutable_ref_s = &s; //创建一个immutable引用
    println!("{}", immutable_ref_s);
}

如果所有权变量被销毁,那么所有存活的引用(这种引用称为悬置引用,dangling reference)都是不可用的,这时候再使用这些引用对象会引起编译错误。

fn wrong() -> &String {
    let s = String::from("hello");
    &s //在这语句后s会销毁,因此返回的引用也将非法
}

fn correct() -> String {
    let s = String::from("hello");
    s //返回所有权可以避免数据被销毁
}

引用的本质是指针。类似C++,我们也可以用*ref来获取引用具体指向的值,但是rust编译器很多时候可以很聪明的推断出我们实际上使用的是引用指向的元素,因此这一步可以缺省。

fn main(){
    let x = 5;
    let y = &x;
    assert_eq!(5, x);
    assert_eq!(5, *y);
    assert_eq!(&5, y);
}

Slice

Slice用来表示某个数据结构的连续的一部分,它不具有所有权,但是会作为原数据结构的immutable引用存在。

fn first_word(s: &String) -> &str{
    for (i, &c) in s.as_bytes().iter().enumerate(){
        if(c == b' '){
            return &s[0..i]
        }
    }
    &s[..]
}
fn main(){
    let s = String::from("hello world");
    let fw = first_word(& s);
    s.clear(); // 调用s.clear()必须先获得s的一个mut引用,而fw是s的一个imutable引用,这里会报错
    println!("{}", fw);
}

一些slice类型:

  • 数组类型,比如&[i32]
  • 字符串类型,str

struct

rust也支持用结构体来组织数据。

struct Rect{
    width: u32,
    height: u32
}

结构体的初始化非常简单:

    let rect = Rect{
        width: 100,
        height: 200
    };

如果我们有同名变量,可以省略初始化时候使用的字段名称

    let width = 10;
    let height = 200;
    let rect = Rect{
        width,
        height
    };

如果你希望从另外一个变量中拷贝大部分字段,但是覆盖其中少部分字段,rust通用提供了语法糖。注意这仅仅只是个语法糖,实际上本质上还是会把需要的属性逐一进行拷贝,这可能会导致所有权的变动。

    let rect2 = Rect{
        width: 200, //覆盖rect中的字段width
        ..rect //表示从rect复制字段,必须放在最后
    };

tuple structs

有时候我们并不需要一个为每个字段提供一个名字,我们需要为tuple声明一个类型。注意不同的tuple struct类型,即使拥有相同的声明,它们的实例也是不能相互转换的。

struct Point (i32, i32);

fn main() {
    let pt = Point(0, 0); //初始化
    println!("{}", pt.0); //类似于tuple通过.下标来获取元素
}

unit-like struct

一个struct允许没有任何字段,这样的struct称为unit-like struct。

    struct AlwaysEqual;
    let subject = AlwaysEqual;

method

我们可以为struct实现特有的函数,这类函数称为关联函数。关联函数的名称可以于struct的某个field相同。

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 { //不可变self引用
        self.width * self.height
    }
    fn init(&mut self){  //可变self引用
        self.width = 0;
        self.height = 0;
    }
}

impl Rectangle { //对于同一个struct,可以有多个impl块
    fn give_back(self) -> Rectangle { //获得所有权
        self
    }
    fn square(size: u32) -> Rectangle { //关联函数也可以没有self参数
        Rectangle{
            width:size, 
            height:size
        }
    }
}

fn main() {
    let mut rect = Rectangle {
        width: 30,
        height: 50,
    };
    let area = rect.area();
    rect.init(); //这个调用和下一行的调用是等价的,rust会自动创建引用作为第一个参数传入
    (&mut rect).init();
    rect = rect.give_back();
    rect = Rectangle::square(32);
}

这里&selfself: &Self的缩写,其中Self是impl后面接的类型在这个impl块中的别名。

rust中我们enum类型更像是一种类型的分组,它内部可以包含多个具有别名的类型。

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

impl Message{
    fn distance(&self) -> i32{
        match self {
            Message::Move { x, y  } => x + y,
            default => 0
        }
    }
}

fn main() {
    let msg = Message::Move{x : 1, y : 1};
    println!("{}", msg.distance());
}

可以发现enum中包含多个子类型,并且enum是在栈上分配内存的,那么enum类型的大小必定是在编译期可知的。实际上rust会分配最大子类型的大小作为enum类型的大小。

match

match可以用来处理整数、枚举、字符串等。

let roll = 1;
let mut x = 0;

let res = match roll { //match可以带返回值
    0 => 0,
    1 => 1,
    other => -1, //用other匹配所有情况
}

match roll {
    0 => x += 1,
    1 => x -= 1,
    _ => () //_表示匹配任意值并丢弃,()表示什么都不做,{}也是相同作用
}

枚举类型也可以同样操作:

    let max = Some(1);
    match max {
        Some(x) => println!("max = {}", x),
        _ => ()
    }

很多时候我们仅处理一种枚举类型,但是这要求我们总是加入_ => ()行,这比较麻烦,还有一种if let语法。

    let max = Some(1);
    if let Some(x) = max {
        println!("max = {}", x);
    } else { //else块是可选的
    }

在rust中,用mod声明一个模块,其类似于其他语言的命名空间。模块内部可以定义其它模块,或者自定义类型、函数等。

默认情况下一个元素仅对于相同父模块及后代模块中的元素可用,要对外部模块可用,我们需要加上pub关键字。模块中如果我们声明某个定义的元素是pub,表示这个元素的所有祖先模块都能访问它。要使用其它模块中的元素,我们需要通过相对路径或者绝对路径来访问。这类似于类unix系统中的路径表示法,默认路径为相对路径,我们用crate表示根路径(即当前包名称),super表示当前元素所在mod的父mod。

每次都需要用冗长的路径来使用相同元素是很麻烦的,我们可以用use来在当前scope引入某个特定的名称。为了避免引入拥有相同名字,但是存在于不同mod下的元素,我们需要通过alias设置别名,默认别名就是元素的名称。use也有访问控制,默认这个别名是不能被mod外访问的,我们可以加入pub修饰符使得它能够被mod外访问。

mod department {
    mod service {
        use super::House; //使用别名
        pub use super::House as H; //使用别名,并暴露mod外
        use super::*; //引入父模块的所有名称
        pub fn clean(house: &mut super::House){

        }
    }

    pub struct House {
        pub address: String,
        pub opened: bool,
        key: String, //私有field
        key_type: KeyType
    }

    pub enum KeyType{
        Physical,
        Electric
    }

    impl House{
        pub fn open(&mut self, key: &String) { //公有方法
            if self.isKeyValid(key) {
                self.opened = true;
            }
        }
        pub fn newHouse() -> House{
            House {
                address: String::from("South"),
                opened: false,
                key: String::from("123"),
                key_type: KeyType::Physical,
            }
        }
        
        fn isKeyValid(&self, key: &String) -> bool { //私有方法
            &self.key == key
        }
    }

    
    pub fn simpleTest() {
        let mut house = crate::department::House::newHouse();
        let mut house = House::newHouse();
        let mut house = super::department::House::newHouse();
        service::clean(&mut house);
        use service::clean; //引入clean
        clean(&mut house);
    }
}

要使用集成测试,我们需要先为src目录建立一个同级目录tests。这个目录中的文件仅在cargo test的时候才会被编译,并且包内的元素并不处于crate下。

如果我们有公共的模块需要供测试使用,我们一般会专门放在一个文件中。但是这样会导致测试的时候这个文件也作为集成测试的一部分显示出来。要不显示我们需要使用一个trick,通过建立{modname}/mod.rs,来讲公共方法放在这个文件中。

use adder::*;

mod common;

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

我们可以很简单的用cargo实现模块管理。首先我们创建一个简单的项目your_lib

# cargo new --lib your_lib

之后创建如下文件,src\lib.rs

pub mod sample;

在创建新的文件src\sample\mod.rs

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

之后我们在your_lib的父目录下建立一个项目practice

# cargo new practice

之后修改main.rs文件

use your_lib::sample::add;
fn main(){
    println!("1 + 2 = {}", add(1, 2));
} 

但是我们还需要加入对sample项目的依赖,由于是本地依赖,我们需要修改Cargo.toml依赖。

simple = {path = "./your_lib"}

rust提供了两种字符串类型,一种是str,一种是String。我们一般用到的字面量比如"hello"就是str类型的,字面量数据硬编码在二进制文件中,我们应该用&str类型来引用它们。String类型可以认为是可变的str类型。

两种类型可以互相转换:

    let s: &str = "hello";
    let s: String = s.to_string();
    let s: &str = s.as_str();

下面演示如何修改String类型。

    let mut s = "hello".to_string();
    s.push(' ');
    s.push_str("world"); 
    s += &"!".to_string(); //等价于s = s + &"!".to_string();,这里s会追加"!",并返回新的所有权
    s = s + "!";
    println!("{}", s);  

这里字符串的加法操作实际调用的是一个类似fn add(self, s: &str)的函数。

在rust中,字符串类型采用utf8编码,因此每个字符的占用空间从1字节到4字节不等,这导致我们无法高效的获取字符串的第i个字符,因此rust禁用了对字符串下标取值。

let s = "hello";
let c = s[0]; //非法操作

在rust中字符串可以有三种表现形式,考虑字符串"नमस्ते"

  • 字节数组,通过.bytes()方法获得:[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, 224, 165, 135]
  • unicode字符(rust中char的实际编码),通过.chars()方法获得:['न', 'म', 'स', '्', 'त', 'े']
  • 字位:["न", "म", "स्", "ते"]

那么我们在使用字符串切片的时候具体是使用哪种表现形式呢。实际上我们使用的是字节数组,s[0..4]表示取s字节数组的最前4个字符组成一个新的字符串。但是如果这四个字符无法组成一个合法的字符串呢?这时候程序会抛出运行时异常。所以在使用字符串切片要额外小心。

rust中错误分为可恢复错误和不可恢复错误。对于不可恢复错误,应该用painc!宏来结束线程,而对于可恢复错误,应该将其包装为Result枚举类返回给调用者,由调用者决定是否恢复。

下面演示如何用panic!处理不可恢复异常。

fn get_file_0(s: &String) -> File {
    let f = File::open(s);
    match f {
        Ok(x) => x,
        Err(err) => panic!("{:?}", err),
    }
}
fn get_file_1(s: &String) -> File {
    let f = File::open(s);
    f.unwrap()
}
fn get_file_2(s: &String) -> File {
    let f = File::open(s);
    f.expect("can't open file")
}

上面的三个函数拥有相同的效果,如果能正常打开文件则返回文件,否则结束线程。

下面演示如何处理可恢复异常:

fn get_file_0(s: &String) -> Result<File, std::io::Error> {
    let f = File::open(s);
    f
}
fn get_file_1(s: &String) -> Result<File, std::io::Error> {
    let f = File::open(s)?; //?表示如果成功,则返回结果,否则将异常转换类型后作为当前函数返回值返回
    Ok(f)
}
fn get_file_2(s: &String) -> Result<File, std::io::Error> {
    let f = File::open(s);
    match f {
        Ok(x) => Ok(x),
        Err(err) => Err(err)
    }
}

上面的三个函数拥有相同的效果,返回一个Result表示操作结果。

一般情况下main函数没有返回值,但是你可以为其增加一个额外的返回值以返回Result

fn main() -> Result<(), Box<dyn Error>> {
    let f = get_file_0(& "hello".to_string())?;
    Ok(())
}

Rust支持泛型,类似于C++,泛型不会有运行时费用,所有工作都在编译期完成。

struct Point<T, U>{ //泛型类型
    x: T,
    y: U,
}

impl<T, U> Point<T, U> { //泛型类型的关联函数
    fn x(&self) -> &T{
        &self.x
    }
    fn combine<W, V>(self, pt: Point<W, V>) -> Point<T, V>{ //泛型关联函数
        Point{
            x: self.x,
            y: pt.y,
        }
    }
}

fn y<T, U>(pt: &Point<T, U>) -> &U { //泛型函数
    &pt.y
}

impl Point<f64, f64>{ //特化
    fn distance(&self) -> f64{
        (self.x * self.x + self.y * self.y).sqrt()
    }
}

impl<T: PartialOrd> Point<T, T> { //只有T实现了偏序关系,才拥有这些方法
    fn max_element(&self) -> &T{
        if self.x > self.y {
            &self.x
        }else{
            &self.y
        }
    }
}

泛型参数支持默认值:

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

trait

trait类似于其它语言中的接口,它包含一组方法签名,任意类型都可以实现这些trait。但是不允许在当前包中为为一个外部包的类型实现某个外部包的trait,这样设计的好处是避免同一个类型将某个trait实现多次。

trait中也可以包含方法的默认实现。


pub trait Summary{
    fn summarize(&self) -> String;
    
    fn summarizeLong(&self) -> String { //方法可以有默认实现
        self.summarize() + "..."
    }
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

pub fn notify(item: &impl Summary) { //impl Summary表示所有实现了Summary trait的类型
    println!("Breaking news! {}", item.summarize());
}

fn main(){
    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    };

    notify(&tweet);
}

考虑如下方法

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

它表示接受两个实现了Summary trait的参数,但是不强制要求它们具有相同的类型。如果我们希望它们拥有相同的类型,需要借助泛型:

pub fn notify<T: Summary>(item1: &T, item2: &T) { //T必须实现Summary trait

如果要求类型同时实现多个trait:

//非泛型版本
pub fn notify(item: &(impl Summary + Display)) {
//泛型版本
pub fn notify<T: Summary + Display>(item: &T) {

很显然随着需要实现的trait数目的增多,会导致方法签名越来越复杂,rust提供了一个where语法糖来优化:

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

我们可以将方法的返回值也改成trait,但是方法必须只能返回同一种类型的数据。

fn returns_summarizable(switch: bool) -> impl Summary {

我们也可以利用泛型为所有实现了某些trait的类型增加一些额外的方法,比如标准库中的ToString trait。

impl<T: Display> ToString for T { //为所有实现了Display的元素实现to_string方法
    // --snip--
}

由于不同泛型参数对应的是不同的类型,因此一个结构体可以实现多个不同泛型参数的相同trait。如果我们希望即拥有泛型的能力,又只希望同一个类型不能重复实现trait,那么就需要用到关联类型。比如标准库中的迭代器trait:

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

全限定名方法

我们类型可能实现了多个trait,一些trait中包含了完全相同签名的方法,这时候我们需要通过全限定名的方式来指定具体执行哪个方法。

struct E;

trait A {
    fn go(&self);
}

trait B {
    fn go(&self);
}

impl A for E {
    fn go(&self) {
        println!("A");
    }
}

impl B for E {
    fn go(&self) {
        println!("B");
    }
}

impl E {
    fn go(&self) {
        println!("E");
    }
}

fn main() {
    let e = E;
    e.go();
    A::go(&e);
    B::go(&e);
    E::go(&e);
}

上面能正常识别的前提是有&self作为参数,如果是静态函数如何指定具体调用哪个实现。

struct E;

trait A {
    fn go();
}

trait B {
    fn go();
}

impl A for E {
    fn go() {
        println!("A");
    }
}

impl E {
    fn go() {
        println!("E");
    }
}

如果我们直接调用A::go(),编译器无法确定是使用哪个实现。我们需要新的写法<Type as Trait>::function

fn main() {
    let e = E;
    E::go();
    <E as A>::go();
}

Supertrait

如果实现一个trait,需要类型必须先实现另外一个trait,后者称为前者的Supertrait,这种约束很容易表达:

trait OutlinePrint: fmt::Display { //实现OutlinePrint必须先实现fmt::Display
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {} *", output);
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

impl OutlinePrint for u32{} //成功编译
impl OutlinePrint for Vec<u32>{} //无法编译

lifetime

当我们在函数中返回引用的时候,rust编译器无法得知返回值的存活时间。这时候返回值一般和入参有关(因为在函数内部创建的元素都会在函数退出时被销毁),其生命周期也和入参挂钩。我们需要指定返回值具体依赖哪些入参的生命周期。

fn longest<'a>(s1: &'a String, s2: &'a String) -> &'a String { //增加生命周期类似于增加泛型参数,不过生命周期是由“'{名称}”格式组成
    if(s1.len() < s2.len()){
        s2
    }else{
        s1
    }
}

上面编译器会理解返回值的生命周期不能超过s1引用的元素,也不能超过s2引用的元素。如果我们一旦在s1s2生命周期结束后还使用这个函数的返回值,编译器能及时发现问题并报告。

同样如果一个结构体中包含引用,这时候我们需要对结构体的生命周期加以限制,保证它的生命周期不会大于任意一个引用成员的生命周期。

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

注意生命周期仅仅是帮助编译器来确定程序是否合法,是否可能存在悬置引用。在以下情况下,如果我们省略生命周期,编译器会自动帮助我们填充生命周期,这使得我们写代码更加简单:

  1. 方法只有一个入参,这时候所有引用返回值的生命周期会默认使用入参的生命周期
  2. 方法拥有多个入参,但是有参数self,那么所有引用返回值的生命周期会默认使用self的生命周期。

如果上面两个方法都不管用,编译器会报错提醒我们。

cargo内置了测试框架,我们可以通过cargo test来执行所有的测试用例。

我们需要在测试函数上加上属性#[test]来标注这个函数为测试用例。对于这类函数,rust会为每个函数启动一个线程来执行,如果在一个函数中调用panic!或者返回Err会视作测试失败。但是如果我们在函数上加上属性#[should_fail],那么只有在函数中调用panic!或者返回Err会视作测试成功。

由于默认情况下会使用多线程,因此我们需要保证代码不会产生竞争条件。或者我们可以用--test-threads=1来指定最多同时运行一条线程。

如果我们只想测试一个函数,我们也可以追加这个函数名字来要求cargo只测这个函数,比如cargo test {name},这时候cargo会只测试函数名中包含{name}的所有函数。

对于跑的很慢的测试用例,我们可以在上面加上#[ignore]注解,这样在运行cargo test的时候会忽略这些函数。我们可以用cargo test -- --ignored来仅运行那些被忽略的函数。

pub fn greeting(name: &str) -> String {
    format!("Hello! {}!", name)
}
#[cfg(test)] //#[cfg(test)]属性要求仅在cargo test的时候编译只部分代码,而在cargo build的时候忽略。
mod tests {
    use super::*;
    #[test]
    fn a_new_name() {
        let result = 2 + 2;
        assert_eq!(result, 4); //assert_eq!在传入的两个参数不等的情况下会调用panic!
        assert!(result == 4); //assert_eq!在传入的两个参数相等的情况下会调用panic!
    }
    #[test]
    #[should_panic]
    fn must_fail() {
        panic!("fail by hand");
    }
    #[test]
    #[ignore] //默认不运行这个函数
    fn greeting_test(){
        let res = greeting("Carol");
        assert!(res.contains("Carol"), "Greeting didn't contain name, value was '{}'", res);
    }
    #[test]
    fn it_works() -> Result<(), String> {
        if 2 + 1 == 4 {
            Ok(())
        }else{
            Err("two plus two does not equal four".to_string()) //返回Err等价于调用panic!
        }
    }
}

我们可以用下面方法创建一个闭包(匿名函数)。

let add = |x, y| x + y;
let add = |x, y| {x + y};
let add = |x: i32, y: i32| -> i32 {x + y};

在rust中我们不需要写出闭包的参数类型和返回值类型,编译器会自动替我们做出决定。这与函数不同,因为函数可能会暴露给包外,我们必须声明类型帮助编译器来确定函数的格式,而闭包只会作用在很小的作用域中,这足够让编译器做出决定。

闭包允许持有外部变量,一个闭包必定实现了FnFnOnceFnMut中的某个trait,它们的区别在于:

  • Fn:以不可变引用的方式捕获外部变量
  • FnMut:以可变引用的方式捕获外部比那里
  • FnOnce:获得外部变量的拥有权,这种闭包只能被调用一次。

默认所有闭包都实现了FnOnce,如果闭包没有获得外部变量的所有权,那么它同样会实现FnMut。如果闭包没有修改外部变量,则它会实现Fn。同样的如果我们希望传递闭包,我们必须获得闭包的类型。闭包的类型可以用实现的trait来表示,比如上面的add的闭包类型为Fn(i32,i32)->i32

struct Function {
    f: Fn(i32, i32) -> i32
}

impl Function {
    fn f(&self, x: i32, y: i32) -> i32{
        (self.f)(x, y)
    }
}

我们也可以强制闭包获得外部变量的所有权,这可以通过move关键字实现。这在将闭包交给新线程运行时会用到。

    let x = vec![1, 2, 3];
    let equal_to_x = move |z| z == x;

我们可以通过foreach遍历一个迭代器。

    let arr = [1, 2, 3];
    let iter = arr.iter();
    for e in iter {
        println!("{}", e);
    }

迭代器在rust是一个trait,要实现这个trait我们需要实现它的next方法。

struct Range {
    l: i32,
    r: i32,
}

impl Range {
    fn new(l: i32, r: i32) -> Self{
        Self{l, r}
    }
}

impl Iterator for Range {
    type Item = i32;
    fn next(&mut self) -> Option<Self::Item> { 
        if(self.l <= self.r){
            let res = Some(self.l);
            self.l += 1;
            res
        }else{
            None
        }
    }
}

rust编译器会把我们的迭代器自动优化从而达到接近手动循环的性能,因此可以放心食用。

cargo

cargo中有profile的概念,默认情况下cargo build使用dev profile。cargo build --release使用release profile。

cargo会为每个profile提供一个默认配置,我们也可以用通过在Cargo.toml文件中加入[profile.*]来覆盖它的默认设置。

[profile.dev]
opt-level = 0

[profile.release]
opt-level = 3
配置 解释 取值
opt-level 编译时优化级别 0-3,越大数字表示优化级别越高

文档注释和一般注释不同,文档注释有两种:

  • ///:注释接下来的元素
  • //!:注释包含这个注释的元素,比如mod、crate

文档注释支持markdown风格,并且用一对```包含的样例代码块会作为doc test的一部分进行测试,保证文档和代码是同步的。如果你希望它不会被允许,可以加上no_run属性。

/// ```ignore //忽略代码
/// fn foo() {
/// ```

/// ```should_panic //只有panic退出才算成功
/// assert!(false);
/// ```

/// ```no_run //只编译不允许
/// loop {
///     println!("Hello, world");
/// }
/// ```

/// ```compile_fail //应该编译失败
/// let x = 5;
/// x += 2; // shouldn't compile!
/// ```
# cargo doc //生成html文档
# cargo doc --open //生成并打开html文档

cargo.io

cargo.io是一个rust包仓库。

cargo.io的使用非常简单,首先注册账号。之后点击Account Setting -> API Access -> New Token获得一个新的token。

# cargo login {token} //登陆并记录token

要发布一个新的包:

# cargo publish

发布包需要保证下面几点:

  • license
  • description

下面提供一个Cargo.toml的样例。

[package]
name = "minigrep-dalingtao"
version = "0.1.1"
edition = "2021"
license = "MIT"
description = "test project"

[dependencies]

如果我们更新了包的版本并重新上传后,希望禁用之前的包,我们会发现无法删除之前的包,因为这样做会break很多用户的Cargo.lock。但是我们可以将之前的包标记为yank,这样可以原来依赖旧版本的项目可以继续拉取这个包,而新依赖不能拉取旧包。

在Rust中,智能指针包括:

智能指针实现了Deref trait,因此我们可以直接像使用引用一样直接使用指针指向的值,而不需要先提取Box封装的指针。同时智能指针实现了Drop trait,允许在被销毁前做一些额外的操作,这时候它会释放指针指向的堆内存。

在使用$x的时候等价于的时候等价于(x.deref())$。

rust还提供了deref coercoin技术,简单讲就是如果一个结构体AA实现了deref为BB,那么rust可以自动将AA的引用转换为BB的引用。并且解引用的代码在编译期插入,没有额外的运行期费用。

    let a = "hello".to_string();
    let b = Box::new(a);
    let z: &str = &b; //&Box<String> -> &String -> &str

为了提供可变指针,还有一个类似的trait:DerefMut

对于Drop trait,它会在元素销毁之前被调用,来释放资源。如果我们希望提前释放对象,我们可以声明一个新的空函数,让它夺走传入变量的所有权,这个函数非常常见,因此rust标准库中包含了这个函数drop(x)来释放对象x

Box用来存储一个指向堆上的指针。它和一般的指针类似,并没有额外的开销。

对于第一点,考虑我们要实现一个单向链表:

enum List {
    Next(i32, List),
    End
}

这样实现是不行的,因为要计算枚举List的大小,必须确定每个子类型的大小。这就会导致无穷递归的问题。我们可以用Box指针来解决这个问题。

enum List {
    Next(i32, Box<List>),
    End
}
fn main() {
    use List::*;
    let x = Next(1, Box::new(Next(2, Box::new(End))));
}

Box智能指针存在一个问题,就是每个指针必须获得元素的所有权。是否有可能一个元素被多个元素所共享,类似于不可变引用。这可以通过Rc来实现。Rc是一个基于引用计数的指针,当克隆指针时计数加一,指针释放的时候计数减少一,如果计数为零则释放资源。

use std::{ops::Deref, rc::Rc};
struct Node {
    adj: Vec<Rc<Node>>
}
impl Node {
    fn new(adj: Vec<Rc<Node>>) -> Self {
        Node { adj }
    }
}

fn main() {
    let a = Rc::new(Node::new(Vec::new()));
    let b = Rc::new(Node::new(vec![Rc::clone(&a)]));
    let c = Rc::new(Node::new(vec![Rc::clone(&a)]));
}

这里我们要拷贝Rc需要使用Rc::clone,它会执行浅拷贝,会增加计数。但是Rc只能获得不可变引用。

RefCell

Rust的borrow规则要求:

  • 不能同时存在可变引用和不可变引用
  • 最多只能有一个可变引用

但是这时候我们会发现如果我们希望构造一个有环图会非常困难。由于一个顶点会被多个其它顶点的邻接表存储,因此我们必须用Rc来存储顶点信息。这导致我们不能修改顶点,自然也无法构造一个有环图。

上面失败的原因是无法通过编译期检测,但是实际上在运行期,只要没有多线程,这个程序完全是没有问题的,因为始终只需要维护一个可变引用。

RefCell里面包含了一些unsafe code,允许我们通过它的一个不可变引用得到它存储的可变引用。RefCell提供了两个方法,borrow_mut以获得其中的可变引用,borrow用于获得一个不可变引用。为了保证borrow规则,rust会额外维护一些计数器来保证运行时没有违背borrow原则,一旦违背会调用panic结束线程。因此会有一定的额外运行时开销。

use std::{ops::Deref, rc::Rc};
use std::cell::RefCell;
struct Node {
    adj: Vec<Rc<RefCell<Node>>>
}
impl Node {
    fn new() -> Self {
        Node { adj:Vec::new() }
    }
}


fn main() {
    let a = Rc::new(RefCell::new(Node::new()));
    let b = Rc::new(RefCell::new(Node::new()));
    let c = Rc::new(RefCell::new(Node::new()));

    a.borrow_mut().adj.push(Rc::clone(&b)); 
    b.borrow_mut().adj.push(Rc::clone(&c));
    c.borrow_mut().adj.push(Rc::clone(&a));
    a.borrow();
}

RefCell演示了如何创建图论中的环形关系,但是可以发现引用计数+环形依赖会导致计数永远大于0,即环上的元素不能被正确释放。这时候我们等程序结束后由操作系统释放所有资源。

解决方案是依赖使用Weak指针来表达。Rc指针可以被降级为Weak指针,而Weak指针的存在不会导致计数值的变化,即即使存在Weak指针,其指向的元素可能也会被释放。这也导致我们不能直接获得Weak指针指向的地址,Weak指针会返回一个Option对象来表示指向的位置是否依旧可用。

fn main() {
    let a = Rc::new(RefCell::new(Node::new()));
    let b = Rc::new(RefCell::new(Node::new()));
    let c = Rc::new(RefCell::new(Node::new()));

    a.borrow_mut().adj.push(Rc::downgrade(&b)); //创建weak指针
    b.borrow_mut().adj.push(Rc::downgrade(&c));
    c.borrow_mut().adj.push(Rc::downgrade(&a));
    a.borrow();

    let bref: Rc<RefCell<Node>> = a.borrow().adj[0].upgrade().unwrap(); //判断元素是否存在
    let should_be_b: &Node = &bref.borrow();
}

如大部分编程语言一样,rust也支持多线程。

use std::{thread, time::Duration};

fn main(){
    let x = "new thread".to_string();
    let t = thread::spawn(move || {
        for i in 1..10 {
            println!("hi {} from {}", i, x);
            thread::sleep(Duration::from_millis(1));
        }
    });
    for i in 1..5 {
        println!("hi {} from current thread", i);
        thread::sleep(Duration::from_millis(1));
    }
    t.join();
}

在rust中,线程之间通过管道而非共享内存进行通信。

use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
    //mpsc表示multi-provider-single-consumer。
    let (tx, rx) = mpsc::channel();
    //利用clone方法获得新的provider
    let tx1 = tx.clone();
    thread::spawn(move || {
        let vals = vec![
            String::from("hi"),
            String::from("from"),
            String::from("the"),
            String::from("thread"),
        ];

        for val in vals {
            //如果receiver一端已经被销毁,result会是Err
            tx1.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    thread::spawn(move || {
        let vals = vec![
            String::from("more"),
            String::from("messages"),
            String::from("for"),
            String::from("you"),
        ];

        for val in vals {
            tx.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    //消费所有收到的数据,直到所有发送端都被销毁
    for received in rx {
        println!("Got: {}", received);
    }
}

要在多线程之间共享元素,我们很自然可以想到用RcRefCell,但是它们都不是线程安全的。Rust提供了它们的线程安全版本:ArcMutex,前者通过原子性保证计数器的安全,后者通过加锁来防止race condition。

use std::sync::{Arc, Mutex, mpsc};
use std::thread;
use std::time::Duration;

fn main() {

    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];
    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("{}", *counter.lock().unwrap());
}

一些trait

只有元素实现了Send trait,这样的元素才能在不同线程间传递所有权。

在Rust中所有基础类型都实现了Send trait,除了raw指针。一个结构体如果所有成员都实现了Send,那么这个结构体也默认实现了Send

在Rust中,Rc没有实现Send。

如果一个类型的引用实现了Send,那么它默认实现了Sync trait。默认所有原生类型都实现了trait,一个结构体的所有成员都实现了Sync,那么它也会默认实现Sync。

在Rust中,RcRefCellCell等类型没有实现Sync。

一般我们无法实现多态,比如说我们不能创建一个Vec<Drop>,因为Drop是一个trait,在编译期无法确定它的具体实现类,自然也不知道它的具体大小。

一种简单的方式是我们使用枚举类:

enum NumType {
    Integer(i32),
    Float(f64)
}

let v = vec![Integer(1), Float(0.1)];

但是这种实现的弊端也是存在的,作为包发布的时候我们无法扩展它的内容,同时枚举类占用的内存可能比需要的多。

还有一种动态分发的方式。我们可以用dyn {Trait}表示一个Trait Object,它们可以作为指针的泛型参数。

trait Go {
    fn go(&self);
}

impl Go for i32 {
    fn go(&self) {
        println!("This is i32: {}", *self);
    }
}


impl Go for f64 {
    fn go(&self) {
        println!("This is f64: {}", *self);
    }
}

struct Container {
    v: Vec<Box<dyn Go>>
}

fn main() {
    let mut c = Container{v : Vec::new()};
    c.v.push(Box::new(1));
    c.v.push(Box::new(1.1));
    for x in c.v.iter(){
        x.go();
    }
}

并不是所有trait都可以作为trait object的,这里有一些限制:

  • trait中不能包含返回Self或Self引用的方法
  • trait没有泛型参数

并且动态分发会导致编译期无法推断具体的类型,这会导致在编译期无法执行一些优化(比如内联代码),同时在运行期要确定具体类型也需要一些额外开销(用和C++类似的虚表实现)。

unsafe

如果仅使用rust已有的功能,会发现很多功能不能实现,比如说底层编程的时候。rust允许我们使用unsafe关键字来实现很多超能力。

我们可以通过unsafe{}块来给予某个代码块unsafe权限,也可以将unsafe加在fn前,给予函数unsafe权限。

一个标记为unsafe的函数,必须只能在unsafe块或其他unsafe函数中调用。

指针解引用

rust中也存在raw指针,一个指向i32的常量指针为*const i32,而可变指针为*mut i32。一般情况下我们不能直接解引用指针。

fn main() {
    let mut x = 3;
    let ptr: *mut i32 = &mut x;
    println!("{}", *ptr); //这一行会编译报错
}

通过增加unsafe块后可以编译通过。

fn main() {
    let mut x = 3;
    let ptr: *mut i32 = &mut x;
    unsafe{
        println!("{}", *ptr);
    }
}

调用外部代码

我们可以通过unsafe权限调用外部代码。所有的外部代码都需要通过unsafe权限才能调用。

extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}

全局静态变量

rust中允许我们定义全局静态变量,但是要修改和读取全局静态变量需要unsafe权限。

static mut counter: u32 = 0;
fn add(x: u32) {
    unsafe{
        counter += x;
    }
}
fn main() {
    add(3);
    add(2);
    unsafe {
        println!("{}", counter);
    }
}

unsafe trait

对于一些trait,比如Send和Sync,一般由编译器自动帮助实现。但是有些元素编译器并不能识别是否能在多线程之间共享,比如一个包含raw 指针的结构体,我们需要用unsafe impl关键字实现unsafe trait。

unsafe trait Foo {
    // methods go here
}

unsafe impl Foo for i32 {
    // method implementations go here
}

type alias

我们可以为某些类型创建别名,这在类型名冗长的时候非常有用。

type int = i32;
type Rs<T> = Result<T, std::io::Error>;

我们很容易表示函数类型,它们是一种具体类型,它的大小是在编译期可以确定的,因此我们可以作为参数、返回值、成员使用。函数本身就是一种不可变指针,因此可以在不涉及所有权的情况下自由传递。

fn apply(f: fn(i32) -> i32, x: i32) -> i32 {
    f(x)
}

fn add(x: i32) -> i32 {
    x + 1
}

fn main(){
    let ans = apply(add, 3);
    println!("ans = {}", ans);
}

要查看宏展开后的结果,需要先安装cargo-expand

# cargo install cargo-expand //安装工具
# cargo expand // 查看展开结果

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK