notes of Rustlearning

学习使用的教程

hello world 和 hello cargo

hello world

创建一个以hello.rs文件,内容如下:

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

然后在终端中运行:

rustc hello.rs
./hello

便会运行输出Hello, world!。这是最简单编写运行Rust程序的方式。

cargo

Cargo 是 Rust 的构建系统和包管理器。使用 Cargo 来管理 Rust 项目,它可以为你处理很多任务,比如构建代码、下载依赖库并编译这些库。

使用cargo创建项目的方法:

cargo new hello_cargo
cd hello_cargo

进入 hello_cargo 目录并列出文件。将会看到 Cargo 生成了两个文件和一个目录:一个 Cargo.toml 文件,一个 src 目录,以及位于 src 目录中的 main.rs 文件。

如果要在已有的文件里使用 Cargo,可以在项目目录下运行:

cargo init

编译和运行

在项目目录下运行:

cargo build

这会编译项目并生成可执行文件。可执行文件位于 target/debug/ 目录下。如果需要发布时可以加上参数--release,这样会进行优化编译:

要运行项目,可以使用:

cargo run

Cargo 还提供了一个叫 cargo check 的命令。该命令快速检查代码确保其可以编译,但并不产生可执行文件

常见编程概念的迁移

变量和可变性

变量的定义

在 Rust 中,使用let声明一个变量。变量默认是不可变的。要声明一个可变变量,可以使用 mut 关键字:

let x = 5; // 不可变变量
let mut y = 10; // 可变变量

如果尝试修改不可变变量,会导致编译错误。Rust的编译器会保证不可变的变量不会发生改变。

常量

常量使用 const 关键字声明,在声明时也必须指明其数据类型,必须在编译时就确定值。常量可以在任何作用域中使用,包括函数内部和外部。

const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

常量可以用常量表达式来定义

在声明它的作用域之中,常量在整个程序生命周期中都有效

遮蔽

遮蔽是指使用同名变量来隐藏之前的变量。可以通过重新声明一个变量来遮蔽之前的变量。

第二个变量遮蔽了第一个变量,此时任何使用该变量名的行为中都会视为是在使用第二个变量,直到第二个变量自己也被遮蔽或第二个变量的作用域结束。

fn main() {
    let x = 5;

    let x = x + 1;

    {
        let x = x * 2;
        println!("The value of x in the inner scope is: {x}");
    }

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

遮蔽和变量修改是有不同的;遮蔽创建的新的变量,修改原来的变量则是改变原来的变量的值。

数据类型

Rust 是一种静态类型的语言,在编译时就必须确定所有变量的类型。在定义变量或者常量时,可以使用类型注解来指定变量的类型。

有两种数据类型:标量类型和复合类型。

标量类型

标量类型表示单一的值。Rust 中有四种基本的标量类型:整数、浮点数、布尔值和字符。

整型

以下是 Rust 中整型的简洁表格:

类型 长度 范围
i8 8 位 -2⁷ 到 2⁷-1
u8 8 位 0 到 2⁸-1
i16 16 位 -2¹⁵ 到 2¹⁵-1
u16 16 位 0 到 2¹⁶-1
i32 32 位 -2³¹ 到 2³¹-1
u32 32 位 0 到 2³²-1
i64 64 位 -2⁶³ 到 2⁶³-1
u64 64 位 0 到 2⁶⁴-1
isize 平台相关 取决于操作系统(32 位或 64 位)
usize 平台相关 取决于操作系统(32 位或 64 位)

说明

  1. i 表示有符号整型,可以表示正数和负数。
  2. u 表示无符号整型,只能表示非负数。
  3. isizeusize 是平台相关的整型类型:
    • 在 32 位系统上,它们分别是 32 位。
    • 在 64 位系统上,它们分别是 64 位。
  4. 默认类型:如果没有显式指定类型,Rust 会默认使用 i32 类型。
浮点型

Rust 的浮点数类型是 f32 和 f64,分别占 32 位和 64 位。默认类型是 f64

布尔型

布尔类型只有两个值:true 和 false。可以使用 bool 类型来声明

字符型

字符类型使用 char 类型来表示,表示单个 Unicode 字符。字符类型是四个字节(32 位),可以表示任何有效的 Unicode 字符,带变音符号的字母(Accented letters),中文、日文、韩文等字符,emoji(绘文字)以及零长度的空白字符都是有效的 char 值

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // with explicit type annotation
    let heart_eyed_cat = '😻';
}

复合类型

复合类型可以将多个值组合成一个值。Rust 中有两种基本的复合类型:元组和数组。

元组

元组是将多个值组合成一个复合类型。元组的元素可以是不同类型的。元组使用小括号 () 来表示,元素之间用逗号分隔。

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);

    let (x, y, z) = tup; // 支持解构

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

可以使用. 加上索引来访问元组的元素

数组

数组是固定长度的同类型元素的集合。数组使用方括号 [] 来表示,元素之间用逗号分隔。

fn main() {
    let a: [i32; 5] = [1, 2, 3, 4, 5]; //方括号中包含每个元素的类型,后跟分号,再后跟数组元素的数量
    let a = [3; 5]; // 创建一个包含 5 个元素的数组,每个元素的值都是 3
    let first = a[0]; // 访问数组的第一个元素
}

函数

定义

在 Rust 中通过输入 fn 后面跟着函数名和一对圆括号来定义函数。Rust 不关心函数定义所在的位置,只要函数被调用时出现在调用之处可见的作用域内就行。

