Rust-base-learning

Rust基础学习–(一)

一、基础知识

1、hello rust–printfn!及其用法

eg:

1
2
3
4
5
6
7
8
9
println!("hello world");
println!("{1} {0}", "world", "Hello");
println!("{name} is {age} years old.", name = "Alice", age = 30);
let pi = 3.1415926;
println!("{}", pi);
println!("Pi is approximately {:.2}", pi); // 保留两位小数
println!("Number: {:05}", 42); // 宽度为5,不足部分补0
let point = (x = 10, y = 20);
println!("Point: {:?}", point);//输出Point: (x = 10, y = 20),输出变量的调试格式

printfn!()是一个宏,其功能是把格式化文本输出到标准输出(stdout),并且会自动换行。

Rust 使用 {} 来作为格式化输出占位符,其它语言可能使用的是 %s%d%p 等,由于 println! 会自动推导出具体的类型,因此无需手动指定。

其他相关宏:

print!:和 println! 类似,但不会自动换行。

eprintln!:把内容输出到标准错误(stderr)。

format!:将格式化后的字符串存储到变量中,而不是直接输出。

2、cargo、rustc

cargo new 新文件名(创建复杂工程)

cargo build 文件名(编译)

cargo run 文件名(编译加执行)

编译:rustc 文件名(rs文件)(简单)

执行:./exe文件

二、变量

1、手动设置变量的可变性:

rust需要手动设置变量是否可变,以同时保证安全性和灵活性。

默认下,变量都是不可变的,如果需要可变,需要通过mut关键字来声明。

eg:

1
2
3
4
5
6
fn main() {
let x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}

如果我们运行这段代码,就会报错,因为没有设置可变,解决方法就是在声明let并进行绑定的时候在let后面加上mut,变为let mut x=5;

(这里没有使用mut关键字的变量并不等同于常量,常量需要使用 const 关键字而不是 let 关键字来声明,并且值的类型必须标注Rust ,以下是一个常量命名示例:

1
const MAX_POINTS: u32 = 100_000;

常量的命名约定是全部字母都使用大写,并使用下划线分隔单词,另外对数字字面量可插入下划线以提高可读性。常量可以在任意作用域内声明,包括全局作用域,在声明的作用域内,常量在程序运行的整个过程中都有效。

2、变量的绑定,移动,克隆与拷贝

我们在rust中将变量声明称之为绑定。 这是因为Rust 最核心的原则——所有权,后续会学到。

何为所有权?简单来讲,任何内存对象都是有主人的,而且一般情况下完全属于它的主人,绑定就是把这个对象绑定给一个变量,让这个变量成为它的主人,同时,该对象之前的主人就会丧失对该对象的所有权。

Rust 中,将一个值赋给另一个变量时,会发生移动(Move),而非复制。移动后,原变量不再有效。若你确实需要复制值,可以使用 clone 方法(深拷贝)。

此外,拷贝(copy)实现了 Copy trait 的类型(如整数、布尔值等),赋值时会直接复制值,不会转移所有权。

1
2
3
4
5
6
7
8
9
10
11
12
//move
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权移交给 s2
println!("{}", s1); // 错误!s1 已无效
//clone
let s1 = String::from("hello");
let s2 = s1.clone(); // 复制 s1 的数据
println!("s1 = {}, s2 = {}", s1, s2); // 正常运行
//copy
let x = 5;
let y = x; // x 的值被复制给 y
println!("x = {}, y = {}", x, y); // 正常运行

3、忽略未被使用的变量:

rust会给声明却未使用的变量一个警告,如想去掉警告,需要在变量声明的时候在变量名前面加一个_(是在变量名前面直接加,没有空格,变量名也没有被改变)

4、变量解构:

let 表达式不仅仅用于变量的绑定,还能进行复杂变量的解构:从一个相对复杂的变量中,匹配出该变量的一部分内容:

1
2
3
4
5
6
7
8
fn main() {
let (a, mut b): (bool,bool) = (true, false);
// a = true,不可变; b = false,可变
println!("a = {:?}, b = {:?}", a, b);

b = true;
assert_eq!(a, b);
}

(assert_eq!:

主要用于测试代码时验证两个值是否相等。要是这两个值不相等,程序就会触发 panic(崩溃,Rust 使用这个术语来表明程序因错误而退出),测试也就失败了。

具体用法:

1
2
3
4
fn test_division() {
let result = 10 / 2;
assert_eq!(result, 5, "10除以2应该等于5,但实际结果是{}", result);//自己指定错误信息,也可以不指定,由系统自动输出失败信息
}

相关地,assert_ne! 用于验证两个值是否不相等)

解构式赋值:

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Struct {
e: i32
}

fn main() {
let (a, b, c, d, e);

(a, b) = (1, 2);
// _ 代表匹配一个值,但是我们不关心具体的值是什么,因此没有使用一个变量名而是使用了 _
[c, .., d, _] = [1, 2, 3, 4, 5];//第一个元素赋值为c,倒数第二个赋值为d,这是 Rust 1.59 引入的数组切片模式
Struct { e, .. } = Struct { e: 5 };//从右侧的结构体中提取 e 字段的值(即5),并赋给e,这是结构体模式匹配

assert_eq!([1, 2, 1, 4, 5], [a, b, c, d, e]);
}

(需要注意的是,使用 += 的赋值语句还不支持解构式赋值)

5、变量遮蔽

Rust 允许声明相同的变量名,在后面声明的变量会遮蔽掉前面声明的(在被遮蔽后,无法再访问到之前的同名变量)。

eg:

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let x = 5;
// 在main函数的作用域内对之前的x进行遮蔽
let x = x + 1;
{
// 在当前的花括号作用域内,对之前的x进行遮蔽
let x = x * 2;
println!("The value of x in the inner scope is: {}", x);
}

println!("The value of x is: {}", x);
}

