学习 Rust 中的笔记

本文档使用和 Rust 官方文档同样的 mdbook 来编写,文中大部分代码可直接运行。

主要参考资料是官方文档:https://doc.rust-lang.org/book/

持续更新中...

开始学习精彩的 Rust 吧!

变量的声明

先来看看下面的代码:

#![allow(unused)]
fn main() {
let hello = "早上好";
hello = "晚上好";
println!("{}",hello);
}

以我多年的编程经验,看了又看,好像没什么问题,但是运行时却报如下错误

   Compiling playground v0.0.1 (/playground)
error[E0384]: cannot assign twice to immutable variable `hello`
 --> src/main.rs:4:1
  |
3 | let hello = "早上好";
  |     -----
  |     |
  |     first assignment to `hello`
  |     help: consider making this binding mutable: `mut hello`
4 | hello = "晚上好";
  | ^^^^^^^^^^^^^^^^ cannot assign twice to immutable variable

For more information about this error, try `rustc --explain E0384`.
error: could not compile `playground` due to previous error

编译器告诉我们,这个变量不能定义两次,如果要多次定义的话,需要使用 mut 关键字,于是我们按照编译器的要求,改成如下代码:

#![allow(unused)]
fn main() {
let mut hello = "早上好";
hello = "晚上好";
println!("{}",hello);
}

编译通过并成功输出晚上好

自动推断变量类型: 可以看到我们在声明变量时,使用了 let 关键字,这个关键字告诉编译器,这个变量是一个可变的(mutable)变量,这样编译器就可以自动推断出这个变量的类型。

变量遮蔽

Rust 允许声明相同的变量名,在后面声明的变量会遮蔽掉前面声明的,如下所示:

#![allow(unused)]
fn main() {
let x = 9;
let x = x+1;
println!("{}",x);
}

上面的代码中,会先将 x 赋值为 9,然后再声明一个相同的变量 x, 将当前的 x9 的变量加 1 后在重新赋值给新定义的 x 上,最后输出 10

不同于 mut 关键字,这中声明变量的方法会在第二次定义 x 变量时,在内存空间中开辟出一个新的空间。这样做有好处也有坏处,好处就是可以减少变量名的定义,比如当你只想要知道一个字符串的长度而不需要知道字符串的内容时,我们可以直接这样定义:

#![allow(unused)]
fn main() {
let x = "hello";
let x = x.len();
println!("字符串长度为: {x}");
}

⚠️ 当后一个变量遮蔽前一个变量时,我们就无法访问到前一个变量的值。

当你用 mut 关键字来实现相同的代码时,编译器就会报错:

#![allow(unused)]
fn main() {
let mut x = "hello";
x = x.len();
println!("字符串长度为: {x}");
}

发生错误:

      Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
 --> src/main.rs:4:5
  |
3 | let mut x = "hello";
  |             ------- expected due to this value
4 | x = x.len();
  |     ^^^^^^^ expected `&str`, found `usize`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `playground` due to previous error

因为第一个 let 已经推断出为们这个变量为字符串类型,但是第二次赋值时是一个 usize 类型,与预期类型不符,所以编译器就报错了。

变量遮蔽的坏处就如上面所说的,第二次定义变量时,就会在内存中在开辟一个空间来存放新的变量,因为它们本质上就是两个完全不同的变量,只是名字相同而已,然后性能上会有所损失。

常量

常量遵循如下规则:

  • 常量的值是不可变的
  • 常量可以在任何范围内声明,包括全局范围
  • 常量定义时必须指定数据类型
  • 常量命名约定是在单词之间使用全大写和下划线
#![allow(unused)]
fn main() {
const TEST_NUMBER:i32 = 123;
println!("{}",TEST_NUMBER);
}

基础数据类型

Rust 有四种主要的基础类型:整数、浮点数、布尔值和字符。

整数

直接用一张表格来说明 Rust 的整数类型:

长度有符号类型无符号类型
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

