基础语法
Rust 是强类型语言,但是其 let 关键字可以充当 C++ auto 的角色。另外,其类型声明是后置类型声明:
let a:i32 = 10;
Rust 中的变量分为三种:
常量:使用 const 声明的变量,不允许更改值
不可变变量:使用 let 声明的变量,只能通过多次声明更改变量值
可变变量:使用 let mut 声明的变量,可以随意更改值
常量必须要指定类型,不可变变量在多次声明的过程中允许改变类型。子作用域中的声明不会更改父作用域变量的值
不允许窄化转换 非零值不能转换为布尔值 没有自增和自减运算符
数据类型
Rust 中内置了诸如 i32, u32 这些宏,另外还添加了 isize, usize 这两个大小与 CPU 字宽度相同的类型
浮点数分为 f32, f64 两种,默认会使用 f64,因为两者处理速度差不多,但是 f64 精度更高
Rust 中的字符大小为 4 字节,只允许 UTF-8 编码
元组使用圆括号声明,数组使用中括号声明:
let a = (510, 1.2, 'c');
let b = [10, 20, 30];
println!("{}", a.2);
println!("{}", b[0]);
f32 和 f64 只实现了 PartialEq 和 PartialOrd,这是因为 float 类型中存在两个特殊的值:
前者不是一个数值,无法进行数值运算。后者是一个无穷值,亦无法进行比较。 |
注释
注释方式和 C++ 一样
函数
函数的声明方式也是后置类型声明:
fn main() {
println!("{}", add(1, 2));
}
fn add(a: i32, b: i32) -> i32 {
return a + b;
}
函数不需要前置类型声明,只要你代码中能找到这个函数就行,写前面还是写后面都行
另外,对于返回值有一种简写形式:
fn add(a: i32, b: i32) -> i32 {
a + b
}
需要注意的是,后面不能跟分号
匿名函数
Rust 中最基础的定义匿名函数的方式如下:
let s = String::from("你好");
let lam = |s_str: &str| {
println!("{}", s_str);
};
这种方式和普通的函数没什么不同。
闭包
和 C++ 相同,如果 Rust 捕获了外部的变量,则成为一个闭包。但是 Rust 会自动计算捕获列表,无需手动指明:
let s = String::from("你好");
let lam = || {
println!("{}", s);
};
如上述代码所示,Lambda 表达式中可以使用前面的变量。默认的捕获方式为 不可变引用。
下面是闭包捕获变量的方式:
捕获方式 | 示例 |
---|---|
不可变引用 |
|
可变引用 |
|
移动捕获 |
|
Rust 中可变引用是独占的。一旦你在 Lambda 表达式中捕获了可变引用,那么在最后一次 Lambda 之前,就不能再修改被捕获的变量了。 |
闭包的捕获模式
闭包在捕获变量时的顺序如下: [1]
不可变引用
唯一不可变引用
可变引用
移动
结构体、元组、枚举等复合类型始终作为一个整体被捕获,无法捕获单个字段。
变量被捕获的方式与变量在闭包中的使用方式有关,而与上下文无关(例如变量的生命周期等)。
如果使用 move 关键字,则变量只能通过拷贝(实现 Copy trait)或者移动被捕获。
条件语句
条件语句中的条件无需加小括号:
fn main() {
let number = 3;
if number < 2 {
println!("number < 2");
} else if number < 5 {
println!("number < 5");
} else {
println!("number >= 5");
}
}
另外还能实现类似 C++ 三目运算符的效果:
let number = if number > 2 { number*2} else {0};
循环
let mut n = 1;
while n != 4 {
println!("{}", n);
n += 1;
}
let a = [10, 20, 30];
for i in a {
println!("{}", i);
}
for i in 0..5 {
println!("{}", i);
}
let _m = loop {
if n == 10 {
break n;
}
n += 1;
};
Rust 中没有 for 循环,只有 while 和 for_each 循环。另外 loop 循环相当于 while(true) 。但是不同的是可以使用 break 返回一个值
loop 在单独成块时末尾无需加分号,但是作为初始化语句的一部分时需要加分号(毕竟是语句)
这里之所以将变量命名为 _m 是因为我的 Rust 将 Warning 视为 Error,这里有命名 Warning
所有权
Rust 也有作用域规则,并将表达式分为值和变量两部分,遵循以下规则:
栈中的值默认执行拷贝语义
堆中的值默认执行移动语义
变量可以引用其它变量
例如:
#![cfg_attr(
debug_assertions,
allow(dead_code, unused_imports, unused_variables, unused_mut)
)]
fn main() {
let s_a = 10;
let s_b = s_a; // 执行拷贝语义
let h_a = String::from("hello");
let h_b = h_a; // 执行移动语义,此处 h_a 会失效
let mut h_c = h_b.clone(); // 执行拷贝
print_s(h_b); // 执行移动语义,此处 h_b 会失效
let r_a = &h_c; // r_a 是指向 h_c 的只读引用
let r_b = &mut h_c; // r_b 是指向 h_c 的可写引用
}
fn print_s(str: String) {
println!("{}", str);
}
第一行代码是为了关闭 unused warning,不关掉的话代码没法通过编译
Rust 将引用的过程称为租赁
另外,Rust 没有 free 或者 delete,它会在作用域结束时自动为你添加资源清理代码
与 C++ 不同的是,引用不会影响原宿主的生命周期,当原宿主失效时,引用必须重新绑定:
#![cfg_attr(
debug_assertions,
allow(dead_code, unused_imports, unused_variables, unused_mut)
)]
fn main() {
let s1 = String::from("hello");
let r = &s1;
let s2 = s1; // 移交 s1 所有权, r 必须重新绑定
let r = &s2;
}
悬垂引用在编译期就会被发现。
切片
Rust 和 Golang 一样也具备切片。切片是对一片内存区域的引用。
不可变切片类似于
const*const
指针。可变切片类似于
*const
指针。
fn main() {
let s1 = "hello"; // s1 是 &str 类型
let mut s2 = String::from("hello"); // s2 是 String 类型
let slice1 = &s2[0..3]; // slice1 是 &str 类型
let slice2 = &s2[3..];
let slice3 = &s2[..3];
println!("{}", slice1);
println!("{}", slice2);
println!("{}", slice3);
}
结构体
使用结构体
struct Student {
id: u32,
name: String,
}
fn main() {
let stu = Student {
id: 1234,
name: String::from("小明"),
};
let stu2 = Student {
id: 2345,
..stu // stu 中非 id 字段被移动
};
// println!("{}", stu.name); // 此处不允许,stu.name 已经被移动
println!("{}", stu2.name);
}
Rust 中的结构体无需加分号结尾,stu2 展示了另一种语法:当新的结构体 至少 有一个字段不同时,可以简化语法,这种语法我认为可以被成为更新
另外还有一个类元组的结构体:
struct Color(u32, u32, u32);
fn main() {
let black = Color(0, 0, 0);
}
这种结构体的使用方式与元组相同,但是元组结构体是一个语句,末尾需要添加分号
方法
Rust 可以将方法绑定到结构体上,具体语法为:
impl Struct {
// 需要绑定到 Struct 上的函数
}
当 func 具备 self 参数时,类似 C++ 中的成员函数。
当 func 不具备 self 参数时,类似 C++ 中的静态成员函数
struct Int {
data: u32,
}
impl Int {
fn create(data: u32) -> Int {
return Int { data };
}
fn bigger(&self, b: &Int) -> bool {
return self.data > b.data;
}
}
fn main() {
let a = Int::create(10);
let b = Int::create(11);
println!("{}", a.bigger(&b));
}
impl 可以写任意次,总的效果相当于他们的并集
构造函数
Rust 存在两种构造函数:
基于约定的 new 方法。
Default trait。
Rust 一般约定使用 new 来构造一个对象。此外还能通过 Default trait 支持默认构造函数:
/// Time in seconds.
///
/// # Example
///
/// ```
/// let s = Second::default();
/// assert_eq!(0, s.value());
/// ```
pub struct Second {
value: u64
}
impl Second {
/// Returns the value in seconds.
pub fn value(&self) -> u64 {
self.value
}
}
impl Default for Second {
fn default() -> Self {
Self { value: 0 }
}
}
如果所有类型的字段都实现了 Default,还能派生 Default:
/// Time in seconds.
///
/// # Example
///
/// ```
/// let s = Second::default();
/// assert_eq!(0, s.value());
/// ```
#[derive(Default)]
pub struct Second {
value: u64
}
impl Second {
/// Returns the value in seconds.
pub fn value(&self) -> u64 {
self.value
}
}
|
枚举
定义枚举
枚举的三种写法:
enum Book {
Papery,
Electronic,
}
enum Anmial {
Dog(String),
Cat(String),
}
enum Pair {
Number { number: u32 },
String { str: String },
}
模式匹配
枚举中可以储存值,这些值要取出来就需要使用 match 控制流。以标准库中的 Option 为例:
enum Option<T> {
None,
Some(T),
}
使用方式为:
let opt = Some(10);
match opt {
Some(v) => {
println!("{}", v)
}
Option::None => {}
}
match 还提供了 _ 充当 default 语义:
match opt {
Some(v) => {
println!("{}", v)
}
_ => {}
}
match 还可以提供更复杂的表示:
let a: Result<i32, ()> = Ok(10);
match a {
Ok(v) if v > 10 => {
println!(">10");
}
/// match v 同时会将值绑定到 o 上。
o @ Ok(v) if v > 5 => {
o.unwrap();
}
Ok(v) => {
println!("v < 5, v is {v}")
}
Err(_err) => {
unreachable!()
}
}
if let
if let 语句用于匹配枚举中的一种情形,失配情形则直接忽略:
let opt = Some(10);
if let Some(v) = opt {
println!("{}", v)
}
集合
vector
vector 和 C++ 中 vector 的底层数据相同。使用方式类似:
let mut v: Vec<i32> = Vec::new();
v.push(10);
v.push(20);
for i in &v { // 使用引用以防止 v 丢失容器的所有权
println!("{}", i);
}
创建容器的另一种方式是使用宏:
let v = vec![1, 2, 3];
vec 访问元素有两种方式:
访问方式 | 返回值 | 错误方式 |
---|---|---|
下标访问 | 对应的值 | 触发 panic |
使用 get | Option |
|
当持有 vec 的引用时,无法向容器中添加更多的值。其根本原因是因为当 vec 容量不够时会重新开辟内存空间,并将以前的数据复制过去,从而导致引用失效。 |
map
和其它语言中的 map 类似。map 要求 key 类型一致,value 类型一致。由于 HashMap 类型不在 rust 的 prelude 中,因此需要手动引入:
use std::collections::HashMap;
fn main() {
let mut map: HashMap<String, i32> = HashMap::new();
map.insert(String::from("a"), 10);
map.insert(String::from("b"), 20);
for (k, v) in &map {
println!("{}: {}", k, v);
}
}
HashMap 的访问方式和 vector 相同,也有两种方式,且行为相同。
覆盖新值
map 的 insert 方法默认会覆盖掉旧值,这时可以使用 entry 方法,当且仅当没有 key 时才插入:
let mut map: HashMap<String, i32> = HashMap::new();
map.entry(String::from("a")).or_insert(50);
字符串
Rust 字符串有两种形式:
最基础的字符串是 &str 类型。这类似于 C++ 中的 char*
String 是对 str 的封装
两种形式都原生支持宽字节类型,因而无法使用下标的方式进行引用。
当使用 String 的切片生成 str 类型时,如果切片落在了宽字符中间,就会导致程序 panic |
String 的生成方式一般有两种:
生成方式 | 要求 |
---|---|
| 参数实现了 Display trait |
| 无 |
fromat 的一般使用方式如下:
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = fromat!("{s1}-{s2}-{s3}");
泛型
简单的泛型语法:
struct Number<T> {
data: T,
}
impl<T> Number<T> {
fn get_data(&self) -> &T {
return &self.data;
}
}
fn main() {
let a = Number { data: 10 };
let b = Number { data: 11 };
println!("{}", a.get_data());
}
和 C++ 一样,函数也允许有泛型,这是 impl 中也需要添加相应的类型,而且泛型的具体类型可以被推断出来。推断时不会发生类型转换
错误处理
Rust 将错误分为可恢复的错误和不可恢复的错误。
不可恢复的错误类似 C++ 中断言失败的宏:
fn main() {
panic!("failed");
}
可恢复错误通过 Result<T,E> 枚举 表示,可能产生异常的函数的返回值都是 Result 类型的:
fn main() {
let f = File::open("hello.txt");
match f{
Ok(file) =>{
println!("file opened");
},
Err(err) =>{
println!("Failed");
}
}
}
实际上报错时显示类型是 tuple |
使用 if let 语法可以简化处理流程:
fn main() {
let f = File::open("hello.txt");
if let Ok(file) = f {
println!("File opened successfully.");
} else {
println!("Failed to open the file.");
}
}
直接对 Result 调用 unwrap/expect 会导致系统直接挂掉
因为异常只是一个类型,所以传递时直接返回就行了,异常还有一个简单语法:
fn g(i: i32) -> Result<i32, bool> {
let t = f(i)?;
Ok(t) // 因为确定 t 不是 Err, t 在这里已经是 i32 类型
}
这里函数 f 的返回值是 Result,其 E 的类型必须和 g 的 E 的类型相同,? 运算符用来将异常取出,如果有异常直接返回,否则向下继续执行
生命周期参数
来看一个返回引用的函数的例子:
fn longer(s1: &str, s2: &str) -> &str {
if s1.len() > s1.len() {
return s1;
}
return s2;
}
fn main() {
let s1 = "he";
let s2 = "she";
let r = longer(&s1, &s2);
print!("{}", r);
}
遗憾的是 Rust 不会允许编译的,根本原因在于 longer 返回的是一个引用,但是引用的值的生命周期不知道。可以通过生命周期参数解决这个问题
下面我会将术语生命周期进行加粗,而和 C++ 中生命周期相同的那个术语不会
生命周期参数的语法是一个单引号后跟一个小写字母,习惯上以 'a
表示。生命周期注解描述了多个引用生命周期相互的关系,而不影响其生命周期。
现在使用生命周期参数来纠正这个函数:
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s1.len() {
return s1;
}
return s2;
}
这个函数的签名分为了三个部分:
fn longer<'a>
中使用泛型的语法将生命周期参数引入函数s1: &'a str
表明 s1 的生命周期至少与'a
的生命周期一样长&'a str
表示返回值的生命周期至少与'a
一样长
'a
的生命周期是 s1 和 s2 中生命周期的较小值
下面无法通过编译的代码,从侧面证实了生命周期参数不会延长值的生命周期:
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s1.len() {
return s1;
}
return s2;
}
fn main() {
let r;
{
let s1 = String::from("he");
let s2 = String::from("she");
r = longer(s1.as_str(), s2.as_str());
}
print!("{}", r);
}
生命周期注释有一个特别的:'static 。所有用双引号包括的字符串常量所代表的精确数据类型都是 &'static str ,'static 所表示的生命周期从程序运行开始到程序运行结束,因此下面的代码可以通过编译:
let r;
{
let s1 = "he";
let s2 = "she";
r = longer(s1, s2);
}
print!("{}", r);
生命周期参数从语法上可以简单地认为就是一个特殊的模板参数 |
模块、包和箱
简单地来讲,含有 Cargo.toml 文件的项目就是一个包,项目编译后生成的二进制文件就是箱
Rust 中的模块类似于 C++ 中命名空间和 Python 中模块的并集,遵循以下原则:
如果没有显式表明模块,则每个文件就代表一个模块(类似 Python)
如果显式声明模块,则遵循声明(类似命名空间)
模块允许嵌套。只有平级或者更深层次的模块才允许访问私有的函数或者结构体。如果希望外部访问,必须使用 pub 公开
模块的使用方式与命名空间类似,同样是使用 ::
,但是导入运算符使用了 use 而不是 using
终端 IO
例如:
use std::io::stdin;
use std::io::BufRead;
fn main() {
let args = std::env::args();
for arg in args {
println!("{}", arg);
}
// echo
let mut buf = String::new();
stdin().read_line(&mut buf).unwrap();
println!("{}", buf);
}
文件 IO
先看一个单向读写的例子:
use std::fs;
fn main() {
fs::write("1.txt", "hello").unwrap();
let content = fs::read_to_string("1.txt").unwrap();
println!("{:?}", content);
}
再看一个双向读写的例子:
use std::{
fs,
io::{Read, Seek, Write},
};
fn main() {
let mut file = std::fs::OpenOptions::new()
.write(true)
.read(true)
.open("1.txt")
.unwrap();
file.write(b"hello,world\n").unwrap();
// 刷新缓冲区
file.flush().unwrap();
file.seek(std::io::SeekFrom::Start(0)).unwrap();
// 读取数据
let mut buf = String::new();
file.read_to_string(&mut buf).unwrap();
println!("{}", buf);
}
智能指针
智能指针是实现了 Deref 和 Drop traits 的结构体。这两个 trait 分别具备以下功能:
Deref | 可以被解引用 |
Drop | 退出作用域时需要执行 |
Box
Box 是功能最少的一个智能指针。其除了数据被分配在堆上,和普通变量并没有什么区别。类似于 C++ 中的 ScopedGuard:
let b = Box::new("你好");
println!("{}", *b);
Rc
Rc 是具备引用计数的不可变智能指针,类似于 C++ 中的共享指针:
use std::rc::Rc;
fn main() {
let b = Rc::new("你好");
println!("{}", *b);
}
同样的,因为 rc 并不是 prelude 的一分子,因此需要实现声明。
Rc 的拷贝会引起引用计数的增加。
类型转换
类型转换一般有以下几种:
隐式类型转换。
使用 as 进行强制类型转换。
使用 into 和 from 进行类型转换。
其中, as 作为隐式类型转换的补充出现,主要是为了填补隐式类型转换不允窄化类型转换的空缺。
into 和 from 作为两个 trait 来用允许自定义类型转换。此外 into 和 from 还有两个安全版本的 try_into 和 try_from。当失败时,返回 Error。
清除 Warning
如果需要关闭当前文件的 Warning,在当前文件顶部添加:
#[allow(dead_code)]
如果关闭 crate 的 Warning,需要在 main.rs 中添加:
#![allow(dead_code)]
格式化
以 println! 为例,格式化的方法有三种:
-
println!("{:?}", some_var);
-
println!{"{1:?}, {0}", var0, var1};
-
println!{"{var0:?}, {var1}"};
Cell,RefCell 和 UnsafeCell
UnsafeCell 提供了对内置对象的可变引用。UnsafeCell 使用 #[repr(transparent)]
来表示数据类型,因此 UnsafeCell 与包含的类型类型布局相同。
Cell 对使用了 Copy 类型的对象使用 Copy in 和 Copyout 实现可变引用。对实现了 Default 类型的对象使用 mem::replace
实现可变引用。由于 memmove 本事不是线程安全的,因此 Cell 也不是线程安全的。
RefCell 在 Cell 的基础上添加了对引用计数的检测。如果多次 borrow 会导致线程 panic。