这和 mut 变量的使用是不同的,第二个 let 生成了完全不同的新变量,两个变量只是恰好拥有同样的名称,涉及一次内存对象的再分配 ,而 mut 声明的变量,可以修改同一个内存地址上的值,并不会发生内存对象的再分配,性能要更好。

6、变量的类型

(1)整数类型

长度 有符号类型 无符号类型
8 位 i8 u8
16 位 i16 u16
32 位 i32 u32
64 位 i64 u64
128 位 i128 u128
视架构而定 isize usize

整型字面量可以用下表的形式书写:

数字字面量 示例
十进制 98_222
十六进制 0xff
八进制 0o77
二进制 0b1111_0000
字节 (仅限于 u8) b'A'

整型溢出

当在 debug 模式编译时,Rust 会检查整型溢出,若存在这些问题,则使程序在编译时 panic

在当使用 --release 参数进行 release 模式构建时,Rust 检测溢出。相反,当检测到整型溢出时,Rust 会按照补码循环溢出(two’s complement wrapping)的规则处理。简而言之,大于该类型最大值的数值会被补码转换成该类型能够支持的对应数字的最小值。比如在 u8 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是期望值。依赖这种默认行为的代码都应该被认为是错误的代码。

(2)浮点数类型与运算

浮点数根据 IEEE-754 标准实现。f32 类型是单精度浮点型,f64 为双精度。

1
2
3
4
fn main() {
// 断言0.1 + 0.2与0.3相等
assert!(0.1 + 0.2 == 0.3);
}

这个程序会panic,因为二进制精度问题,导致了 0.1 + 0.2 并不严格等于 0.3(在i64类型中),它们可能在小数点 N 位后存在误差。

那如果非要进行比较的话,可以考虑用这种方式 (0.1_f64 + 0.2 - 0.3).abs() < 0.00001 ,具体小于多少,取决于对精度的需求。

(NaN:

对于数学上未定义的结果,例如对负数取平方根 -42.1.sqrt() ,会产生一个特殊的结果:Rust 的浮点数类型使用 NaN (not a number) 来处理这些情况

所有跟 NaN 交互的操作,都会返回一个 NaN,而且 NaN 不能用来比较,代码会崩溃)

运算:

综合示例:

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
26
fn main() {
// 编译器会进行自动推导,给予twenty i32的类型
let twenty = 20;
// 类型标注
let twenty_one: i32 = 21;
// 通过类型后缀的方式进行类型标注:22是i32类型
let twenty_two = 22i32;

// 只有同样类型,才能运算
let addition = twenty + twenty_one + twenty_two;
println!("{} + {} + {} = {}", twenty, twenty_one, twenty_two, addition);

// 对于较长的数字,可以用_进行分割,提升可读性
let one_million: i64 = 1_000_000;
println!("{}", one_million.pow(2));

// 定义一个f32数组,其中42.0会自动被推导为f32类型
let forty_twos = [
42.0,
42f32,
42.0_f32,
];

// 打印数组中第一个值,并控制小数位为2位
println!("{:.2}", forty_twos[0]);
}

位运算:

Rust 的位运算基本上和其他语言一样

运算符 说明
& 位与 相同位置均为1时则为1,否则为0
| 位或 相同位置只要有1时则为1,否则为0
^ 异或 相同位置不相同则为1,相同则为0
! 位非 把位中的0和1相互取反,即0置为1,1置为0
<< 左移 所有位向左移动指定位数,右位补0
>> 右移 所有位向右移动指定位数,带符号移动(正数补0,负数补1)

序列:Rust 提供了一个非常简洁的方式,用来生成连续的数值,例如 1..5,生成从 1 到 4 的连续数字,不包含 5 ;1..=5,生成从 1 到 5 的连续数字,包含 5,它的用途很简单,常常用于循环中。

eg:

1
2
3
4
5
6
for i in 1..=5 {
println!("{}",i);
}
for i in 'a'..='z' {
println!("{}",i);
}

序列只允许用于数字或字符类型,原因是:它们可以连续,同时编译器在编译期可以检查该序列是否为空,字符和数字值是 Rust 中仅有的可以用于判断是否为空的类型。

有理数和复数并未包含在标准库中,如需使用,需要引入 num 库:

步骤如下:

  1. 创建新工程 cargo new complex-num && cd complex-num
  2. Cargo.toml 中的 [dependencies] 下添加一行 num = "0.4.0"
  3. src/main.rs 文件中的 main 函数替换为下面的代码
  4. 运行 cargo run