其中 iu 开头分别代表有符号和无符号类型的数据。有符号类型就是说明这个整数包括负数,而无符号类型就是说明这个整数不包括负数。 其中有 isizeusize 这两个类型,它可以根据你当前运行代码的系统来自动定义整数的类型长度,如果您使用的是 64 位架构,则数据长度为 64-bit;如果您使用的是 32 位,则数据长度为 32-bit。

我们可以和使用其他类型来定义数据一样来使用它:

#![allow(unused)]
fn main() {
let x:isize = 5;
println!("{}",x);
}

如果你不知道怎么选择数据的长度,那么我们一般可以不指定整数的类型,这时它的默认类型为 i32

#![allow(unused)]
fn main() {
let x = 5;
println!("{}",x);
}

更多表示方式

当然除了上面那些常规的表示方式,Rust 还支持如下表示方式:

数据例子
十进制98_222
十六进制0xff
八进制0o77
二进制0b1111_0000
Byte (u8 only)b'A'

如果十进制数据太长的话可以用 _ 符号来分割,比如上面的例子中的 98_222,其实还是 98222,我们还可以换成我们比较能一眼读出的 9_8222

#![allow(unused)]
fn main() {
let a = 98_222;
let b = 9_8222;
println!("a: {}\nb: {}",a,b);
}

如果要直接是用其他进制的值,只需在对应进制数据前加上一个常用的标识符即可,例如:

  • 十六进制:0x
  • 八进制: 0o
  • 二进制: 0b
#![allow(unused)]
fn main() {
let a = 0xFF;
let b = 0o77;
let c = 0b1111_0000;

println!("a: {}\nb: {}\nc: {}",a,b,c);
}

当然如果你想直接输出进制数据,我们可以直接这样写:

输出大小十六进制用::#X,输出小写十六进制::#x

#![allow(unused)]
fn main() {
let a = 0xFF;
let b = 0o77;
let c = 0b1111_0000;

println!("a: {:#X}\nb: {:#o}\nc: {:#b}",a,b,c);
}

当我们想输出一个字节的数据时,我们可以用 b 标识符来表示,例如:

#![allow(unused)]
fn main() {
let a = b'A';
println!("a: {}",a);

// 输出十六进制值
println!("a HEX: {:#X}",a);
}

浮点数

Rust 支持两种浮点数类型:f32f64

f32 代表 32 位浮点数,f64 代表 64 位浮点数。

#![allow(unused)]
fn main() {
let x = 20.0;
let y:f32 = 21.0;
println!("x: {:.1}\ny: {:.1}",x,y);
}

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

布尔值

Rust 当然也和其他语言一样支持布尔类型啦。 用法:

#![allow(unused)]
fn main() {
let x = true;
// or
let y:bool = false;
println!("x: {}\ny: {}",x,y);
}

字符

#![allow(unused)]

fn main() {
let a = 'a';
let b: char = 'q'; // with explicit type annotation
let c = '😻';

println!("a: {}\nb: {}\nc: {}",a,b,c);
}

请注意,我们 char 使用单引号指定文字,而不是使用双引号的字符串文字。Rust 的char类型大小为 4 个字节,表示一个 Unicode 标量值,这意味着它可以表示的不仅仅是 ASCII。在 Rust 中,重音字母、中文、日文、韩文字符、表情符号和零宽度空格都是 char 的有效值。Unicode 标量值的范围从 U+0000 到 U+D7FF 和 U+E000 到 U+10FFFF 在内。 这个 char 只能表示单个字符,如果用它来表示多个字符的话就会发生错误

#![allow(unused)]
fn main() {
let hello = '你好';
println!("hello: {}",hello);
}
   Compiling playground v0.0.1 (/playground)
error: character literal may only contain one codepoint
 --> src/main.rs:3:13
  |
3 | let hello = '你好';
  |             ^^^^^^
  |
help: if you meant to write a `str` literal, use double quotes
  |
3 | let hello = "你好";
  |             ~~~~~~

error: could not compile `playground` due to previous error

