Rust常见编程概念

发布时间 2024-01-05 11:27:10作者: 二次元攻城狮


本文大部分内容来自官方文档,对于一些基础常识的内容做了删减,适合已经掌握其它编程语言的人查阅,原文档内容参考常见编程概念

变量和可变性

变量默认是不可变的,可以在变量名前添加 mut 来使其可变。
示例代码:

fn main() {
    //let x = 5; 默认不可变,编译报错
    let mut x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}

常量

类似于不可变变量,常量 (constants)是绑定到一个名称的不允许改变的值,常量与变量的区别:

  • 不允许对常量使用 mut,常量不光默认不可变,它总是不可变。
  • 声明常量使用 const 关键字而不是 let,并且必须注明值的类型
  • 常量可以在任何作用域中声明,包括全局作用域。
  • 常量只能被设置为常量表达式,而不可以是其他任何只能在运行时计算出的值。

示例代码:

const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

Rust 对常量的命名约定是在单词之间使用全大写加下划线,编译器能够在编译时计算一组有限的操作。
在声明它的作用域之中,常量在整个程序生命周期中都有效,将硬编码值声明为常量有助于后期代码维护。

隐藏

可以用相同变量名称来隐藏一个变量,以及重复使用 let 关键字来多次隐藏,如下所示:

fn main() {
    //将 x 绑定到值 5 上
    let x = 5;

    //创建了一个新变量 x
    let x = x + 1;

    {
        //创建了一个新变量 x
        let x = x * 2;
        println!("The value of x in the inner scope is: {x}");
        //输出 The value of x in the inner scope is: 12
    }

    println!("The value of x is: {x}");
    //输出 The value of x is: 6
}

隐藏与将变量标记为 mut 是有区别的

  • 对变量重新赋值时不使用 let 关键字会导致编译时错误,通过使用 let 可以用这个值进行一些计算,计算完之后变量仍然是不可变的
  • 当再次使用 let 时,实际上创建了一个新变量,我们可以改变值的类型,并且复用这个名字,例如:
//字符串类型
let spaces = "   ";
//数字类型
let spaces = spaces.len();

隐藏使我们不必使用不同的名字,如 spaces_str 和 spaces_num。

下面这种代码会报错,不能改变变量的类型:

let mut spaces = "   ";
spaces = spaces.len();

注:不要滥用隐藏特性,被隐藏变量和新变量尽量处于同一个作用域

数据类型

每一个值都属于某一个 数据类型(data type),有两类数据类型子集:标量(scalar)复合(compound)

Rust 是 静态类型(statically typed)语言,编译时就必须知道所有变量的类型。
根据值及其使用方式,编译器通常可以推断出我们想要用的类型:
image

当多种类型均有可能时必须增加类型注解,例如:

let guess: u32 = "42".parse().expect("Not a number!");

标量类型

标量(scalar)类型代表一个单独的值,Rust 有四种基本的标量类型:整型浮点型布尔类型字符类型

整型

整数 是一个没有小数部分的数字,Rust 内建的整数类型如下:

长度 有符号 无符号
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch isize usize

可以使用其中的任一个来声明一个整数值的类型,类型取值范围如下(n 是变体使用的位数):

  • 有符号的变体可以储存包含从 -(2ⁿ⁻¹) 到 2ⁿ⁻¹ - 1 的数字,如 i8 可以储存从 -(2⁷) 到 2⁷ - 1 的数字,也就是从 -128 到 127。
  • 无符号的变体可以储存从 0 到 2ⁿ - 1 的数字,如 u8 可以储存从 0 到 2⁸ - 1 的数字,也就是从 0 到 255。
  • isize 和 usize 类型依赖运行程序的计算机架构:64 位架构上它们是 64 位的,32 位架构上它们是 32 位的。

可以使用下表中的任何一种形式编写数字字面值:

数字字面值 例子
Decimal (十进制) 98_222
Hex (十六进制) 0xff
Octal (八进制) 0o77
Binary (二进制) 0b1111_0000
Byte (单字节字符)(仅限于u8) b'A'
  • 可以是多种数字类型的数字字面值允许使用类型后缀,例如 57u8 来指定类型;
  • 允许使用 _ 做为分隔符以方便读数,例如1_000 的值与 1000 相同。

