模块
rust 模块分为:
package- 至少包含一个
cargo.toml文件 - 默认情况下,
src/main.rs是package的入口
- 至少包含一个
crate- 在创建项目时
cargo new --lib,src/lib.rs是crate的入口 - 是
rust最小编译单元
- 在创建项目时
module- 使用
mod关键字定义
- 使用
如果这个模块需要被外部访问,需要使用 pub 关键字修饰
mod foo {
pub mod bar {
pub fn baz() -> String {
"Hello, world!".to_string()
}
}
}
fn main() {
println!("{}", foo::bar::baz());
}
可见性
rust 中模块内部的代码,结构体,函数等类型默认是私有的,通过 pub 关键字可以将其公开
pub:成员对模块可见pub(self):成员对模块内的子模块可见mod foo { pub(self) const NUMBER: i32 = 10; pub mod bar { pub fn get_number() -> i32 { super::NUMBER } } } fn main() { println!("{}", foo::bar::get_number()); }pub(crate):成员对整个crate可见mod foo { pub(crate) const NUMBER: u32 = 10; } fn main() { println!("{}", foo::NUMBER); }
super 和 self
super 和 self 关键字用于访问父模块和当前模块的变量
fn function() {
println!("function");
}
mod mod1 {
pub fn function() {
println!("mod1::function");
super::function();
}
pub mod mod2 {
pub fn function() {
println!("mod1::mod2::function");
super::super::function();
}
pub fn function2() {
self::function();
}
}
}
泛型
PartialOrd 是一个部分顺序比较,对应的还有一个 Ord 是全序关系,区别就是 PartialOrd 有些值之间的大小关系是未定义的,而 Ord 是所有值之间都有大小关系
fn largest<T: std::cmp::PartialOrd>(a: T, b: T) -> T {
if a > b {
a
} else {
b
}
}
fn main() {
let a = largest(1, 2);
println!("The largest number is {}", a);
}
泛型约束
泛型约束是 Clone + PartialOrd,因为在返回的时候需要返回 x 或 y 的副本
因为直接返回 x 或 y 会导致所有权转移,所以我们就需要对泛型约束,需要那个类型实现 Clone 和 PartialOrd 这两个 trait
而类型 i32 是实现了 Clone 和 PartialOrd 这两个 trait 的,所以就可以使用了
use std::cmp::PartialOrd;
struct Point<T> {
x: T,
y: T,
}
impl<T: Clone + PartialOrd> Point<T> {
fn largest(&self) -> T {
if self.x > self.y {
self.x.clone()
} else {
self.y.clone()
}
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("The largest number is {}", p.largest());
}
trait
trait 是一种定义共享行为的方法,类似于其他语言的接口
下面代码 impl<T: Display> Display for Point<T> 的意思是:
Display是一个traitfor Point<T>是为Point<T>实现Display
方法 fmt 签名是 fn fmt(&self, f: &mut Formatter<'_>) -> Result,这个方法是 Display 的一个方法,用于格式化输出,它的第二个参数 f 的意思是,作为一个"输出目标"或"写入器",输出的内容需要写到 f 中
use std::fmt::{Display, Formatter, Result};
struct Point<T> {
x: T,
y: T,
}
impl<T: Display> Display for Point<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(f, "Point {{ x: {}, y: {} }}", self.x, self.y)
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("{}", p);
}
trait 实现
show 函数需要接收一个实现了 Display trait 的类型,所以它可以接收 Point 类型
use std::fmt::{Display, Formatter, Result};
struct Point<T> {
x: T,
y: T,
}
impl<T: Display> Display for Point<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(f, "Point {{ x: {}, y: {} }}", self.x, self.y)
}
}
fn show<T: Display>(p: T) {
println!("show {}", p);
}
fn main() {
let p = Point { x: 5, y: 10 };
show(p);
}
fn show<T: Display>(p: T) {} 函数可以换成下面这种语法糖
fn show(p: impl Display) {
println!("show {}", p);
}
自动派生
自动派生是 rust 编译器自动为我们的结构体实现的一些 traits,比如 Debug,PartialEq,Default 等
比如下面代码中打印 p 的时候,rust 会报错:Point doesn't implement Debug
这是因为 Point 结构体没有实现 Debug 这个 trait,所以我们需要手动实现
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("{}", p);
}
Debug 这个 trait 使用的占位符是 {:?},下面代码就能正常打印 p 了
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("{:?}", p);
}
还有 PartialEq 这个 trait,用于比较两个值是否相等,rust 也可以自动为我们实现
在 Point 上加上 PartialEq 这个 trait,两个实例就可以直接比较大小了,这种本来你需要手动实现 p1.x == p2.x && p1.y == p2.y 的操作
#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 5, y: 10 };
let p2 = Point { x: 5, y: 10 };
println!("{:?}", p1 == p2);
}
还有 Default 这个 trait,用于给结构体一个默认值,rust 也自动为我们实现了
Point::default() 会为我们自动添加上对应类型的默认值
#[derive(Debug, Default)]
struct Point {
x: i32,
y: i32,
z: f64,
}
fn main() {
let p = Point::default();
println!("{:?}", p); // Point { x: 0, y: 0, z: 0.0 }
}
所有权
所有权的规则:
rust中每个值都绑定一个变量,称为该值的所有者- 每个值只有一个所有者,而且每个值都有它的作用域
- 一旦当这个值离开作用域,这个值占用的内容将被回收
下面代码,a 的所有权会被转移给 b,所以 a 不能再使用了
fn main() {
let a = String::from("Hello");
let b = a;
println!("a: {}, b: {}", a, b);
}
下面代码,a 的所有权不会被转移给 b,所以 a 还可以继续使用
fn main() {
let a = "Hello";
let b = a;
println!("a: {}, b: {}", a, b);
}
因为 String 是在堆上分配的,而字符串字面量是存储在程序的只读数据段中的,a 和 b 实际上是对这个固定数据的引用,这种引用实现了 Copy trait,意味着赋值时会进行复制而不是移动
借用
借用是指在不转移所有权的情况下,允许对值进行访问
fn main() {
let a = String::from("Hello");
let b = &a;
println!("a: {}, b: {}", a, b);
}
引用分为可变引用和不可变引用,不可变引用可以有多个,但是可变引用只能有一个
fn main() {
let mut a = String::from("Hello");
let b = &a;
let c = &a;
let d = &mut a;
println!("a: {}, b: {}, c: {}", a, b, c);
}
不可变引用
let a = String::from("Hello");
let b = &a;
let c = &a;
let d = &a;
可变引用
let mut a = String::from("Hello");
let b = &mut a;
let c = &mut a; // error
下面两段代码中第二段代码会报错,因为 b 的不可变引用和持续到了 println! 中,所以出现了报错
fn main() {
let mut a = String::from("Hello");
let b = &a;
let c = &mut a;
}
fn main() {
let mut a = String::from("Hello");
let b = &a;
let c = &mut a; // error
println!("a: {}, b: {}, c: {}", a, b, c);
}
生命周期注解
在大多数情况下 rust 编译器可以自动推到变量的生命周期,但是有时候编译器无法推到,就需要我们手动指定
比较常见的场景是与 &str 交互时
比如下面的代码,在 rust 中是编译不通过的,因为字符串 a 有自己的生命周期,而 b 也有自己的生命周期,rust 编译器无法推到返回的字符串的生命周期,所以编译就报错了
fn bigger(a: &str, b: &str) -> &str {
if a > b {
a
} else {
b
}
}
fn main() {
println!("bigger: {}", bigger("a", "b"));
}
为了让编译通过,就需要手动指定生命周期,'a 是一个生命周期注解,表示返回的字符串的生命周期与参数 a 的生命周期一样
fn bigger<'a>(a: &'a str, b: &'a str) -> &'a str {
if a > b {
a
} else {
b
}
}
fn main() {
println!("bigger: {}", bigger("a", "b"));
}
要知道的是生命周期注解不会改变变量本身的生命周期,只是告诉编译器如何推到返回值的生命周期
这里为什么要加生命周期注解呢?因为 name 是一个引用,同时 Person 的实例化也是一个引用,所以 name 的生命周期必须与 Person 的生命周期一样
#[derive(Debug)]
struct Person<'a> {
name: &'a str,
}
fn main() {
let p = Person { name: "Peter" };
println!("Name: {:?}", p);
}
错误
在 rust 中错误是可恢复的,异常是不可恢复的
不可恢复的错误有:
panic!("error")assert!(1 == 2)assert_eq!(1, 2)unimplemented!()未实现的函数unreachable!()不可能到达的代码
可恢复的错误:是一个泛型枚举 Result,有两个值 Ok 和 Err
fn main() {
let a: Result<u32, &'static str> = Result::Ok(1);
println!("{:?}", a); // Ok(1)
let b: Result<u32, &'static str> = Result::Err("result error");
println!("{:?}", b); // Err("result error")
}
例如在读取文件时,如果文件不存在,就会进入 Err 分支,如果文件存在,就会进入 Ok 分支
fn main() {
let r = std::fs::read("src/main.rs");
match r {
Ok(data) => println!("{:?}", std::str::from_utf8(&data).unwrap()),
Err(e) => println!("Error: {:?}", e),
}
}
错误传递
有时候在捕获到错误时,是希望继续向上传递的,而不是直接崩溃,在 rust 中使用问号表达式是可以做到这一点的
但是问号表达式有一个要求:需要调用函数的错误类型与当前的错误类型相同时,才可以使用
fn bar() -> Result<u32, &'static str> {
// Ok(42)
Err("error msg")
}
fn foo() -> Result<i32, &'static str> {
let a = bar()?;
Ok(a as i32)
}
fn main() {
match foo() {
Ok(v) => println!("value: {}", v),
Err(e) => println!("error: {}", e),
}
}
将其他的错误包装成自己定义的错误
#[derive(Debug)]
pub enum Error {
IO(std::io::ErrorKind),
}
impl From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self {
Error::IO(error.kind())
}
}
fn do_read_file() -> Result<(), Error> {
let _file = std::fs::read("src/main2.rs")?;
let data_str = std::str::from_utf8(&_file).unwrap();
println!("{}", data_str);
Ok(())
}
fn main() {
match do_read_file() {
Ok(_) => println!("File read successfully"),
Err(e) => println!("Error reading file: {:?}", e),
}
}
最后
main 函数可以有返回值
fn main() -> Result<(), Error> {
do_read_file()?;
Ok(())
}
问号表达式可以一路写下去,直到最后,如果其中一个报错,会终止这个函数
fn main() -> Result<(), Error> {
do_read_file()?;
do_read_file()?;
do_read_file()?;
Ok(())
}