编译器提示这是字符串,可以用双双引号来表示,例如:

#![allow(unused)]
fn main() {
let hello = "你好";
println!("hello: {}",hello);
}

复合数据类型

除了基础数据类型,Rust 还支持两种复合数据类型,如:元组(Tuple)和数组(Array)。

元组

元组是将具有多种类型的多个值组合成一个复合类型的一般方法。元组具有固定长度:一旦声明,它们的大小就不能增长或缩小。

元组里面可以包含不同类型的数据,然后用括号 () 包围,比如:

#![allow(unused)]
fn main() {
let tup = (1,2.0,true,"hello");
println!("tup: {:?}",tup);
println!("tup[0]: {}",tup.0);
println!("tup[1]: {:.1}",tup.1);
println!("tup[2]: {}",tup.2);
println!("tup[3]: {}",tup.3);
}

当然我们在定义元组时也可以指定元组中每个位置数据的类型,例如:

#![allow(unused)]
fn main() {
let tup:(i32,f32,bool,&str)=(1,2.0,true,"hello");
println!("tup: {:?}",tup);
}

我们也可以对元组进行结构,将元组中的值分别放入不同的变量中,例如:

#![allow(unused)]
fn main() {
let tup = (1,2.0,true,"hello");
let (x,y,z,w) = tup;
println!("x: {}\ny: {:.1}\nz: {}\nw: {}",x,y,z,w);
}

数组

与元组不同,数组的每个元素都必须具有相同的类型。与其他一些语言中的数组不同,Rust 中的数组具有固定长度,也就是说,一旦声明,它们的大小就不能增长或缩小。(如果需要增大或缩小可以使用 Rust 中的向量类型)

在数组中,用方括号 [] 来包围数据,用逗号 , 来分割每个数据,比如:

#![allow(unused)]
fn main() {
let arr = [1,2,3,4,5];
println!("arr: {:?}",arr);
}

我们也可以指定数组的类型和大小:

#![allow(unused)]
fn main() {
let arr:[i32;5] = [1,2,3,4,5];
println!("arr: {:?}",arr);
}

此时就必须严格按照数组的大小和类型来声明数组,比如下面这些声明都是错误的:

// ❌ 预定义的数组大小不匹配
let arr:[i32;5] = [1,2,3,4];

// ❌ 预定义的数组类型不匹配
let arr:[i32;5] = [1.0,2.0,3.0,4.0,5.0];

也可以初始化一拥有相同默认值和指定长度的数组:

#![allow(unused)]
fn main() {
// 初始化长度为 5 ,默认值为 0 的数组
let arr = [0;5];
println!("arr: {:?}",arr);
}

函数

函数定义

在 Rust 中,用 fn 关键字来定义函数。

fn my_function() {
    println!("Hello, world!");
}
fn main() {
    my_function();
}

下面是一个带参数的函数,在函数入参区,先定义参数名,然后在定义参数的类型:

fn my_function(name: &str){
    println!("hello,{}",name);
}
fn main(){
    my_function("world");
}

函数返回值

函数可以将值返回给调用它们的代码。我们不命名返回值,但我们必须在箭头 -> 之后声明它们的类型。在 Rust 中,函数的返回值与函数体块中的最终表达式的值同义。可以通过使用 return 关键字并指定一个值来提前从函数返回,但大多数函数会隐式返回最后一个表达式,如下面的例子:

fn my_function() -> i32{
    9
}
fn main(){
    let x=my_function();
    println!("{x}");
}

可以看到,我们的 my_function() 函数并没有使用 return 关键字来返回指定的值,这样的话 Rust 会隐式返回最后一个表达式的值。注意这边最后一个表达式的值末尾不能带 ; 符号。

函数返回多个值

如果我们想返回多个值的时候,我们可以用元组来返回,然后在调用的时候进行解构:

fn my_function() -> (i32,f32){
    (9,3.14)
}
fn main(){
    let (x,y)=my_function();
    println!("x: {}\ny: {}",x,y);
}

if 语句

