Rust入门 - 语言特性

306 阅读7分钟

1. 概述

Rust是一门编译型的语言(AOT - ahead of time),生成可执行代码前需要先编译,这是和 JavaScript 等解释型语言根本上的区别。

2. 变量与可变性

Rust中通过let来声明变量,但let声明的变量默认是不可变(Immutable)变量

let 声明变量后尝试修改它的值,编译时就会报错。

fn main() {
    let a = 5;
    a = 6; // error: cannot assign twice to immutable variable
}

我们可以使用以下两种方式来解决这个问题

  • let 声明变量时使用 mut 关键字进行修饰,表示它是一个可变变量。需要注意的是,Rust是强类型语言,所以即使声明为可变变量,也只能重新赋值为相同数据类型的值
fn main() {
    let mut a = 5;
    a = 6;
    // 改变数据类型,编译报错
    a = "6"; // error: expected integer, found `&str`
}
  • 使用 shadowing 特性,再次声明此变量覆盖之前的值,并且不受之前数据类型的限制。相当于重新声明后,之前的变量就被隐藏了
fn main() {
    let a = '5';   // '5'
    let a = 5;     // 5
    let a = a + 1; // 6
    let a = a * 2; // 12
}

3. 常量

Rust中通过 const 关键字声明常量,常量与变量的区别是

  • 不可以使用 mut 关键字
  • 常量在声明时必须指定数据类型
  • 常量可以在任何作用域声明,包括全局作用域
  • 常量只可以绑定到常量表达式

命名规范:字母全部大写,单词之间通过 _ 连接,如

const MAX_AGE: u32 = 10_0000;

4. 数据类型

4.1 标量类型

4.1.1 整数类型

  • 有符号整数使用 i 开头 [(2n1),2n11][-(2^n-1) , 2^{n-1}-1]
  • 无符号整数使用 u 开头[0,2n1][0 , 2^n - 1]
  • isize和usize类型的位数是由程序运行的计算机的架构所决定,运行在64位的计算机上,则是64位的
  • 整数的默认类型为 i32

截屏2022-02-01 下午9.48.42.png

4.1.1.1 整数字面值

  • 16进制使用 0x 开头
  • 8进制使用 0o 开头
  • 2进制使用 0b 开头
  • byte类型的数据类型仅限 u8,使用 b 开头

截屏2022-02-01 下午10.10.27.png

除了 byte 类型外,所有数值的字面值都允许使用类型后缀,如

// 表示 u8 类型的数值 57
let a = 57u8;

4.1.1.2 整数溢出

如果把一个 u8(0-255) 类型的变量设置为 256,会有一下情况:

  • 开发模式下,Rust检测到溢出,在程序运行时就会 panic
  • 发布模式下,Rust不会检测溢出,当发生溢出时会执行环绕操作:
    • 256 -> 0
    • 257 -> 1
    • ...

4.1.2 浮点类型

  • f32,单精度
  • f64,双精度(Rust浮点数的默认类型,因为在现代CPU上 f64 和 f32 的速度差不多)
  • Rust 浮点类型使用了 IEEE-754 标准

4.1.3 布尔类型

  • 占用一个字节
  • 符号为 bool,值为 true | false

4.1.4 字符类型

  • 符号为 char
  • 字面值使用单引号

4.2 复合类型

4.2.1 元组(tuple)

  • 声明后长度不可变
  • 可将不同数据类型的值组合到一起
// 声明一个元组
let tup: (i32, f64, u8) = (500, 5.6, 23);

// 获取元组成员

// 1.解构
let (x, y, z) = tup;
println!("{}, {}, {}", x, y, z); // 500, 5.6, 23

// 2.点标记法
println!("{}, {}, {}", tup.0, tup.1, tup.2); // 500, 5.6, 23

4.2.2 数组

  • 和 tuple 一样,声明后长度不可变
  • 数组成员的数据类型必须相同
  • 数组是存在栈内存中
  • 数组的类型通过 [type; length] 的形式表示
let arr: [i32; 5] = [1,2,3,4,5];

// 特殊的数组声明方法
let sameMemberArray = [3; 5]; // [3,3,3,3,3]

// 访问数组成员
let f = arr[0]; // 1

// 访问数组越界 - rust编译器会进行简单的越界检查
let more = arr[6]; // 编译报错

// 访问数组越界 - rust编译器检测不到的场景
let temp = [6,7,8,9];
let more_temp = arr[temp[0]]; // 编译通过,运行时报错

5. 函数

  • Rust中使用 fn 关键字声明函数
  • 对于函数和变量名,使用 snake case 规范来命名 (单词小写,使用 _ 拼接)
  • 参数在定义时必须指定数据类型
  • 如果要提前返回一个值,则使用 return 关键字
  • 函数默认返回值是一个空元组
fn main() {
    let number = get_number(5);
    
    println!("{}", number); // 10
}