如果不知道使用哪种类型的数字,可以使用 Rust 的默认类型,数字类型默认是 i32isize 或 usize 主要作为某些集合的索引

关于整型溢出的处理:

  • 当在 debug 模式编译时,Rust 检查这类问题并使程序 panic,这个术语被 Rust 用来表明程序因错误而退出。
  • 使用 --release flag 在 release 模式中构建时,Rust 不会检测会导致 panic 的整型溢出。发生整型溢出时会进行二进制补码操作,值 256 变成 0,值 257 变成 1,依此类推。

为了显式地处理溢出的可能性,可以使用这几类标准库提供的原始数字类型方法:

  • 所有模式下都可以使用 wrapping_* 方法进行 wrapping,如 wrapping_add
  • 如果 checked_* 方法出现溢出,则返回 None值
  • 用 overflowing_* 方法返回值和一个布尔值,表示是否出现溢出
  • 用 saturating_* 方法在值的最小值或最大值处进行饱和处理

浮点型

Rust 也有两个原生的 浮点数(floating-point numbers)类型,分别是 f32f64,分别占 32 位和 64 位,默认类型是 f64

fn main() {
    let x = 2.0; // f64
    let y: f32 = 3.0; // f32
}

浮点数采用 IEEE-754 标准表示,f32 是单精度浮点数,f64 是双精度浮点数

注:Rust 中的所有数字类型都支持基本数学运算:加法、减法、乘法、除法和取余,整数除法会向零舍入到最接近的整数

布尔型

Rust 中的布尔类型有两个可能的值:true 和 false,使用 bool 表示:

fn main() {
    let t = true;
    let f: bool = false; 
}

字符类型

Rust 的 char 类型是语言中最原生的字母类型:

fn main() {
    //单引号声明 char 字面量,双引号声明字符串字面量
    let c = 'z';
    let z: char = 'ℤ'; 
    let heart_eyed_cat = '?';
}

Rust 的 char 类型的大小为四个字节 (four bytes),并代表了一个 Unicode 标量值(Unicode Scalar Value),Unicode 标量值包含从 U+0000 到 U+D7FF 和 U+E000 到 U+10FFFF 在内的值。带变音符号的字母(Accented letters)、中文、日文、韩文、emoji(绘文字)以及零长度的空白字符都是有效的 char 值。

复合类型

复合类型(Compound types)可以将多个值组合成一个类型,Rust 有两个原生的复合类型:元组(tuple)数组(array)

元组类型

元组是一个将多个其他类型的值组合进一个复合类型的主要方式,元组长度固定:一旦声明,其长度不会增大或缩小。

使用包含在圆括号中的逗号分隔的值列表来创建一个元组,下面的例子使用了可选的类型注解:

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

为了从元组中获取单个值,可以使用模式匹配(pattern matching)解构(destructure)元组值:

fn main() {
    let tup = (500, 6.4, 1);
    let (x, y, z) = tup;
    println!("The value of y is: {y}");
}

也可以使用点号(.)后跟值的索引来直接访问它们:

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;
}

不带任何值的元组叫做 单元(unit) 元组,这种值以及对应的类型都写作 (),表示空值或空的返回类型。如果表达式不返回任何其他值,则会隐式返回单元值。

数组类型

数组中的每个元素的类型必须相同且长度固定,将数组的值写成在方括号内并用逗号分隔:

fn main() {
    let a = [1, 2, 3, 4, 5];
}

在Rust中,数组默认是分配在栈上的,而不是堆上。数组并不如 vector 类型灵活,当不确定应该使用数组还是 vector 的时,通常应该使用 vector。
注:vector 类型是标准库提供的一个 允许 增长和缩小长度的类似数组的集合类型。

当你确定元素个数不会改变时,数组会更有用。例如月份:

let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];

可以像这样编写数组的类型:在方括号中包含每个元素的类型,后跟分号,再后跟数组元素的数量:

//i32 是每个元素的类型,数字 5 表明该数组包含五个元素。
let a: [i32; 5] = [1, 2, 3, 4, 5];

