MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Rust基元类型详解

2021-01-154.0k 阅读

Rust 基元类型概述

Rust 作为一种现代系统编程语言,拥有丰富且强大的基元类型,这些类型构成了 Rust 编程的基础。基元类型是 Rust 语言内置的基本数据类型,它们直接由语言提供,无需额外引入库即可使用。这些类型涵盖了整数、浮点数、布尔值、字符、元组、数组等,每种类型都有其独特的特性和用途。

在 Rust 中,基元类型的设计旨在提供高效、安全且灵活的编程体验。例如,整数类型具有明确的大小和符号属性,这使得开发者能够精确控制内存使用和数值范围;而 Rust 的类型系统会在编译时进行严格检查,确保类型的安全性,避免许多在其他语言中常见的运行时类型错误。

整数类型

整数类型的分类

Rust 提供了多种整数类型,根据其大小和是否有符号可分为不同类别。有符号整数类型能够表示正数、负数和零,而无符号整数类型只能表示零和正数。

  1. 有符号整数类型

    • i8:8 位有符号整数,取值范围为 -128 到 127。
    • i16:16 位有符号整数,取值范围为 -32768 到 32767。
    • i32:32 位有符号整数,取值范围为 -2147483648 到 2147483647。
    • i64:64 位有符号整数,取值范围为 -9223372036854775808 到 9223372036854775807。
    • i128:128 位有符号整数,取值范围极大,在处理高精度数值时非常有用。
    • isize:与目标机器指针大小相同的有符号整数,在处理依赖于机器架构的内存地址相关操作时很实用。
  2. 无符号整数类型

    • u8:8 位无符号整数,取值范围为 0 到 255。常用于表示字节数据,比如在处理文件 I/O 或网络协议中的字节流时。
    • u16:16 位无符号整数,取值范围为 0 到 65535。
    • u32:32 位无符号整数,取值范围为 0 到 4294967295。
    • u64:64 位无符号整数,取值范围为 0 到 18446744073709551615。
    • u128:128 位无符号整数,可表示极大的正整数。
    • usize:与目标机器指针大小相同的无符号整数,常用于表示集合的索引或内存大小。

整数类型的使用示例

下面通过代码示例来展示整数类型的声明和使用:

fn main() {
    let a: i32 = 42;
    let b: u8 = 255;

    // 整数运算
    let sum = a + b as i32;
    println!("Sum: {}", sum);

    // 溢出处理
    let mut x: u8 = 255;
    x = x.wrapping_add(1);
    println!("Wrapped addition: {}", x);
}

在上述代码中,首先声明了一个 i32 类型的变量 a 和一个 u8 类型的变量 b。在进行加法运算时,由于 b 的类型与 a 不同,需要将 b 转换为 i32 类型。此外,代码还展示了无符号整数的溢出处理,使用 wrapping_add 方法来实现环绕加法,避免程序因溢出而崩溃。

整数类型的内存布局与性能

不同的整数类型在内存中占用不同的字节数,这直接影响到程序的内存使用和性能。较小的整数类型(如 u8i8)占用较少的内存,适用于对内存空间要求较高的场景,例如在嵌入式系统中存储小数值或标志位。而较大的整数类型(如 i64u64)则适用于需要处理较大数值范围的情况,但会占用更多的内存。

在性能方面,现代 CPU 对特定大小的整数运算有优化。例如,32 位和 64 位 CPU 对 i32i64 类型的运算通常会比其他大小的整数类型更高效。因此,在选择整数类型时,需要综合考虑数值范围需求和性能因素。

浮点数类型

浮点数类型的种类

Rust 提供了两种主要的浮点数类型:f32f64,分别对应 32 位和 64 位的 IEEE 754 标准浮点数。

  1. f32:单精度浮点数,占用 4 个字节。它的精度大约为 7 位十进制数字,适用于对精度要求不特别高且对内存使用较为敏感的场景,例如游戏开发中的一些图形计算。
  2. f64:双精度浮点数,占用 8 个字节。其精度大约为 15 位十进制数字,在大多数科学计算和数值分析场景中被广泛使用,因为它能提供更高的精度。

浮点数类型的使用示例

fn main() {
    let pi_f32: f32 = 3.1415926;
    let pi_f64: f64 = 3.141592653589793;

    println!("f32 PI: {}", pi_f32);
    println!("f64 PI: {}", pi_f64);

    // 浮点数运算
    let result = pi_f64 * 2.0;
    println!("Result: {}", result);
}

在这段代码中,分别声明了 f32f64 类型的变量来表示圆周率 pi。通过打印可以看到,f32 类型由于精度限制,显示的数值相对 f64 类型不够精确。同时,代码展示了浮点数的乘法运算。