1
2
3
4
5
6
7
8
9
use num::complex::Complex;

fn main() {
let a = Complex { re: 2.1, im: -1.2 };
let b = Complex::new(11.1, 22.2);
let result = a + b;

println!("{} + {}i", result.re, result.im)
}

(3)字符类型

在 Rust 语言中这些都是字符,Rust 的字符不仅仅是 ASCII,所有的 Unicode 值都可以作为 Rust 字符,包括单个的中文、日文、韩文、emoji 表情符号等等,都是合法的字符类型。由于 Unicode 都是 4 个字节编码,因此字符类型也是占用 4 个字节

(4)布尔类型

Rust 中的布尔类型有两个可能的值:truefalse,布尔值占用内存的大小为 1 个字节。

使用布尔类型的场景主要在于流程控制,例如上述代码的中的 if 就是其中之一。

(5)单元类型

单元类型就是 (),唯一的值也是 ()

main函数就返回单元类型(),常见的 println!() 的返回值也是单元类型 ()。

再比如,可以用 () 作为 map 的值,表示我们不关注具体的值,只关注 key。 这种用法 ()可以作为一个值用来占位,但是完全不占用任何内存。

7、类型转换

Rust 中可以使用 As 来完成一个类型到另一个类型的转换,其最常用于将原始类型转换为其他原始类型,但是它也可以完成诸如将指针转换为地址、地址转换为指针以及将指针转换为其他指针等功能。包括AsRef trait和AsMut trait

AsRef trait 用于不可变引用的转换,定义如下:

1
2
3
pub trait AsRef<T> where T: ?Sized {
fn as_ref(&self) -> &T;
}

AsMut trait 用于可变引用的转换,定义如下:

1
2
3
pub trait AsMut<T> where T: ?Sized {
fn as_mut(&mut self) -> &mut T;
}

下面是一些使用AsRefAsMut的常见场景:

(1)String 转换为 & str:

1
2
3
4
5
6
7
8
9
10
fn print_str(s: &str) {
println!("{}", s);
}

fn main() {
let s = String::from("hello");
print_str(s.as_ref()); // 将String转换为&str
// 或者直接使用deref coercion:
print_str(&s); // &String可以自动转换为&str
}

(2) 使用 AsRef 处理多种类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn process_file<P: AsRef<std::path::Path>>(path: P) {
let path_ref = path.as_ref();
println!("Processing file: {:?}", path_ref);
}

fn main() {
use std::path::PathBuf;

let path_str = "/tmp/foo.txt";
let path_buf = PathBuf::from("/tmp/bar.txt");

process_file(path_str); // &str实现了AsRef<Path>
process_file(path_buf); // PathBuf实现了AsRef<Path>
}

(3)AsMut 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Buffer(Vec<u8>);

impl AsMut<[u8]> for Buffer {
fn as_mut(&mut self) -> &mut [u8] {
&mut self.0
}
}

fn main() {
let mut buf = Buffer(vec![1, 2, 3]);
let slice = buf.as_mut(); // 获取可变切片
slice[0] = 42; // 修改切片内容
println!("{:?}", buf.0); // 输出: [42, 2, 3]
}

三、语句、表达式、函数

1、语句

1
2
3
4
let a = 8;
let b: Vec<f64> = Vec::new();
let (a, c) = ("hi", false);

以上都是语句,它们完成了一个具体的操作,但是并没有返回值,因此是语句。

由于 let 是语句,因此不能将 let 语句赋值给其它值,如下形式是错误的:

1
let b = (let a = 8);

2、表达式

表达式会进行求值,然后返回一个值。例如 5 + 6,在求值后,返回值 11,因此它就是一条表达式。

表达式可以成为语句的一部分,例如 let y = 6 中,6 就是一个表达式,它在求值后返回一个值 6(有些反直觉,但是确实是表达式)。

调用一个函数是表达式,因为会返回一个值,调用宏也是表达式,用花括号包裹最终返回一个值的语句块也是表达式,总之,能返回值,它就是表达式。

(表达式不能包含分号!)

最后,表达式如果不返回任何值,会隐式地返回一个() 。

3、函数

Rust 的函数体是由一系列语句组成,最后由一个表达式来返回值,例如:

1
2
3
4
5
6
fn add_with_extra(x: i32, y: i32) -> i32 {
let x = x + 1; // 语句
let y = y + 5; // 语句
x + y // 表达式,没有分号
}

【注意:函数的位置可以随便放,Rust 不关心我们在哪里定义了函数,只要有定义即可

每个函数参数都需要标注类型】

(发散函数:

当用 ! 作函数返回类型的时候,表示该函数永不返回( diverging functions ),特别的,这种语法往往用做会导致程序崩溃的函数:

1
2
3
4
fn dead_end() -> ! {
panic!("你已经到了穷途末路,崩溃吧!");
}

To Be Continue…


Rust-base-learning
http://example.com/2025/06/28/Rust-base-learning/
作者
oxygen
发布于
2025年6月28日
许可协议