在函数的签名中,必须声明每一个参数的类型。

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {value}{unit_label}");
}

语句和表达式

  • 语句:是执行一些操作但不返回值的指令。以分号结尾
  • 表达式:计算并产生一个值,可以嵌套。不会以分号结尾
fn main() {
    let y = {
        let x = 3;
        x + 1
    };

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

函数返回值

函数可以返回值,使用 -> 语法来指定返回值的类型。函数的返回值等同于函数体最后一个表达式的值。

fn five() -> i32 {
    5 // 注意这里没有分号,因为这是一个表达式
}

控制流

if

if 表达式用于条件判断。可以使用 else ifelse 来处理其他情况。

fn main() {
    let number = 6;
    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 or 3");
    }
}

注意的是代码中的条件必须是 bool 值。如果条件不是 bool 值,将会报错。Rust并不会尝试将其他类型转换为布尔值。

因为if是一个表达式,所以可以将其结果赋值给变量。

let condition = true;
let number = if condition { 5 } else { 6 }; // 注意这里的 else分支也必须返回相同类型的值(Rust需要在编译时就明确变量的类型)
println!("The value of number is: {number}");

loop

loop 用于创建无限循环。可以使用 break 退出循环,使用 continue 跳过当前迭代。

break 可以返回一个值作为循环的结果。

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {result}");
}

可以利用循环标签来标记循环,以便在嵌套循环中使用 breakcontinue 时指定要跳出或继续的循环。

fn main() {
    'outer: loop {
        println!("Entered the outer loop");
        loop {
            println!("Entered the inner loop");
            break 'outer; // 跳出外层循环
        }
        println!("This line will not be printed");
    }
    println!("Exited the outer loop");
}

while

while 循环在条件为 true 时执行。可以在循环体中使用 breakcontinue 来控制循环。

fn main() {
    let mut number = 3;
    while number != 0 {
        println!("{number}!");

        number -= 1;
    }
    println!("LIFTOFF!!!");
}

for

for 循环用于遍历集合或范围。可以使用 in 关键字来指定要遍历的集合或范围。

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {element}");
    }
}

所有权

所有权是 Rust 的核心概念之一。Rust 通过所有权系统来管理内存,确保内存安全和防止数据竞争。

  • rust 中的每个值都有一个所有者(变量)。
  • 每个值只能有一个所有者。
  • 当所有者离开作用域时,值会被自动释放。

变量的作用域

作用域是一个项在程序中的有效范围

    {                      // s 在这里无效,它尚未声明
        let s = "hello";   // 从此处起,s 是有效的

        // 使用 s
    }                      // 此作用域已结束,s 不再有效

以 String 类型为例

String在rust中被存储在堆上,是一个可变的、动态大小的字符串类型。

// 体现string类型的可变性
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() 在字符串后追加字面值
println!("{s}"); // 这将打印 `hello, world!`

内存和分配

当变量离开作用域,Rust 为我们调用一个特殊的函数。这个函数叫做 drop,在这里 String 的作者可以放置释放内存的代码。Rust 在结尾的 } 处自动调用 drop。

{
    let s = String::from("hello"); // s 进入作用域

    // 使用 s
}                                  // 这里,s 离开作用域并调用 `drop` 方法。内存被自动释放

移动的变量与数据

对于简单的标量值,Rust 会在赋值时复制值。

let x = 5;
let y = x; // 这里 x 被复制到 y,x 仍然有效
println!("x = {}, y = {}", x, y); // 输出: x = 5, y = 5

但是对于复杂的数据类型,比如 String,Rust 会在赋值时移动值。

let s1 = String::from("hello");
let s2 = s1; // 这里 s1 的值被移动到 s2
// println!("{}", s1); // 这行代码会报错,因为 s1 不再有效
println!("{}", s2); // 输出: hello

这是因为,string由三部分组成:指向数据的指针、数据的长度和数据的容量。简单地复制这三部分会导致两个变量指向同一块内存区域,从而引发双重释放的问题。

为了确保内存安全,在 let s2 = s1; 之后,Rust 认为 s1 不再有效,因此 Rust 不需要在 s1 离开作用域后清理任何东西。

    let s1 = String::from("hello");
    let s2 = s1;

    println!("{s1}, world!");
    // 这里会报错,因为 s1 的值已被移动到 s2,s1 不再有效

具体报错信息如下:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:15
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |
5 |     println!("{s1}, world!");
  |               ^^^^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let s2 = s1.clone();
  |                ++++++++

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

这类似于其他语言中的浅拷贝,但是 Rust 会使原有的变量无效,因此这个操作被称为“移动”(move)。

clone

如果想要深拷贝数据,可以使用 clone 方法。

let s1 = String::from("hello");
let s2 = s1.clone(); // 这里 s1 被深拷贝到 s2
println!("s1 = {}, s2 = {}", s1, s2); // 输出: s1 = hello, s2 = hello

会直接copy的数据类型:

  • 所有的整数类型,比如 u32
  • 布尔类型,bool,其值是 true 和 false
  • 所有的浮点类型,比如 f64
  • 字符类型,char
  • 元组,只要元组中的类型也都是可以 copy 的话
    • 例如,(i32, i32) 是可以 copy 的,但 (i32, String) 就不行,因为 String 不是 copy 的



    Enjoy Reading This Article?

    Here are some more articles you might like to read next:

  • notes of ML
  • notes of VCI
  • notes of AIP
  • notes of AI Math Fundamentals
  • notes of ICS