还可以通过在方括号中指定初始值加分号再加元素个数的方式来创建一个每个元素都为相同值的数组:

//变量名为 a 的数组将包含 5 个元素,这些元素的值最初都将被设置为 3
let a = [3; 5];

访问数组元素

数组是可以在栈 (stack) 上分配的已知固定大小的单个内存块,可以使用索引来访问数组的元素:

fn main() {
    let a = [1, 2, 3, 4, 5];
    let first = a[0];
    let second = a[1];
}

无效的数组元素访问

如果访问数组结尾之后的元素会发生什么,比如执行以下代码:

use std::io;

fn main() {
    let a = [1, 2, 3, 4, 5];
    println!("Please enter an array index.");
    let mut index = String::new();
    io::stdin()
        .read_line(&mut index)
        .expect("Failed to read line");
    let index: usize = index
        .trim()
        .parse()
        .expect("Index entered was not a number");
    let element = a[index];
    println!("The value of the element at index {index} is: {element}");
}

如果输入一个超过数组末端的数字(如 10),程序在索引操作中使用一个无效的值时导致 运行时 错误,程序带着错误信息退出并且不会执行最后的 println! 语句。

当尝试用索引访问一个元素时,Rust 会检查指定的索引是否小于数组的长度,如果索引超出了数组长度 Rust 会 panic

函数

Rust 代码中的函数和变量名使用 snake case (蛇式)规范风格,所有字母都是小写并使用下划线分隔单词:

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

fn another_function() {
    println!("Another function.");
}

在 Rust 中通过输入 fn 后面跟着函数名和一对圆括号来定义函数,大括号告诉编译器哪里是函数体的开始和结尾。
Rust 不关心函数定义所在的位置,只要函数被调用时出现在调用之处可见的作用域内就行。

参数

可以定义为拥有 参数(parameters)的函数,参数是特殊变量,是函数签名的一部分。当函数拥有参数(形参)时,可以为这些参数提供具体的值(实参)

fn main() {
    another_function(5);
}

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

在函数签名中,必须 声明每个参数的类型。要求在函数定义中提供类型注解,意味着编译器再也不需要你在代码的其他地方注明类型来指出你的意图,编译器也能够给出更有用的错误消息。

当定义多个参数时,使用逗号分隔:

fn main() {
    print_labeled_measurement(5, 'h');
}

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

语句和表达式

函数体由一系列的语句和一个可选的结尾表达式构成。 Rust 是一门基于表达式(expression-based)的语言,这是一个需要理解的(不同于其他语言)重要区别,其他语言并没有这样的区别。

  • 语句(Statements)是执行一些操作但不返回值的指令。
  • 表达式(Expressions)计算并产生一个值。

实际上,我们已经使用过语句和表达式。使用 let 关键字创建变量并绑定一个值是一个语句。在列表 3-1 中,let y = 6; 是一个语句。

使用 let 关键字创建变量并绑定一个值是一个语句,函数定义本身也是语句:

fn main() {
    let y = 6;
}

语句不返回值,不能把 let 语句赋值给另一个变量:

fn main() {
    let x = (let y = 6);
}

这与其他语言不同,例如 C 和 C#,它们的赋值语句会返回所赋的值。在这些语言中,可以这么写 x = y = 6,这样 x 和 y 的值都是 6;Rust 中不能这样写。

表达式会计算出一个值,并且大部分 Rust 代码是由表达式组成的。

表达式可以是语句的一部分(如 let y = 6),函数调用、宏调用、用大括号创建的一个新的块作用域都是一个表达式:

fn main() {
    let y = {
        let x = 3;
        x + 1  //这里没有分号
    };
    println!("The value of y is: {y}");
}

表达式的结尾没有分号,如果在表达式的结尾加上分号,它就变成了语句,而语句不会返回值。

具有返回值的函数

函数可以向调用它的代码返回值,需要在箭头(->)后声明它的类型:

fn five() -> i32 {
    5 //如果添加分号表示不返回值,与函数定义相矛盾,程序会报错无法通过编译
}

fn main() {
    let x = five(); //等价于 let x = 5;
    println!("The value of x is: {x}");
}