浮点数的精度问题与注意事项

浮点数在计算机中以二进制形式存储,由于二进制无法精确表示某些十进制小数,会导致精度损失。例如,0.1 在十进制中是一个简单的小数,但在二进制中是一个无限循环小数,因此在使用浮点数进行精确比较时需要特别小心。

fn main() {
    let a: f64 = 0.1 + 0.1 + 0.1;
    let b: f64 = 0.3;
    if (a - b).abs() < f64::EPSILON {
        println!("They are equal within tolerance");
    } else {
        println!("They are not equal");
    }
}

在上述代码中,虽然直观上 0.1 + 0.1 + 0.1 应该等于 0.3,但由于精度问题,直接比较可能会得到错误结果。这里通过比较两者差值的绝对值与 f64::EPSILON(一个非常小的数,代表 f64 类型的最小可表示精度)来判断是否相等,这是处理浮点数精度比较的常用方法。

布尔类型

布尔类型的定义与用途

Rust 的布尔类型 bool 只有两个可能的值:truefalse。它主要用于控制流语句,如 if - elsewhile 循环等,通过判断条件的真假来决定程序的执行路径。

布尔类型的使用示例

fn main() {
    let is_rust_cool = true;
    if is_rust_cool {
        println!("Rust is really cool!");
    } else {
        println!("Well, maybe not...");
    }
}

在这个简单的示例中,通过定义一个布尔变量 is_rust_cool 并在 if - else 语句中使用它来决定输出不同的信息。

布尔运算

Rust 支持常见的布尔运算,如逻辑与(&&)、逻辑或(||)和逻辑非(!)。

fn main() {
    let a = true;
    let b = false;

    let and_result = a && b;
    let or_result = a || b;
    let not_result =!a;

    println!("AND result: {}", and_result);
    println!("OR result: {}", or_result);
    println!("NOT result: {}", not_result);
}

上述代码展示了布尔运算的基本使用,通过不同的布尔值组合进行逻辑运算并输出结果。

字符类型

字符类型的本质

Rust 的字符类型 char 用于表示单个 Unicode 字符。与其他一些语言中字符类型可能只表示 ASCII 字符不同,Rust 的 char 类型可以表示任何 Unicode 标量值,占用 4 个字节。

字符类型的声明与使用

fn main() {
    let c1: char = 'A';
    let c2: char = '😀';

    println!("Character 1: {}", c1);
    println!("Character 2: {}", c2);
}

在这个示例中,分别声明了一个 ASCII 字符 'A' 和一个 Unicode 表情字符 '😀',展示了 char 类型对不同类型字符的支持。

字符与字符串的关系

虽然 char 表示单个字符,但 Rust 中的字符串是由多个 char 组成的。字符串类型 String 和字符串切片 &str 本质上是 char 的集合。不过,需要注意的是,字符串在内存中是以 UTF - 8 编码存储的,这意味着一个 char 可能对应多个字节。

fn main() {
    let s = "Hello, 世界";
    for c in s.chars() {
        println!("{}", c);
    }
}

上述代码通过遍历字符串 s 中的 char 来展示字符串与字符的关系。chars 方法将字符串按字符进行迭代,即使字符串中包含非 ASCII 字符,也能正确处理。

元组类型

元组的定义与特点

元组是 Rust 中一种将多个值组合在一起的复合类型。元组中的元素可以是不同类型,并且元组的长度是固定的,一旦声明,长度不能改变。

元组的声明与访问

fn main() {
    let tup: (i32, f64, char) = (42, 3.14, 'A');

    // 通过索引访问元组元素
    let first = tup.0;
    let second = tup.1;
    let third = tup.2;

    println!("First: {}, Second: {}, Third: {}", first, second, third);

    // 元组解构
    let (a, b, c) = tup;
    println!("a: {}, b: {}, c: {}", a, b, c);
}

在这个示例中,首先声明了一个包含 i32f64char 类型元素的元组 tup。可以通过索引(如 tup.0)来访问元组中的元素。此外,还展示了元组解构的用法,通过将元组的值解构成多个变量,更加方便地获取元组中的各个元素。

元组作为函数参数和返回值

元组在函数参数和返回值中非常有用,因为它允许函数一次性处理多个不同类型的值。

fn calculate() -> (i32, f64) {
    let sum: i32 = 10 + 20;
    let average: f64 = (10 + 20) as f64 / 2.0;
    (sum, average)
}

fn main() {
    let (result_sum, result_average) = calculate();
    println!("Sum: {}, Average: {}", result_sum, result_average);
}

