Rust 入门笔记【四】

使用use关键字将路径引入作用域

mod A {
    pub mod B {
        pub fn f() {
            println!("Hello, world!");
        }
    }
}
use crate::A::B;
mod X {
    fn g() {
        B::f();
    }
}

use只适用于所在的作用域,而rust中模块是作用域层级的唯一管理者,并且默认情况子模块并不会继承父模块中的声明。所以上述的代码是无法通过编译的。解决方法有两种,第一种是显示的应用父模块的声明。

mod A {
    pub mod B {
        pub fn f() {
            println!("Hello, world!");
        }
    }
}
use crate::A::B;
mod X {
    fn g() {
        super::B::f();
    }
}

第二种方法是在模块内进行声明。

mod A {
    pub mod B {
        pub fn f() {
            println!("Hello, world!");
        }
    }
}
mod X {
    use crate::A::B;
    fn g() {
        B::f();
    }
}

use一般不会引入具体的方法或者类,而是引入到模块,这样是为了避免变量冲突。

mod A {
    pub mod B {
        pub fn f() {
            println!("Hello, world!");
        }
    }
}

fn fun() {
    use A::B::f;
    f();
}

这样是可以正常调用的。但是如果下面这种情况就会冲突。

mod A {
    pub mod B {
        pub fn f() {
            println!("Hello, world!");
        }
    }
}
mod X {
    pub mod Y {
        pub fn f() {
            println!("Hello, world!");
        }
    }
}

fn fun() {
    use A::B::f;
    use X::Y::f;
    f();
}

解决方法有两种,第一种引入到模块,然后用从模块访问。

fn fun() {
    use A::B;
    use X::Y;
    B::f();
    Y::f();
}

第二种方法是as关键字起别名。

fn fun() {
    use A::B::f as Bf;
    use X::Y::f as Yf;
    Bf();
    Yf();
}

重导出

使用use导入的关键字只能在当前作用域种使用。如果希望父作用域中也可能访问到子作用域中导入的声明可以用重导出。

mod A {
    pub mod B {
        pub fn f() {
            println!("Hello, world!");
        }
    }
}
mod X {
    pub mod Y {
        pub use crate::A::B::f;
    }
    fn g() {
        Y::f();
    }
}

这样对于父类来说,函数f就好像是定义在模块Y中一样。

嵌套路径

pub mod test_mod {
    pub mod x {
        pub fn f() {
            println!("this is x::f");
        }
    }
    pub mod y {
        pub fn f() {
            println!("this is y::f");
        }
    }
    pub mod z {
        pub fn f() {
            println!("this is z::f");
        }
    }
}

fn main() {
    use test_mod::x;
    use test_mod::y;
    use test_mod::z;

    x::f();
    y::f();
    z::f();
}

看这个例子中,use其实是非常冗余的,但是为了简化,可以嵌套路径。

fn main() {
    use test_mod::{x, y, z};
    x::f();
    y::f();
    z::f();
}

如果想要同时引入当前模块的话,可以用self代指当前模块

pub mod test_mod {
    pub fn f(){
        println!("this is test_mod::f");
    }
    pub mod x {
        pub fn f() {
            println!("this is x::f");
        }
    }
    pub mod y {
        pub fn f() {
            println!("this is y::f");
        }
    }
    pub mod z {
        pub fn f() {
            println!("this is z::f");
        }
    }
}

fn main() {
    use test_mod::{self,x, y, z};
    test_mod::f();
    x::f();
    y::f();
    z::f();
}

如果希望将一个路径下的所有公有项全部引入作用域,可以用*glob运算符。

fn main() {
    use test_mod::*;
    f();
    x::f();
    y::f();
    z::f();
}

利用模块拆分文件

│─main.rs
│─test_mod.rs
│
└─test_mod
  └─────x.rs
  └─────y.rs
  └─────z.rs

可以根据模块把上述的代码划分成如上多个文件。在新版本中已经舍弃了test_mod/mod.rstest_mod/x/mod.rs这种声明方法,因为这样如果打开多个模块就会难以分辨。

vector

声明vector

fn main() {
    let v1: Vec<i32> = Vec::new();
    let v2 = vec![1, 2, 3]; // 通过宏自动推导类型
}

更新vector

