🌞Moon Will Know
💻

Hello Rust!

因为最近看到了很多关于 Rust 的项目,然后又了解到了 webassembly 可以使用 Rust 等语言编译为 wasm 供 javascript 使用,所以准备入门了解下。本文可以看作是我的读书笔记。

1.学习途径

2. 安装 rust

Rust 的源文件为 .rs 文件
  • 通过 homebrew 进行安装: brew install rust
  • 查看 Rust 版本:rustc -V
  • 编译 .rs 文件:rustc path

3.Hello World

Rust 的主程序通过 main 函数进行执行,所以 Rust 主程序的入口一般为
fn main() { println!("Hello world!"); }

4.Hello Cargo

绝大多数 Rust 项目使用 Cargo,官方安装包的一般自带了 Cargo
  • 查看 cargo 版本:cargo —version
  • 使用 cargo 创建项目:cargo new hello_cargo
  • 使用 cargo 构建项目:cargo build
  • 一步构建并运行项目:cargo run
  • 在不生成二进制文件的情况下构建项目来检查错误:cargo check

5.猜数字游戏

  • let 在 Rust 中定义变量的关键字,如果后续值需要可变则 使用 let mut 进行定义。
  • match 类似于 if,但需要为所有可能的结果设定处理程序。
  • 在 Cargo.toml 中引入外部包
    • [dependencies] 后使用 crateName = “version” 的格式引入,如
      rand = "0.8.3"
      然后重新构建项目 cargo build,或更新 crate cargo update

6.数据类型

Rust 是静态类型语言,在编译时就必须知道所有变量的类型,编译器通常可以通过值或者使用方式推断出我们想要的类型。可以通过这样为变量增加类型注解:
let guess: u32 = "42".parse().expect("Not a number!");

标量类型