在上述代码中,calculate 函数返回一个包含 i32f64 类型的元组。在 main 函数中,通过元组解构获取函数返回的两个值并进行打印。

数组类型

数组的定义与特性

Rust 中的数组是一种固定大小的、同类型元素的集合。数组的大小在编译时就确定,这意味着数组的长度不能在运行时改变。数组在内存中是连续存储的,这使得对数组元素的访问非常高效。

数组的声明与初始化

fn main() {
    // 声明并初始化数组
    let numbers: [i32; 5] = [1, 2, 3, 4, 5];

    // 使用默认值初始化数组
    let zeros: [i32; 10] = [0; 10];

    println!("Numbers: {:?}", numbers);
    println!("Zeros: {:?}", zeros);
}

在这个示例中,首先声明了一个包含 5 个 i32 类型元素的数组 numbers 并进行了初始化。然后展示了使用默认值初始化数组的方法,创建了一个包含 10 个 i32 类型元素且值都为 0 的数组 zeros。通过 {:?} 格式化输出数组的内容。

数组的访问与遍历

fn main() {
    let numbers: [i32; 5] = [1, 2, 3, 4, 5];

    // 通过索引访问数组元素
    let first = numbers[0];
    println!("First number: {}", first);

    // 遍历数组
    for num in &numbers {
        println!("Number: {}", num);
    }
}

上述代码展示了通过索引访问数组元素的方法,如 numbers[0] 获取数组的第一个元素。同时,使用 for 循环遍历数组,通过 &numbers 获取数组的引用,因为数组在传递给 for 循环时会隐式地借用,这样可以避免所有权转移。

数组与切片的关系

切片(&[T])是对数组的一部分的引用,它提供了一种灵活的方式来操作数组的部分内容。切片并不拥有数据的所有权,而是借用数组的数据。

fn main() {
    let numbers: [i32; 5] = [1, 2, 3, 4, 5];
    let slice: &[i32] = &numbers[1..3];

    println!("Slice: {:?}", slice);
}

在这个例子中,通过 &numbers[1..3] 创建了一个从数组 numbers 的第二个元素到第三个元素(不包括第四个元素)的切片 slice。切片在 Rust 中广泛用于函数参数,使得函数可以处理不同长度的数组部分,而无需关心数组的具体大小。

指针类型

原始指针

  1. *const T*mut T Rust 中的原始指针分为不可变指针 *const T 和可变指针 *mut T。与 Rust 的借用规则不同,原始指针可以无视所有权和借用规则,直接指向内存中的数据。这使得原始指针非常强大,但同时也很危险,因为使用不当可能会导致内存安全问题,如悬空指针、数据竞争等。
fn main() {
    let mut num = 42;
    let raw_const_ptr: *const i32 = &num;
    let raw_mut_ptr: *mut i32 = &mut num;

    // 从原始指针读取数据(需要 unsafe 块)
    unsafe {
        let value_from_const = *raw_const_ptr;
        let value_from_mut = *raw_mut_ptr;
        println!("Value from const ptr: {}", value_from_const);
        println!("Value from mut ptr: {}", value_from_mut);

        // 通过可变原始指针修改数据
        *raw_mut_ptr = 100;
        println!("Modified value: {}", num);
    }
}

在上述代码中,首先创建了一个 i32 类型的可变变量 num,然后分别获取了指向它的不可变原始指针 raw_const_ptr 和可变原始指针 raw_mut_ptr。由于原始指针的操作涉及到内存安全风险,所以对原始指针的解引用和修改操作都必须放在 unsafe 块中。

智能指针

  1. Box<T> Box<T> 是 Rust 中的一种智能指针,它在堆上分配内存来存储数据 TBox<T> 负责管理其所指向数据的生命周期,当 Box<T> 离开作用域时,它所指向的数据会被自动释放。
fn main() {
    let b = Box::new(42);
    println!("Value in box: {}", *b);
}

在这个示例中,使用 Box::new 创建了一个 Box,里面存储了一个 i32 类型的值 42。通过解引用 Box*b)可以获取里面存储的值。

  1. Rc<T>(引用计数指针) Rc<T> 用于在堆上分配数据,并通过引用计数来管理数据的生命周期。多个 Rc<T> 可以指向同一个数据,当最后一个指向数据的 Rc<T> 被销毁时,数据才会被释放。这在需要共享数据且数据生命周期依赖于引用数量的场景中非常有用,比如在构建树形结构等。
use std::rc::Rc;