fn main() {
    let mut v = Vec::new();
    v.push(1);
    v.push(2);
    v.push(3);
}

因为这里有push,并且push的都是i32类型,所以编译可以自动推导类型。

如果要删除最后一个元素可以pop

读取vector元素

fn main() {
    let v = vec![1, 2, 3, 4, 5];
    let first = v[0];
    let second = v[1];
    let third = &v[2];
    let fourth = v.get(3);;
    println!("first {}", first);
    println!("second {}", second);
    println!("third {}", third);    
}

上述例子中first,second的类型是i32third类型是&i32fourth类型是Option<&i32>。因此fourth是不能直接输出的。

看起来get很麻烦,但是如果是下面的情况。

fn main() {
    let v = vec![1, 2, 3, 4, 5];
    // let val = v[100];
    let val = v.get(100);
}

如果是[]就会直接运行是错误,如果是get则会得到None

rust中如果获得了一个有效引用,编译器就会检查所有权和借用规则。

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];
    let v0 = &v[0];
    v.push(6);
    println!("{:?}", v0);
}

这个例子就无法通过编译,因为我们已经获得了一个不可变了引用。我们获得的是第一个元素的引用,为怎么在末尾修改也不行?因为在末尾新增元素可能会触发扩容,如果当前位置内存不够就会发生移动,此时v0就指向了一个被释放的内存。

遍历vector

如果只遍历

fn main() {
    let v = vec![1, 2, 3, 4, 5];
    for i in &v {
        println!("{}", i);
    }
}

如果要修改

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];
    for i in &mut v {
        *i += 1;
        println!("{}", i);
    }
}

*是解引用运算符,因为i实际上是一个&mut i32是引用,不是值。

通过枚举类型存储不同的类型

vector要求存储的值类型必须相同,如果确实需要不同类型,可以使用枚举类型。

fn main() {

    enum Data{
        int(i32),
        long(i64),
        double(f64),
        text(String),
    }
    
    let v = vec![
        Data::int(1), 
        Data::long(123465790),
        Data::double(3.0),
        Data::text(String::from("Hello, world!")),
    ];
    
    for i in &v{
        match i {
            Data::int(i) => println!("{}", i),
            Data::long(i) => println!("{}", i),
            Data::double(i) => println!("{}", i),
            Data::text(i) => println!("{}", i),
        }
    }
}

字符串

rust核心语言中只有一种字符串类型,字符串slice,通常以借用的&str形式出现。字符串slice是对存储在别处的UTF-8编码数据的引用。比如字符串字面值就是存储在编译的二进制输出种。

字符串String,是标准库提供的,可增长、可变、可拥有。

定义 String

fn main() {
    let s1 = String::new();
    let s2 = String::from("Hello, world");
    let s3 = "Hello, world!".to_string();
}

更新 String

fn main() {
    let mut s = String::new();
    s.push_str("Hello,");
    let s2 = "World";
    s.push_str(s2);
    println!("s is {}", s);
    println!("s2 is {}", s2);
}

push_str接受一个&str,并拼接到当前String后面,因为是引用,所有拼接后不会对s2变量产生影响。

fn main() {
    let mut s = String::from("lo");
    s.push('l');
    println!("{}", s);
}

push是拼接一个字符char,并且因为是char所以也不会造成所有权的移动。

如果想要拼接两个String可以用

fn main() {
    let s1 = String::from("hello ");
    let s2 = String::from("world!");
    let s3 = s1 + &s2;
    // 此时 s1 无效,s2, s3 有效
    println!("{}", s3);
}

这里+实际上用到add函数,add函数签名如下

fn add(self, s : &str) -> String {}

也就是说,add函数获得了s1的所有权,并返回了s1的所有权,然后又通过赋值导致所有权给了s3

注意这里第二个参数的类型应该是&str但是实参是&String,这里发生了类型不匹配的情况,这里使用的了Deref强制转换。

如果要链接多个字符串,可以用format!宏。

fn main() {
    let s1 = String::from("A");
    let s2 = String::from("B");
    let s3 = String::from("C");
    let s = format!("{s1}-{s2}-{s3}");
    println!("{}", s);
}

索引String

rust 的 String 不支持索引。因为String 本质是基于Vec<u8>的封装,但在UTF-8中,并不是所有的字符都只占用一个字节。这样的话,如果想要索引一个完整的字符就必须要从前向后遍历,这样的话索引的常数就不能保证是O(1),因此rust拒绝了String的索引。