标量scalar)类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。
  • 整型是一个没有小数部分的数字。在 Rust 中存在有符号(i)和无符号(u)两大类整型,有符号无符号代表数字能否为负值。
    • 有符号
      • i8
      • i16
      • i32
      • i64
      • i128
      • isize
      无符号
      • u8
      • u16
      • u32
      • u64
      • u128
      • usize
      字面量
      • Decimal (十进制)
      • Hex (十六进制)
      • Octal (八进制)
      • Binary (二进制)
      • Byte (单字节字符 仅限于u8)
  • 浮点型,Rust 的浮点数类型是 f32 和 f64,分别占 32 位和 64 位。默认类型是 f64,因为在现代 CPU 中,它与 f32速度几乎一样,不过精度更高。所有的浮点型都是有符号的。
  • 布尔型使用 bool 表示,有两个可能的值 truefalse
  • 字符类型使用 char 表示,是语言中最原生的字母类型。
  • 元组类型一旦声明,长度和成员位置不会再改变,使用包含在圆括号中的逗号分隔的值列表来创建一个元组,不同位置的成员类型不必相同。
    • 元组是一个单独的复合元素。为了从元组中获取单个值,可以使用模式匹配(pattern matching)来解构(destructure)元组值
      fn main() { // 定义一个元组 let tup: (i32, f64, u8) = (500, 6.4, 1); // 解构元组 let (x, y, z) = tup; // x: 500, y: 6.4, z: 1 }
      除了使用解构外,还可以使用点号(.)后跟索引值来直接访问。
      fn main() { let x: (i32, f64, u8) = (500, 6.4, 1); let five_hundred = x.0; let six_point_four = x.1; let one = x.2; }
  • 复合类型可以将多个值组合成一个类型,Rust 中有元组数组
    • 数组类型中的每个元素类型必须相同,且 Rust 中的数组长度是固定的。可以这样来定义数组的类型:
      • let a: [i32;5] = [1,2,3,4,5];
        可以通过在方括号中指定初始值和个数来创建一个每个元素都相同的数组:
        let a = [3;5]; // 和 let a = [3,3,3,3,3] 的作用相同
        可通过索引来访问数组元素,但是当索引超出数组的长度时,将会导致运行时错误。
        let a = [1,2,3,4,5]; let first = a[0]; // 1 let second = a[1]; // 2

7.函数

在 Rust 中函数通过 fn 关键字来定义,函数的参数和返回值定义为:
fn sum(a:i32,b:i32)->i32 { a + b }
语句是执行一些操作但不返回值的指令。表达式计算并产生一个值。
函数的返回值等同于函数体最后一个表达式的值(不加分号;),使用 return关键字和指定值,可从函数中提前返回;但大部分函数隐式的返回最后的表达式。

8. 注释

Rust 中 使用双斜杠进行注释。

9.控制流

根据条件是否为真来决定是否执行某些代码,以及根据条件是否为真来重复运行一段代码的能力是大部分编程语言的基本组成部分。Rust 代码中最常见的用来控制执行流的结构是 if表达式和循环。

if 表达式

表达式允许根据条件执行不同的代码分支。你提供一个条件并表示 “如果条件满足,运行这段代码;如果条件不满足,不运行这段代码。
let number = 3; if number < 0 { // ... do something } else if number > 10 { // ... do something } else { // ... do something }
因为 if 是一个表达式,所以可以在 let 语句中使用:
let number = 4; let number = if number < 3 { 3 } else { number };
当 if 用于赋值时,if 的每个分支的返回值都必须是相同类型,否则会出现错误。

循环

Rust 有三种循环:loopwhile和 for。用于多次执行同一段代码。
  • loop 关键字告诉 Rust 一遍又一遍地执行一段代码直到你明确要求停止。
    • break 关键字可以停止循环并返回值。
      • fn main () { let mut counter = 0; let result = loop { counter += 1; if counter == 10 { break counter * 2; } }; println!("The result is {result}"); }
    • continue 关键字告诉程序跳过这个循环迭代中的任何剩余代码,并转到下一个迭代。
    • 循环标签用于在嵌套循环时和 break continue 一起使用,用于指定要停止或退出的循环。
      • fn main() { 'loop_label' loop { // ... do something }; }
  • while 条件循环,当条件为真,执行循环。当条件不再为真,调用 break 停止循环。
    • fn main() { let mut number = 10; while number > 0 { number -= 1; } }
  • for 循环可以用来对集合的每一个元素执行一些代码。
    • fn main() { let a = [1,2,3,4,5]; for item in a { println!("the value is: {item}") } }

10.所有权

所有权(系统)是 Rust 最为与众不同的特性,对语言的其他部分有着深刻含义。它让 Rust 无需垃圾回收(garbage collector)即可保障内存安全,因此理解 Rust 中所有权如何工作是十分重要的。

所有权规则

  1. Rust 中的每一个值都有一个 所有者owner)。
  1. 值在任一时刻有且只有一个所有者。
  1. 当所有者(变量)离开作用域,这个值将被丢弃。

变量作用域

变量从声明的点开始直到当前 作用域 结束时都是有效的,当执行到作用域结尾时,会为变量调用drop方法。
{ // s 在这里无效, 它尚未声明 let s = "hello"; // 从此处起,s 是有效的 // 使用 s } // 此作用域已结束,s 不再有效

移动

在 Rust 中将一个变量的值赋给另一个变量时,对于基本数据类型,会在栈中放入两个相同的值。但对于复杂类型来说,这个操作将变为移动,即第一个变量自动失效,而第二个变量的指针指向内存。
fn main() { let s1 = String::from("hello"); let s2 = s1; println!("{}, world!", s1); // 报错 }

克隆

如果需要深度复制堆上的数据,则可以使用一个叫做 clone 的通用函数。
fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {}, s2 = {}", s1, s2); }

拷贝

对于只在栈上的数据,使用变量赋值方法,则不会使第一个值失效,可以实现 拷贝的类型有:
  • 所有整数类型,比如 u32
  • 布尔类型,bool,它的值是 true 和 false
  • 所有浮点数类型,比如 f64
  • 字符类型,char
  • 元组,当且仅当其包含的类型也都实现 Copy 的时候。比如,(i32, i32) 实现了 Copy,但 (i32, String) 就没有。

所有权和函数

向函数传递值可能会移动或者复制,就像赋值语句一样。

返回值和作用域

函数的返回值也可以转移所有权。
fn main() { let a = String::from("test"); let b =String::from("test2"); let (result,a) = get_length(a); let (result2,c) = get_length(b); println!("result is {},a is {}",result,a); println!("result2 is {},b is {}",result2, b); // 报错,b已经无效 } fn get_length (s:String) -> (String,usize) { let length = s.len(); (s,length) }

11.引用和借用

在 Rust 中调用函数,因为参数被移动到了函数内,所以必须返回和接受才能重复使用。则可以以一个对象的引用作为参数,而不是获取值的所有权。
fn main() { let a = String::from("test"); let result = get_length(&a); println!("result is {},a is {}",result,a); } fn get_length (s:&String) -> (String,usize) { let length = s.len(); length }
在这里我们向函数传递了 &a,函数也接受了 &String,这些 &符号就是 引用,它们允许你使用值但不获取其所有权。
函数签名中的&String 代表参数是一个引用,所以当引用停止时,它所指向的值也不会被丢弃。
借用是指创建一个引用的行为,尝试修改借用的变量时将会报错。

可变引用

使用 &mut 创建可变引用,在函数签名处也需要使用 &mut 使其接受一个可变引用,表明函数将改变借用的值。当有一个对该变量的可变引用时,就不能再创建对该变量的引用。也不可以在有一个变量的不可变引用的同时创建可变引用。
一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。编译器在作用域结束之前判断不再使用的引用的能力被称为 非词法作用域生命周期Non-Lexical Lifetimes,简称 NLL)。

垂悬引用

在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针,所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
fn main() { let reference_to_nothing = dangle(); } fn dangle() -> &String { // dangle 返回一个字符串的引用 let s = String::from("hello"); // s 是一个新字符串 &s // 返回字符串 s 的引用 } // 这里 s 离开作用域并被丢弃。其内存被释放。 // 危险!

12.Slice 类型

字符串 Slice

这是 String 中一部分值的引用,看起来像:
fn main() { let s = String::from("hello world"); let hello = &s[0..5]; let world = &s[6..11]; }
对于 Rust 的 .. range 语法,如果想要从索引 0 开始,可以不写两个点号之前的值。如果 slice 包含 String的最后一个字节,也可以舍弃尾部的数字。也可以同时舍弃这两个值来获取整个字符串的 slice。
fn main() { let s = String::from("hello"); let slice = &s[..2]; let slice = &s[1..]; let slice = &s[..]; }
Slice 将会保持引用在失效前一直存在,所以不能再次创建该值可变引用。

数组 Slice

fn main() { let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; assert_eq!(slice, &[2, 3]); }

13.结构体

struct,或者 structure,是一个自定义数据类型,允许你包装和命名多个相关的值,从而形成一个有意义的组合。类似于对象。
struct 的定义类似于 TS 中的interface,通过在花括号前添加 struct 名字可以使用结构体来创建实例。
从结构体中获取某个特定的值,可以通过点号,也可以通过点号为可变结构体的对应字段赋值。
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn main() { let user1 = User { email: String::from("[email protected]"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; user1.email = String::from("[email protected]"); }
结构体和 JS 类似的是,如果参数和字段名完全相同,可以简写。
fn build_user(email: String, username: String) -> User { User { email, username, active: true, sign_in_count: 1, } } fn main() { let user1 = build_user( String::from("[email protected]"), String::from("someusername123"), ); }

结构体更新语法

结构体更新语法,类似对象解构赋值方法,初始化结构体时,未被明确写出的字段可以使用 .. 语法(注意:不是 ...)使用已有实例中的值填充。
结构体更新语法类似于带有 = 的赋值,移动了数据,在创建 user2 后 user1 将不可用。如果只是使用更新语法更新了 Copy tarit 的类型,那么 user1 依然可用。
fn main() { let user1 = User { email: String::from("[email protected]"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; let user2 = User { email: String::from("[email protected]"), ..user1 // 除 email 字段外,其他字段都和 user1 保持一致 }; }

元组结构体

可以定义与元组类似的结构体,称为 元组结构体tuple structs)。元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。当你想给整个元组取一个名字,并使元组成为与其他元组不同的类型时,元组结构体是很有用的,这时像常规结构体那样为每个字段命名就显得多余和形式化了。
即使两个元组结构体的字段类型相同,但是在获取 Color 类型参数的函数不能使用 Point 类型作为参数
struct Color(i32, i32, i32); struct Point(i32, i32, i32); fn main() { let black = Color(0, 0, 0); let origin = Point(0, 0, 0); }

没有字段的类单元结构体

也可以定义一个没有任何字段的结构体!它们被称为 类单元结构体unit-like structs)因为它们类似于 (),即“元组类型”一节中提到的 unit 类型。
struct AlwaysEqual; fn main() { let subject = AlwaysEqual; }

dbg! 方法

之前经常使用的 println! 方法需要通过 println!("rect1 is {#?}", rect1); 才能打印出 struct 类型的实例。dbg! 方法接受一个表达式的所有权,打印出所在文件和行号以及表达式的结果,并返回所有权。

14. 结构体方法