在 Rust 中,函数的返回值等同于函数体最后一个表达式的值使用 return 关键字和指定值可从函数中提前返回;但大部分函数隐式的返回最后的表达式。

注释

注释与C++、C#语言大致相同,有单行注释(//)、多行注释(/**/)。不同的是 Rust 还有另一种注释,称为文档注释

控制流

Rust 代码中最常见的用来控制执行流的结构是 if 表达式和循环

if 表达式

if 表达式允许根据条件执行不同的代码分支:

fn main() {
    let number = 3;
    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

所有的 if 表达式都以 if 关键字开头,其后跟一个条件,if 表达式中与条件关联的代码块有时被叫做 arms。
可以包含一个可选的 else 表达式来提供一个在条件为 false 时应当执行的代码块。

代码中的条件 必须 是 bool 值,如果条件不是 bool 值,将无法通过编译:

fn main() {
    let number = 3;
    //Rust 会抛出了一个错误
    if number {
        println!("number was three");
    }
}

不像 Ruby 或 JavaScript 这样的语言,Rust 并不会尝试自动地将非布尔值转换为布尔值,必须总是显式地使用布尔值作为 if 的条件

使用 else if 处理多重条件

可以将 else if 表达式与 if 和 else 组合来实现多重条件:

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 if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

使用过多的 else if 表达式会使代码显得杂乱无章,如果有多于一个 else if 表达式最好重构代码,比如使用另一个强大的 Rust 分支结构match

在 let 语句中使用 if

因为 if 是一个表达式,可以在 let 语句的右侧使用它:

fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };
    println!("The value of number is: {number}");
}

number 变量将会绑定到表示 if 表达式结果的值上, if 的每个分支的可能的返回值都必须是相同类型。

如果它们的类型不匹配,则会编译出错:

fn main() {
    let condition = true;
    //错误:if 和 else 分支的值类型是不相容的
    let number = if condition { 5 } else { "six" };
    println!("The value of number is: {number}");
}

使用循环重复执行

多次执行同一段代码是很常用的,一个循环执行循环体中的代码直到结尾并紧接着回到开头继续执行,Rust 有三种循环:loop、while 和 for

使用 loop 重复执行代码

loop 关键字告诉 Rust 一遍又一遍地执行一段代码直到你明确要求停止:

fn main() {
    loop {
        println!("again!");
    }
}

大部分终端都支持一个快捷键ctrl-c,来终止一个陷入无限循环的程序。

Rust 提供了一种从代码中跳出循环的方法,可以使用 break 关键字来告诉程序何时停止循环。
循环中的 continue 关键字告诉程序跳过这个循环迭代中的任何剩余代码,并转到下一个迭代。

从循环返回值

如果将返回值加入用来停止循环的 break 表达式,它会被停止的循环返回:

fn main() {
    let mut counter = 0;
    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2;
        }
    };
    //result 的值是 20
    println!("The result is {result}");
}

循环标签:在多个循环之间消除歧义

可以选择在一个循环上指定一个 循环标签(loop label),然后将标签与 break 或 continue 一起使用,使这些关键字应用于已标记的循环而不是最内层的循环:

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

        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;               //将只退出内层循环
            }
            if count == 2 {
                break 'counting_up;  //将退出外层循环
            }
            remaining -= 1;
        }

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

while 条件循环

在程序中计算循环的条件也很常见,Rust 内置了 while 循环

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

当条件为 true 就执行,否则退出循环。

使用 for 遍历集合

可以使用 for 循环来对一个集合的每个元素执行一些代码:

fn main() {
    let a = [10, 20, 30, 40, 50];
    for element in a {
        println!("the value is: {element}");
    }
}

for 循环增强了代码安全性,并消除了可能由于超出数组的结尾或遍历长度不够而缺少一些元素而导致的 bug。

for 循环的安全性和简洁性使得它成为 Rust 中使用最多的循环结构,即使是在想要循环执行代码特定次数时,也可以使用 for 循环:

fn main() {
    for number in (1..4).rev() {
        println!("{number}!");
    }
    println!("LIFTOFF!!!");
}

注:上面使用 Range,并使用 rev 方法进行了反转。Range是标准库提供的类型,用来生成从一个数字开始到另一个数字之前结束的所有数字的序列