fn main() {
    let shared_data = Rc::new(42);

    let clone1 = Rc::clone(&shared_data);
    let clone2 = Rc::clone(&shared_data);

    println!("Reference count: {}", Rc::strong_count(&shared_data));
}

在上述代码中,首先创建了一个 Rc 指向 i32 类型的值 42。然后通过 Rc::clone 创建了两个对 shared_data 的克隆,每克隆一次,引用计数就会增加。通过 Rc::strong_count 可以获取当前 Rc 的引用计数。

  1. Arc<T>(原子引用计数指针) Arc<T>Rc<T> 类似,也是通过引用计数来管理数据生命周期,但 Arc<T> 是线程安全的,适用于多线程环境下共享数据。
use std::sync::Arc;
use std::thread;

fn main() {
    let shared_data = Arc::new(42);

    let handle1 = thread::spawn(move || {
        let local_data = Arc::clone(&shared_data);
        println!("Thread 1: {}", local_data);
    });

    let handle2 = thread::spawn(move || {
        let local_data = Arc::clone(&shared_data);
        println!("Thread 2: {}", local_data);
    });

    handle1.join().unwrap();
    handle2.join().unwrap();
}

在这个多线程示例中,Arc 被用于在多个线程间共享数据。每个线程通过 Arc::clone 获取对共享数据的引用,并且 Arc 保证了在多线程环境下引用计数操作的原子性,从而确保数据的正确释放和线程安全。

函数指针类型

函数指针的定义与用途

函数指针是一种指向函数的指针类型。在 Rust 中,函数名本身就可以被看作是一个函数指针。函数指针可以作为参数传递给其他函数,也可以作为函数的返回值,这使得 Rust 能够实现类似于回调函数的功能。

函数指针的使用示例

fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn subtract(a: i32, b: i32) -> i32 {
    a - b
}

fn operate(a: i32, b: i32, func: fn(i32, i32) -> i32) -> i32 {
    func(a, b)
}

fn main() {
    let result1 = operate(5, 3, add);
    let result2 = operate(5, 3, subtract);

    println!("Add result: {}", result1);
    println!("Subtract result: {}", result2);
}

在上述代码中,首先定义了两个函数 addsubtract,分别用于加法和减法运算。然后定义了 operate 函数,它接受两个整数和一个函数指针作为参数,并通过调用传入的函数指针来执行相应的运算。在 main 函数中,分别将 addsubtract 函数作为函数指针传递给 operate 函数,实现不同的运算操作。

函数指针与闭包的关系

闭包是一种匿名函数,可以捕获其所在环境中的变量。在某些情况下,闭包可以自动转换为函数指针。例如,当闭包不捕获任何环境变量且其类型可以被推断为函数指针类型时,就可以将闭包作为函数指针使用。

fn main() {
    let func_ptr: fn(i32, i32) -> i32 = |a, b| a * b;

    let result = func_ptr(2, 3);
    println!("Result: {}", result);
}

在这个示例中,定义了一个闭包并将其赋值给 func_ptrfunc_ptr 的类型被指定为函数指针类型 fn(i32, i32) -> i32。由于闭包没有捕获环境变量,所以可以自动转换为函数指针类型,并且可以像使用普通函数指针一样调用它。

单元类型

单元类型的定义与特点

单元类型在 Rust 中用 () 表示,它只有一个值,也写作 ()。单元类型没有实际的数据,主要用于表示函数不返回任何有意义的值(类似于其他语言中的 void),或者作为元组中的占位符。

单元类型的使用场景

  1. 无返回值函数
fn print_message() {
    println!("This is a message.");
}

fn main() {
    let result: () = print_message();
    println!("Function returned: {:?}", result);
}

在上述代码中,print_message 函数没有显式的返回值,其返回类型默认为单元类型 ()。在 main 函数中调用 print_message 并将返回值赋值给 resultresult 的类型为 (),打印结果为 (),表示函数执行完毕但没有返回有意义的数据。

  1. 元组中的占位符
fn main() {
    let tup: (i32, (), f64) = (42, (), 3.14);
    println!("Tuple: {:?}", tup);
}

在这个示例中,元组 tup 包含一个 i32 类型、一个单元类型和一个 f64 类型的元素。单元类型在这里作为占位符,可能用于表示在该位置不需要特定的数据,但元组结构需要保持某种一致性。

通过对 Rust 各种基元类型的详细介绍和代码示例,我们深入了解了这些类型的特点、使用方法以及在不同场景下的应用。掌握这些基元类型是熟练运用 Rust 进行编程的关键基础。在实际开发中,根据具体需求选择合适的基元类型,可以提高程序的性能、安全性和可读性。