Rust学习笔记
Rust是Mozilla主导的编译型通用编程语言,它的特点包括:
- 内存高效,不包含运行时和垃圾回收器(对比Go语言)
- 性能接近于C++开发的应用程序
- 可靠性,丰富的内存系统、所有权模型保证内存安全、线程安全
基于rustup是官方推荐的安装方式,执行下面的命令即可:
1 |
curl https://sh.rustup.rs -sSf | sh |
你需要根据向导提示完成Rust编译器、Rust包管理器(Cargo)的安装。 默认情况下,所有工具被安装到~/.cargo/bin目录中。
使用fn关键字可以声明一个函数,名为main的函数可以作为入口点:
1 2 3 4 |
fn main() { // 调用宏需要用! println!("Hello, Rust!"); } |
调用下面的命令可以构建二进制文件:
1 |
rustc main.rs |
默认的编译结果位于当前目录,和源文件名字一致,可以直接执行。
Cargo是Rust的构建系统和包管理器,通过它可以创建Rust项目、构建项目、下载并编译依赖库。
调用下面的命令创建新的Cargo项目:
1 |
cargo new hello_cargo |
项目的布局如下:
1 2 3 4 5 6 7 8 |
├── Cargo.lock ├── Cargo.toml # Cargo项目元数据文件 ├── src # 源码目录 │ └── main.rs ├── .gitignore # 自动创建此文件 ├── .git # 自动创建Git仓库 └── target # 存放构建结果 └── debug |
此文件格式为 TOML (Tom's Obvious, Minimal Language) ,声明Cargo项目的元数据、依赖:
1 2 3 4 5 6 7 |
[package] name = "hello_cargo" version = "0.1.0" authors = ["Alex Wong <alex@gmem.cc>"] edition = "2018" [dependencies] |
第二个段dependencies用于声明依赖,在Rust中代码包被称为crate,此项目没有依赖,因此dependencies段中没有crate。
1 2 3 4 5 |
# 默认构建出调试版本:./target/debug/hello_cargo cargo build # 构建发布版本,耗时更多,代码更加优化 cargo build --release |
执行下面的命令构建并运行:
1 |
cargo run |
执行下面的命令可以快速的检查代码是否通过编译:
1 |
cargo check |
Rust的数据类型分为两大类:标量(scalar)和复合(compound)。
标量类型包括:
标量类型 | 说明 | ||
整型 |
8-bit整数:
i8(有符号)
u8(无符号) 直接量语法:
关于整型溢出:u8可以存放的最大数为255,如果为其赋值256:
|
||
浮点型 | Rust支持两种精度的浮点数 f32 、 f64。 | ||
布尔型 | bool, true或者 false | ||
字符类型 | char,使用单引号包围,代表了一个Unicode标量值,任何Unicode字符均可以表示 |
复合类型包括:
复合类型 | 说明 | ||||||
元组类型 | 将多个其它类型组合在一起:
要获取元组的单个元素,可以进行解构(destructure)操作:
或者,使用索引值来访问元素:
|
||||||
数组类型 | 类似于元组,但是每个元素的类型都必须相同:
数组的类型名语法很特殊: [type; number],例如:
数组元素的个数不可变化。访问数组元素的语法不同于元组:
|
默认情况下,Rust将 prelude模块中定义的类型自动引入到所有程序的作用域中。不再prelude的类型,可以使用use语句显式引入作用域:
1 2 3 4 5 |
use std::io; io::stdin().read_line(&mut guess).expect("Failed to read line"); // 如果不用use,也可以使用全限定名称引用: std::io::stdin().read_line(...); |
此关键字用于定义一个可变变量 :
1 2 |
// 不需要声明变量类型 let mut guess = String::new(); |
此关键字用于定一个不可变变量。
1 |
let foo = bar; |
可以用于基于索引来访问元组的元素:
1 |
let five_hundred = x.0; |
以及用来访问结构的方法、结构的字段。
可以用于显式的声明变量类型:
1 |
let f: bool = false; |
此操作符用于访问类型的静态成员:
1 |
let mut guess = String::new(); // 关联函数,也叫静态方法 |
很多类型上都有名为new的关联函数,这种命名是Rust的一种惯用方式,用于创建类型的实例。
此关键字用于获得变量的引用:
1 |
io::stdin().read_line(&mut guess); |
和变量类似, &mut guess表示可变引用, &guess则表示不可变引用。
用于调用宏:
1 |
println!("You guessed: {}", guess); |
声明数组类型、数组直接量、访问数组元素时使用该关键字。
声明元组类型、数组直接量时使用该关键字。
声明函数形参、调用函数时,使用该关键字。
单行注释。
用于声明注解。
关键字 fn用于定义一个新函数。在Rust中函数、变量名都使用snake case风格 —— 全部小写、使用下划线分隔单词。
函数声明示例:
1 2 3 |
fn another_function(x: i32) { println!("The value of x is: {}", x); } |
带有返回值的函数声明示例:
1 2 3 |
fn five() -> i32 { 5 } |
使用 return 关键字和指定值,可从函数中提前返回;但大部分函数隐式的返回最后的表达式 。
要想返回多个值,可以使用元组:
1 2 3 4 |
fn calculate_length(s: String) -> (String, usize) { let length = s.len(); (s, length) } |
不需要括号,类似于Go语言:
1 2 3 4 5 6 7 |
if number % 4 == 0 { println!("number is divisible by 4"); } else if number % 3 == 0 { println!("number is divisible by 3"); } else { println!("number is not divisible by 4, 3, or 2"); } |
if语句可以用在let语句中:
1 2 3 4 5 6 |
let condition = true; let number = if condition { 5 } else { 6 }; |
重复执行直到break为止:
1 2 3 4 5 6 7 8 9 |
let mut counter = 0; let result = loop { counter += 1; if counter == 10 { break counter * 2; } }; |
可以看到,loop语句和if类似,可以用作let表达式的右值。
1 2 3 4 5 6 7 |
let mut number = 3; while number != 0 { println!("{}!", number); number = number - 1; } |
可以用来方便的迭代集合元素:
1 2 3 4 5 |
let a = [10, 20, 30, 40, 50]; for element in a.iter() { println!("the value is: {}", element); } |
所有权(ownership)系统是 Rust 最独特的功能,其令 Rust 无需垃圾回收(garbage collector)即可保障内存安全,同时不需要容易出错的手工内存管理。
对于每个Rust中的值,以下规则使用:
- Rust 中的每一个值都有一个被称为其 所有者(owner)的变量
- 值有且只有一个所有者
- 当所有者(变量)离开作用域,这个值将被丢弃
以String类型为例说明,和字符串直接量不同,它可以变化:
1 2 |
let mut s = String::from("hello"); s.push_str(", world!"); // 追加字符串 |
字符串直接量不可改变,在编译期长度可知, 因而可以在栈上分配,随着栈帧的销毁自动回收内存。
为了支持运行时动态改变,String类型是在堆上分配(编译时未知长度的内存空间),并在不需要String变量后将内存归还给OS。传统编程语言,要么手工释放内存,要么使用垃圾回收器,Rust使用了不同的方式处理内存释放:
1 2 3 4 5 |
{ let s = String::from("hello"); // 从此处起,s 是有效的 // 使用 s } // 此作用域已结束, // s 不再有效 |
当变量离开作用域后,Rust自动调用一个特殊的 drop函数,完成内存的释放。
变量的所有权总是遵循相同的模式:
- 将值赋给另一个变量时移动它
- 当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有
考虑下面的语句:
1 2 3 4 |
let s1 = String::from("hello"); let s2 = s1; // 移动语义 println!("{}, world!", s1); // Error |
将s1“赋值”(移动)给s2后,Rust认为s1不再有效,因此第三个语句会报错:value used here after move。这和其它语言非常的不同。
由于s1已经无效,Rust就不会尝试调用 drop函数,也就不会遇到二次释放(double free)问题。
一个设计选择是:Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何自动的复制可以被认为对运行时性能影响较小。
Rust包含一个叫Copy trait的特殊注解,可以应用在类似整型这样的存储在栈上的类型上。如果一个类型拥有 Copy trait,一个旧的变量在将其赋值给其他变量后仍然可用——也就是说这种类型的变量的赋值操作具有复制语义,这相当于对上节的移动语义做了例外:
1 2 3 4 |
let x = 5; let y = x; // 复制语义 println!("x = {}, y = {}", x, y); // OK |
Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait。
使用复制语义的类型包括:
- 整数
- 布尔
- 浮点数
- 字符
- 元组(所有元素均支持复制语义的话)
如果的确需要实现类似于C++/Go语言的赋值语义,可以使用通用的 clone函数:
1 2 3 4 |
let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {}, s2 = {}", s1, s2); |
将值传递给函数时的移动、赋值语义,和变量赋值时类似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
fn main() { let s = String::from("hello"); // s 进入作用域 takes_ownership(s); // s 的值移动到函数里 ... // ... 所以到这里不再有效 let x = 5; // x 进入作用域 makes_copy(x); // x 应该移动函数里, // 但 i32 是 Copy 的,所以在后面可继续使用 x } // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走, // 所以不会有特殊操作 fn takes_ownership(some_string: String) { // some_string 进入作用域 println!("{}", some_string); } // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放 fn makes_copy(some_integer: i32) { // some_integer 进入作用域 println!("{}", some_integer); } // 这里,some_integer 移出作用域。不会有特殊操作 |
可以注意到,在同一作用域内变量的销毁顺序,和它的声明顺序相反。
和传参类似,返回值也牵涉到所有权的转移:
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 |
fn main() { let s1 = gives_ownership(); // gives_ownership 将返回值 // 移给 s1 let s2 = String::from("hello"); // s2 进入作用域 let s3 = takes_and_gives_back(s2); // s2 被移动到 // takes_and_gives_back 中, // 它也将返回值移给 s3 } // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走, // 所以什么也不会发生。s1 移出作用域并被丢弃 fn gives_ownership() -> String { // gives_ownership 将返回值移动给 // 调用它的函数 let some_string = String::from("hello"); // some_string 进入作用域. some_string // 返回 some_string 并移出给调用的函数 } // takes_and_gives_back 将传入字符串并返回该值 fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域 a_string // 返回 a_string 并移出给调用的函数 } |
一个变量传递给函数后,在调用者处就无法使用,除非函数再次将其返回:
1 2 3 4 5 6 7 8 9 10 |
fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); println!("The length of '{}' is {}.", s2, len); // 调用函数后仍然需要使用s1指向的字符串 } fn calculate_length(s: String) -> (String, usize) { let length = s.len(); (s, length) // 为了满足仍然需要使用的需求,需要将s的所有权再次转移回去 } |
这种方式很罗嗦,因此Rust提供引用(references)功能来简化之:
1 2 3 4 |
// 以String的引用为形参,不获得入参的所有权 fn calculate_length(s: &String) -> usize { s.len() } |
引用允许你使用值但不获取其所有权。 没有所有权,则离开作用域时就不会调用drop进行清理。
Rust中将获取引用作为函数参数称为 借用(borrowing)。 默认情况下,借用的变量是不支持修改的,除非使用mut关键字:
1 2 3 4 |
// 可变引用 fn change(some_string: &mut String) { some_string.push_str(", world"); } |
可变引用有一个很大的限制:在特定作用域中的特定数据有且只有一个可变引用。这些代码会失败:
1 2 3 4 |
let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; // 错误 |
同一作用域中,同时使用可变、不可变引用也不支持:
1 2 3 |
let r1 = &s; let r2 = &s; // 多个不可变引用是允许的 let r3 = &mut s; // 混合使用可变、不可变引用不允许 |
再不同作用域中,使用两个可变引用则没有问题:
1 2 3 4 |
{ let r1 = &mut s; } // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用 let r2 = &mut s; |
在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针(dangling pointer)—— 其指向的内存可能已经被分配给其它持有者。
在Rust中,编译期检查可以避免悬垂引用:
1 2 3 4 5 |
fn dangle() -> &String { // dangle 返回一个字符串的引用 let s = String::from("hello"); // s 是一个新字符串 &s // 返回字符串 s 的引用 } // 这里 s 离开作用域并被丢弃。其内存被释放 // 危险! |
这个例子中s是在函数体内创建,dangle具有它的所有权,返回&s仅仅是借用,而不获得s的所有权,这导致dangle函数结束时s被销毁,返回的是悬垂引用。编译器会识别这个错误。
解决这个错误的方法很简单,那就是返回s而非它的引用,转移掉所有权:
1 2 3 4 |
fn no_dangle() -> String { let s = String::from("hello"); s } |
另一个没有所有权的数据类型是 slice。slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
let s = String::from("hello world"); // 切片语法,不包含结束索引 let hello = &s[0..5]; let world = &s[6..11]; // 包含结束索引 let hello = &s[0..=4]; let world = &s[6..=10]; // 从0开始 let slice = &s[..2]; // 直到结尾 let slice = &s[3..]; // 整个字符串 let slice = &s[..]; |
切片的本质是引用,对字符串的一部分的引用。
结构是一种自定义数据类型,和元组不同,结构的每个部分——字段——都具有名字:
1 2 3 4 5 6 |
struct User { username: String, email: String, sign_in_count: u64, active: bool, } |
创建结构实例的语法如下:
1 2 3 4 |
let mut alex = User { name: "Alex Wong".to_string(), age: 32, }; |
要访问结构字段,可以使用点号:
1 |
alex.age = 33; |
1 2 3 4 5 6 7 8 9 10 11 12 |
let mut alex = User { first_name: "Alex".to_string(), last_name: String::from("Wong"), age: 32, }; let mut bo = User { first_name: "ChangBo".to_string(), age: 3, // 其它字段从alex这个实例复制 ..alex }; |
元组结构体类似于元组,但是限定每个成员的类型,类似于结构,但是不指定成员的名称:
1 2 3 4 5 |
struct Color(i32, i32, i32); struct Point(i32, i32, i32); let black = Color(0, 0, 0); let origin = Point(0, 0, 0); |
类似于元组,元组结构体支持解构操作、点号+索引访问成员。
即unit-like structs,这是没有任何字段的结构体。它们类似于 ()即unit类型。当你需要为某个类型实现trait但却不需要在类型中存储任何数据时,可以使用类单元结构体。
结构体的字段,如果是非引用类型,则结构体本身对其拥有所有权。
如果字段是引用类型,则需要使用生命周期(lifetimes)这一特性,确保结构体引用数据的有效性和结构体本身一致。
Rust支持为结构体添加行为,这种行为叫做方法,即位于结构体(以及下文讨论的枚举、Trait)的上下文中定义的函数。方法的第一个参数可以是:
- self,指向结构体实例
- &self,执行结构体实例的引用
方法语法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 这是一个注解,表示为下面的结构体保留调试信息 #[derive(Debug)] struct Rectangle { width: u32, height: u32, } // 为结构体添加方法 impl Rectangle { // 第一个入参是结构的引用 fn area(&self) -> u32 { self.width * self.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50 }; println!( "The area of the rectangle is {} square pixels.", // 使用点号语法访问结构方法 rect1.area() ); } |
在调用方法时,Rust会自动进行获得引用、解引用操作,而不需要C++那样手工处理。
也就是说,当调用 object.something()时,Rust会按照需要添加 &、 &mut或者 *以匹配方法签名。
除了方法,你还可以在 Impl块中定义不以self作为入参的函数,这种函数即关联(和结构相关,但是非实例方法)函数(associated functions),类似于C++中的静态函数。
在Rust中通常使用关联函数实现返回结构实例的新实例的工厂函数:
1 2 3 4 5 6 |
impl Rectangle { // 关联函数,第一个参数不是self fn square(size: u32) -> Rectangle { Rectangle { width: size, height: size } } } |
调用关联函数时,需要使用结构体名::的语法形式: let sq = Rectangle::square(3);
定义枚举:
1 2 3 4 |
enum IpAddrKind { V4, V6, } |
创建枚举实例:
1 2 |
let four = IpAddrKind::V4; let six = IpAddrKind::V6; |
使用枚举,和普通类型一样:
1 2 3 4 5 6 7 8 9 10 11 |
fn route(ip_type: IpAddrKind) { } struct IpAddr { kind: IpAddrKind, address: String, } let home = IpAddr { kind: IpAddrKind::V4, address: String::from("127.0.0.1"), }; |
你可以将任何数据类型定义为枚举的字段,包括其它枚举。而且,每个枚举成员的字段可以各不相同:
1 2 3 4 5 6 |
enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } |
和结构类似,枚举也可以有方法:
1 2 3 4 |
impl Message { fn call(&self) { } } |
Rust没有引入其它语言支持的 null,null的缺陷是,你无法像使用普通值那样使用null值,这会导致错误。但是,“控制”所表示的概念的确是有意义的,为了解决此问题,Rust标准库提供了Option枚举:
1 2 3 4 |
enum Option<T> { Some(T), None, } |
由于Option的使用场景非常广泛,因此不需要显式导入即可使用Option及其成员:
1 2 3 4 |
let some_number = Some(5); let some_string = Some("a string"); // 使用None时,必须指定泛型参数,因为Rust不能自动推断 let absent_number: Option<i32> = None; |
在对 Option<T>中的 T进行操作时,必须进行类型转换,这时有机会处理None值的情况。
类似于switch-case语句:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
fn value_in_cents(coin: Coin) -> u32 { match coin { Coin::Penny => { println!("Lucky penny!"); 1 }, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, // 枚举成员的字段,可以在语句中访问 Coin::Quarter(state) => { println!("State quarter from {:?}!", state); 25 }, } } |
每个分支有两个部分:一个模式和一些代码,如果值匹配模式,则对应的代码就会执行。
Rust中的枚举必须穷尽所有case,无法列举则必须使用 _通配:
1 2 3 4 5 6 7 8 9 |
let some_u8_value = 0u8; match some_u8_value { 1 => println!("one"), 3 => println!("three"), 5 => println!("five"), 7 => println!("seven"), // 类似于default语句 _ => (), } |
可以用来简单的处理单个匹配,忽略其它匹配:
1 2 3 4 |
let some_u8_value = Some(0u8); if let Some(3) = some_u8_value { println!("three"); } |
支持指定else分支:
1 2 3 4 5 |
if let Coin::Quarter(state) = coin { println!("State quarter from {:?}!", state); } else { count += 1; } |
和Rust模块系统相关的概念包括:
- 包(Package):是Cargo的功能,用于构建、测试、分享Crate
- Crate:一个模块的树状结构,提供库项目或者二进制项目
- 模块(Modules)+ use 关键字:用于控制作用域、路径的私有性
- 路径(path):命名结构、函数、模块等
Rust标准库的很多函数会返回Result类型:
1 2 |
let mut input = String::new(); let mut result = io::stdin().read_line(&mut input).expect("Error!"); |
std::result::Result是一个泛型枚举,它的两个成员是Ok、Error,表示操作是否成功。各子模块有Result的子类,例如上例中的 io::Result。
io::Result具有expect方法,如果Result为Error则调用expect会导致程序崩溃,传递给expect的消息会展示给用户。否则,该方法返回Result的值。
Leave a Reply