rust 基础知识

282 阅读7分钟

变量

rust 中,变量分为可变变量和不可变变量

不可变变量和常量的区别是:

常量是在编译时就确定的,而不可变变量可以在运行时确定,常量后面可以跟上常量表达式

// 常量
const MAX_POINTS = 1 + 2 + 3;
// 不可变变量
let x = calculate();

隐藏

let x = 5;
let x = x + 1;
let x = x * 2;
println!("The value of x is: {}", x); // 12

溢出处理

rust 中如果两个数相加产生了溢出,在编译时会报错:attempt to add with overflow

如果想要忽略溢出,可以使用 wrapping_add 方法,如果想要检查溢出,可以使用 overflowing_add 方法

fn main() {
    let a: u32 = 4294967295;
    let b: u32 = 1;
    let sum = a.wrapping_add(b);
    println!("The sum of {} and {} is {}", a, b, sum);
    let (sum, is_overflow) = a.overflowing_add(b);
    println!(
        "The sum of {} and {} is {}, is_overflow: {}",
        a, b, sum, is_overflow
    );
}

元组

元组可以由不同类型的元素组成

let a = 10;
let b = true;

let tuple = (a, b);

获取元组中的元素有两种方式:

  • 使用模式匹配: let (c, d) = tuple;
  • 使用索引: let one = tuple.0;

使用索引访问不存在的元素会报错no field 2 on type ({integer}, bool)

let (c, d) = tuple;
println!("c = {}, d = {}", c, d);
let one = tuple.0;
let two = tuple.1;
println!("one = {}, two = {}", one, two);

获取元组的长度可以使用标准库 std::mem::size_of_val

let size = std::mem::size_of_val(&tuple);
println!("size = {}", size);

数组

数组中的元素必须是相同类型的

let array = [1, 2, 3, 4, 5];

访问数组中的元素:array[0],如果访问越界

在编译时报错:this operation will panic at runtime

在运行时报错:

thread 'main' panicked at src/main.rs:5:35:
index out of bounds: the len is 5 but the index is 5

如果数组中元素都是同一个,可以这样声明:

let array: [i32; 5] = [1; 5];
println!("{:?}", array);  // [1, 1, 1, 1, 1]

切片

切片是对一个数组(包括固定大小数组和动态数组)的引用片段,有利于安全有效地访问数组的一部分,而不需要拷贝数组或者数组中的内容

切片在编译的时候长度是未知的,在底层实现上,一个切片保存着两个 usize 成员,第一个 usize 成员指向切片起始位置,第二个 usize 成员表示切片长度

let arr = [1, 2, 3, 4, 5];
let slice = &arr[0..3];
println!("{:?}", slice); // [1, 2, 3]
println!("len: {:?}", slice.len()); // 3
println!("slice[0]: {:?}", slice[0]); // 1

如果是一个可变的 slice,使用 &mut 声明,当改了 slice 中的元素,arr 相对应的元素也会被修改

let mut arr = [1, 2, 3, 4, 5];
let slice = &mut arr[..];
slice[0] = 10;
println!("slice[0]: {:?}", slice[0]); // 10
println!("arr[0]: {:?}", arr[0]); // 10

结构体

元组结构:

struct Pair(i32, f32);
let pair = Pair(1, 1.0);
println!("{}", pair.0); // 1

常规结构:

struct Point {
    x: i32,
    y: i32,
}
let point = Point { x: 1, y: 1 };
println!("{}", point.x); // 1
println!("{}", point.y); // 1

单元结构(无字段,通常在泛型里使用较多):

struct Unit;
let unit = Unit;

在开发环境下可以使用 #[derive(Debug)] 注解来自动生成 Debug trait 的实现,方便调试

#[derive(Debug)]
struct Person {
    name: String,
    age: u8,
}
fn main() {
    let person = Person {
        name: String::from("Alice"),
        age: 30,
    };
    println!("{:?}", person);
}

枚举

无参枚举:

enum Planent{
    Mars,
    Earth,
}

带枚举值的枚举:

enum Color {
    Red = 0xff0000,
    Green = 0x00ff00,
    Blue = 0x0000ff,
}

带参数的枚举:

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8),
}

模式匹配,如果不是 IpAddr::V4 类型,会执行 _ 分支

let localhost = IpAddr::V4(127, 0, 0, 1);

match localhost {
    IpAddr::V4(a, b, c, d) => {
        println!("localhost: {}.{}.{}.{}", a, b, c, d);
    }
    _ => {
        println!("Not a V4 address");
    }
}

模式匹配可以使用 if let 语法糖

if let IpAddr::V4(a, b, c, d) = localhost {
    println!("localhost: {}.{}.{}.{}", a, b, c, d);
} else {
    println!("Not a V4 address");
}

类型转换

rust 不支持隐式类型转换,只能使用 as 关键字进行显式类型转换

let a: i32 = 1;
let b: i64 = a as i64;