if 语句中的判断条件不需要用括号包围,但是代码块需要用大括号包围。

#![allow(unused)]
fn main() {
let x = 3;

if x == 7 {
    println!("x is 7");
} else {
    println!("x is not 7");
}
}

也可以使用 else if 语句来添加判断条件

#![allow(unused)]
fn main() {
let x = 3;

if x == 7 {
    println!("x is 7");
} else if x == 8 {
    println!("x is 8");
}else {
    println!("x is not 7 or 8");
}
}

在让我们看看下面的例子:

#![allow(unused)]
fn main() {
// ❌ 错误写法
let condition = true;
let mut x = condition?3:5;
println!("x is {}",x);
}

如果熟悉其他语言的朋友就知道这是一个标准的三元表达式,是不是感觉这样写没什么问题?但是当我们执行代码时却会发生错误。

在 Rust 中,可以用 if 语句来实现类似的表达式

#![allow(unused)]
fn main() {
let condition = true;
let y = if condition { 3 } else { 5 };
println!("y is {}",y);
}

循环

Rust 中有三种循环:loop、while 和 for。 这三种循环各有各的侧重点:

  • loop:如果需要无限循环,且不需要退出循环时(当然在 loop 中可以用 breakcontinue 来退出当前循环或整个循环 ),可以使用 loop 循环。像比如端口监听,我们可以使用 loop 循环来监听端口,直到收到一个新的连接。loop 循环还可以拥有返回值,可以将它赋值给一个变量。
  • while:如果需要循环,但是当满足某个条件要退出时,可以使用 while 循环。
  • for:如果要遍历一个数组时,可以用 for 循环。

loop 语句

最基础的用法:

loop {
    println!("more and more!")
}

上面代码会不断的输出 more and more!,如果要停止输出,只能终止这个程序运行。

跳出 loop 循环

当然我们也可以在 loop 循环体里添加 if 语句,然后用 break 跳出循环。

#![allow(unused)]
fn main() {
let mut x = 0;
loop {
    if x >= 5{
        break;
    }
    println!("more and more!");
    x += 1;
}
}

上面代码只会输出 5 遍 more and more!,然后就跳出循环。

loop 循环返回值

loop 还有一特性是它可以在结束循环时,使用 break 返回一个值

#![allow(unused)]
fn main() {
let mut x = 0;
let y = loop {
    if x >= 5{
        break "kill";
    }
    println!("more and more!");
    x += 1;
};
println!("{y}");
}

loop 循环嵌套标签

当我们有多个 loop 循环嵌套时,我们可以为 loop 循环定义一个标签,然后使用 break 来跳出指定标签下的循环。

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;

        'remain:loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {count}");
}

标签的命名以符号 ' 开头。

while 语句

在上面的 loop 循环中,我们通过在循环体中编写 if 语句来判断是否跳出循环体,如果你不想编写 if 语句,那么可以考虑使用 while 循环。

#![allow(unused)]
fn main() {
let mut x = 0;
while x < 5 {
    println!("hello");
    x+=1;
}
}

while 循环也支持嵌套写:

#![allow(unused)]
fn main() {
let mut x = 0;
while x < 2 {
    println!("hello");
    x += 1;
    let mut y = 0;
    while y < 3{
        println!("world!");
        y += 1;
    }
}
}

while 循环也可以使用循环标签,通过 break 跳出指定循环:

#![allow(unused)]
fn main() {
let mut x = 0;
'is_x:while x < 2 {
    println!("hello");
    x += 1;
    let mut y = 0;
    'is_y: while y < 3{
        println!("world!");
        if y == 1{
            break 'is_x;
        }
        y += 1;
    }
}
}

for 语句

如果要遍历一个数组或字符串时,可以用 for 循环。

#![allow(unused)]
fn main() {
let arr = [1,3,5,7,9];
for i in arr {
    println!("curr is {}",i);
}
}

或者这样

#![allow(unused)]
fn main() {
for i in 1..5 {
    println!("curr is {}",i);
}
}