当然了如果确实需要索引字符,可以使用slice实现。

fn main() {
    let hello = "Здравствуйте";
    let s = &hello[0..4];
    println!("{}", s);
}

西里尔字母一个字母占用两个字节,slice 是按照字节进行切片,上述的代码实际上是截取了前两个字符。

let s = &hello[0..1];

并且如何这样截取,会运行时错误,rust会检查截取是否是完整的字符。

遍历字符串

遍历字符串有两种方法,一种是按照字符。

fn main() {
    let hello = "Здравствуйте, HELLO, 你好";
    for c in  hello.chars() {
        println!("{}", c);
    }
}

另一种策略就是按照字节遍历

fn main() {
    let hello = "Здравствуйте, HELLO, 你好";
    for c in  hello.bytes() {
        println!("{}", c);
    }
}

Hash Map

创建 Hash Map

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("apple"), 10);
    scores.insert(String::from("banana"), 30);
}

访问 Hash Map

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("apple"), 10);
    scores.insert(String::from("banana"), 30);

    let fruit = String::from("applee");
    let scores = scores.get(&fruit).copied().unwrap_or(-1);
    println!("{} is {}", fruit, scores);
}

这里的get的返回值是Option<&i32>,使用copied的作用是把他转变成Option<i32>,这个copied只能适用于栈内存上的,如果是堆内存上的需要用cloned

对于一个Option<T>的值,可以用unwrap_or函数返回T,并且如果是None就会返回实参。

遍历 Hash Map

use std::collections::HashMap;

fn main() {

    let mut scores: HashMap<String, i32> = HashMap::new();
    scores.insert(String::from("apple"), 10);
    scores.insert(String::from("banana"), 30);
    scores.insert(String::from("banana"), 35);

    for (key, value) in &scores {
        println!("{}: {}", key, value);
    }
}

因为是哈希表,所以key不会重复,且key是乱序的。并且如果一个key被多次插入,value记录的是最后一次插入的值。

Hash Map 所有权

对于i32这种类型,插入是直接把值拷贝进哈希表,但是如果是String则所有权就会被哈希表所拥有。

如果插入的是引用,则必须保证在哈希表有效是,引用也是有效的。

检测 Hash Map

如果想要检测一个key是否在哈希表中出现可以用entry函数。

use std::collections::HashMap;

fn main() {
    let mut scores: HashMap<i32, i32> = HashMap::new();

    scores.entry(25).or_insert(52);
    scores.entry(36).or_insert(63);
    scores.entry(25).or_insert(45);
    for (key, val) in scores {
        println!("{}: {}", key, val);
    }
}

实际上entry是查找给定的键,or_insert是当键是None时插入。

根据旧值更新

rust 的变量默认时不可变,如果需要修改变量需要使用mut显式的定义。但是这个对于结构体的成员变量是不行的。结构体的成员变量是否可以被修改依赖结构体的定义规则。

比如要用哈希统计一个文本中单词出行的次数。

use std::collections::HashMap;

fn main() {
    let text = "aaa bbb aaa bbb cc dd ddd dd";

    let mut map = HashMap::new();
    for word in text.split_whitespace() {
        let entry = map.entry(word).or_insert(0);
        *entry += 1;
    }
    println!("{:?}", map);
}

panic

panic是rust处理不可恢复错误的机制,其核心机制是当程序遇到无法合适处理的错误时,立刻结束终止当前线程的执行,并默认触发栈展开开始调用栈上对象的修改函数,释放资源,最终安全终止线程。

panic触发有两种情况。一种是遇到了错误,比如

fn main() {
    let v = vec![1, 2, 3];
    v[11];
}

比如这样的一段代码,这种代码如果在C++是并不会导致线程结束。但是这样的不安全操作可能会导致一些严重的错误。因此在rust中,这个错误就会直接触发panic。

thread 'main' panicked at src\main.rs:3:6:
index out of bounds: the len is 3 but the index is 11

还有一种方法是利用panic!显式的手动触发。

thread 'main' panicked at src\main.rs:2:5:
crash and burn
posted @ 2025-05-05 17:45  PHarr  阅读(44)  评论(0)    收藏  举报