数值转换的语义:

  • 两个相同大小的整型直接的转换是一个 (no-op),例如 i32 -> u32
  • 从一个大的整型转换到一个小的整型,会进行截断,例如 i32 -> i8
  • 从一个小的整型转换到一个大的整型,会进行符号扩展,例如 i8 -> i32
    • 如果源类型是无符号会补零
    • 如果源类型是有符号会补符号位
  • 从一个浮点转换为一个整型会向 0 舍入
  • 从一个整型转换为一个浮点会产生整型的浮点表示,如有必要会舍入(未指定舍入策略)
  • f32 转换为 f64 是完美无缺的
  • f64 转换为 f32 会产生最接近的可能值(未指定舍入策略)

for 循环

1..51..=5 的区别是,前者是左闭右开区间,后者是左闭右闭区间

fn main() {
    for i in 1..=5 {
        println!("Hello, World! {}", i);
    }
}
// Hello, World! 1
// Hello, World! 2
// Hello, World! 3
// Hello, World! 4
// Hello, World! 5

.iter 方法返回一个迭代器,.iter_mut 方法返回一个可变的迭代器

let mut arr = [1, 2, 3, 4, 5];
for i in arr.iter_mut() {
    *i = *i * 2;  // 修改 arr 中的元素
}
for i in arr.iter() {
    println!("Hello, World! {}", i);
}
// Hello, World! 2
// Hello, World! 4
// Hello, World! 6
// Hello, World! 8
// Hello, World! 10

方法

rust 中构造方法和普通方法没啥区别,只是约定为 new

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}
impl Point {
    fn new(x: i32, y: i32) -> Point {
        Point { x, y }
    }
}
fn main() {
    let point = Point::new(1, 2);
    println!("{:?}", point)     // Point { x: 1, y: 2 }
}

结构体中的方法可以通过 self 访问自身的数据

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}
impl Point {
    fn new(x: i32, y: i32) -> Point {
        Point { x, y }
    }
    fn get_x(&self) -> i32 {
        self.x
    }
}
fn main() {
    let point = Point::new(1, 2);
    println!("{:?}", point.get_x())   // 1
}

调用 Point::new() 是两个冒号,但是 point.get_x() 是点,这是为什么呢?

因为调用实例化后的方法用 .,直接调用结构体上的方法用 ::

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}
impl Point {
    fn new(x: i32, y: i32) -> Point {
        Point { x, y }
    }
    fn set_x(&mut self, x: i32) {
        self.x = x;
    }
    fn get_x(&self) -> i32 {
        self.x
    }
}
fn main() {
    let mut point = Point::new(1, 2);
    println!("{:?}", point.get_x());    // 1
    point.set_x(3);
    println!("{:?}", point.get_x());    // 3
}

修改结构体上的属性,在 set_x 方法上要加上 &mut self,在声明 point 时也要用 mut

闭包

闭包可以保存在变量中

let times = |n: u32| -> u32 { n * 3 };
println!("{}", times(5));  // 15

应用,使用 move 将主线程的变量移动到子线程中

use std::thread;
fn main() {
    let hello = "Hello, World!";

    thread::spawn(move || {
        println!("{}", hello);
    })
    .join() // 除非主线程退出,否则会一直等待
    .unwrap();
}

发散函数

发散函数永远不会被返回,它们的返回被标记为 !,这是一个空类型

fn foo() -> ! {
    panic!("This call never returns.");
}

发散函数与空返回值函数不同,空返回值的函数可以被返回

fn some_fn() {
  ()
}

发散函数最大的用处是通过 rust 类型检查系统

fn main() {
    let a = if false { 1 } else { foo() };
    println!("a: {}", a);
}

fn foo() -> ! {
    panic!("This call never returns.");
}

参数游戏

rust 中,标准库并没有直接给出随机数的 api,需要使用第三方库 rand

Cargo.toml 中添加依赖

[dependencies]
rand = "*"

导入 rand 需要导入 trait,也就是 rand::Rng

rand::thread_rng() 作用是返回一个线程本地的随机数生成器,这是一个线程安全的随机数,每个线程有自己的生成器

expectunwrap 的区别:

  • expect:可以自定义错误信息
  • unwrap:使用默认的错误信息

比较表达式:

guess.cmp(&secret_number) 会返回一个 Ordering 枚举值,用来比较两个数之间的大小,match 语句会根据枚举值执行相应的代码分支

match guess.cmp(&secret_number) {
    Ordering::Less => println!("Too small!"),
    Ordering::Greater => println!("Too big!"),
    Ordering::Equal => {
        println!("You win!");
        break;
    }
}

如果是小于等于可以写成:

match guess.cmp(&secret_number) {
    Ordering::Less | Ordering::Equal => println!("Too small!"),
    Ordering::Greater => println!("Too big!"),
    Ordering::Equal => {
        println!("You win!");
        break;
    }
}

猜数游戏源码:

use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..101);

    loop {
        println!("Please input your guess.");
        let mut guess = String::new();

        io::stdin().read_line(&mut guess).expect("Failed to read line");

        // io::stdin().read_line(&mut guess).unwrap();

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}