fn get_number(a: i32) -> i32 {
    // 函数中最后行如果是表达式,那么表达式的值则会作为函数的返回值
    // 如果行尾加了分号,则会被识别为语句,就不会被作为函数的返回值
    a + 5
}

6. 控制流

6.1 条件分支 - if

  • 需要注意的是,每个分支块如果有返回值,必须保证数据类型相同
let a = 3;

// if else
if a == 3 {
    println!("a is 3");
} else {
    println!("a is not 3");
}

// 使用 表达式 的特性达到其他语言中 三元表达式 的效果
let b = if a == 3 { 5 } else { 6 }; // 5 

6.2 循环

  • loop
    • loop 会无限循环执行循环体中的代码,直到被 break 中断
    • break 可以为 loop 循环的表达式提供一个返回值
// loop 循环
let mut count = 0;

let value = loop {
    count += 1;
    
    if count == 10 {
    	break count * 2;
    }
}

println!("{}", value); // 20
  • while
let arr = [1,2,3,4,5];
let mut index = 0;

// 使用 while 循环遍历数组
while index < 5 {
    println!("{}", arr[index]);
    
    index = index + 1;
}


  • for
let arr = [1,2,3,4,5];

for item in arr.iter() {
    println!("for item {}", item);
}

// 使用 range 实现指定次数的循环
// 1. (1..5) -> 一个包含 1,2,3,4 的可迭代器
// 2. 使用 rev 方法进行反转
for item in (1..5).rev() {
    println!("range item is {}", item)
}

7. 所有权

所有权是 Rust 无需 GC 就能保证内存安全的核心特性

7.1 内存和分配

当变量走出作用域范围后,Rust会自动调用 drop 函数将内存空间交还给操作系统

7.2 Stack上的数据复制:copy

  • 对于简单的标量数据类型,以下代码最终会向 stack 中压入两个 5
    • 本质上是因为标量类型实现了 Copy trait
    • Copy 这个 trait 用于类似整数这种存放在 stack 上的数据类型,需要分配内存的数据类型都不能实现这个 trait
    • 如果实现了 Copy trait,那么旧变量在赋值后任然可以使用
    • 如果一个类型实现了 Drop trait,那 Rust 就不允许再实现 Copy trait 了
    • 拥有 Copy trait 的数据类型
      • 整数
      • 浮点数
      • 布尔
      • 字符
      • 元祖 (需要满足成员都是拥有 Copy trait 的数据类型)
let x = 5;
let y = x;

println!("{}, {}", x, y); // 5, 5

7.3 变量和数据交互的方式:move

  • 对应长度未知的复合数据类型,将一个变量赋给另一个变量后,前者就会失效(在 Rust 中被称作 move,即 原先 s1 指向的内存空间移动到了 s2,完成移动后,s1 便失效了,由此来避免 s1 和 s2 走出作用域时对同一内存空间产生两次释放操作)
    • 二次释放(double free)在其他需要手动控制内存的语言中是严重的bug,可能释放掉正在被其他程序所使用的内存,导致未知的问题
let s1 = String::from("haha");
let s2 = s1;

println!("{}", s1); // error: value borrowed here after move

7.4 所有权与函数

  • 其实把值传递给函数和变量的情况是类似的,将发生 move 或 copy
fn main() {
    let string = String::from("hello");

    // Move,所有权发生移动,传入到了函数作用域中
    move_case(string);

    /*
     * 在调用 move_case 时,string指向的内存空间 的所有权发生了 move,
     * 当 move_case 调用完毕时,string指向的内存空间已经被释放了,
     * 所以之后再访问 string,编译时就会报错
     */
    // println!("{}", string); // value borrowed here after move

    // ---------------------------------------------------------------------

    let number = 12;

    // Copy,传入 number 值的副本
    copy_case(number);

    /*
     * number 是简单的标量类型,实现了 Copy trait
     * 在调用 copy_case 时,仅仅是传入了一个副本,
     * 所以后续任然可以继续使用
     */
    println!("{}", number);

    // ---------------------------------------------------------------------

    let bar = String::from("bar");

    /*
     * 在以下函数的调用过程中,bar 指向的内存空间的 所有权 被移动到了 函数作用域 中,
     * take_ownership_and_return 这个函数的作用是得到一个内存空间的所有权并将其返回,
     * 最终 foo 拿到了该内存空间的 所有权,其实这段代码的效果与 let foo = bar; 相同
     */
    let foo = take_ownership_and_return(bar);

    println!("{}", foo)
}

fn move_case(string: String) {
    println!("{}", string);
}

fn copy_case(number: u32) {
    println!("{}", number);
}

fn take_ownership_and_return(s: String) -> String {
    s
}

8. Stack & Heap

  • stack 是连续的内存空间
  • heap 是散列的内存空间,指向 heap内存 的指针是存储在 stack 中的
  • 所有存储在 stack 上的数据必须拥有已知的大小,编译时大小未知的数据或运行时大小可能发生变化的数据必须存储在 heap 上