前端都能看懂的rust入门教程(二)——基本类型(Primitive Type)

0 阅读16分钟

前言

本文介绍Rust中的基本类型(Primitive Type)。

不同于js或java的基本类型(primitive value)或引用类型的概念, Rust 里的“primitive types”意思是:

  • 这些类型由 Rust 语言本身内建,不是用 trait 或用户自定义类型实现的。
  • 某些“基本类型”是直接可以用的(比如 i32),但有些(比如 str)本身不能被直接拿来用,只能通过引用或指针来用。
  • Rust 的类型系统允许基本类型是dynamically sized type(DST),但不是所有 primitive type 都能在不带引用情况下独立出现。

rust的基本类型非常多,但好在大多数都容易掌握。

image.png

以上截图来自于rust标准库文档,这是rust最重要的一个文档。

标量类型和复合类型

Rust 的类型系统更倾向于使用标量类型(scalar types)和复合类型(compound types)来分类其内置的原始类型(primitive types):标量类型代表单一值,而复合类型允许将多个值组合成一个单元。

1. 标量类型(Scalar Types)

  • 特点

    • 固定大小:在编译时已知大小,便于优化。
    • 不可分解:不能进一步拆分成更小的子类型。
    • 用于基本计算和逻辑操作。
  • Rust 中的标量类型示例

    • 整数类型(Integers) :有符号(i8, i16, i32, i64, i128, isize)和无符号(u8, u16, u32, u64, u128, usize)。例如,let x: i32 = 42; 表示一个单一的整数值。
    • 浮点类型(Floating-point) :f32 和 f64。例如,let y: f64 = 3.14; 表示一个单一的浮点数值。
    • 布尔类型(Boolean) :bool,只有 true 或 false。例如,let z: bool = true;。
    • 字符类型(Character) :char,表示单个 Unicode 标量值(Scalar Value),占用 4 字节。例如,let c: char = 'A';。

标量类型常用于简单变量声明和表达式计算,是 Rust 性能优化的基础。

2. 复合类型(Compound Types)

  • 特点

    • 可组合:可以嵌套其他类型,形成层次结构。
    • 大小通常固定(对于数组和元组),但可以是动态的(如果涉及切片)。
    • 用于表示关系或集合的数据,支持模式匹配和解构。
  • Rust 中的复合类型示例(聚焦于原始复合类型):

    • 元组类型(Tuples) :一个固定长度的异构集合(元素类型可以不同)。例如,let tup: (i32, f64, char) = (42, 3.14, 'A');。你可以解构它:let (a, b, c) = tup;。
    • 数组类型(Arrays) :一个固定长度的同构集合(所有元素类型相同)。例如,let arr: [i32; 3] = [1, 2, 3];。数组的大小在编译时必须已知。
    • 注意:Rust 的原始复合类型主要限于元组和数组。更高级的复合类型如结构体(structs)和枚举(enums)是用户定义的类型(derived types),但它们基于这些基础构建。

数字

对标js中的number,rust中分为了8,16,32,64,128位整数/无符号整形(只能正数)/浮点数,分别以i,u,f开头,比如i32,f64,u128。

除了这些,还有两个特殊的类型,isize和usize。集合类型(数组,向量,字符串等)的下标,长度只能用unsize,isize可用于指针偏移量。

pub fn number_test() {
    int_test();
    float_test();
    spec_num_test();
}
fn int_test() {
    let decimal = 98_222; // 十进制:98,222
    let hex = 0xff; // 十六进制:255
    let octal = 0o77; // 八进制:63
    let binary = 0b1111_0000; // 二进制:240
    let byte = b'A'; // 字节:65(仅限 u8)
    let with_type = 42u32; // 指定类型:42 as u32
    println!(
        "{},{},{},{},{},{}",
        decimal, hex, octal, binary, byte, with_type
    )
}
fn float_test() {
    let x = 2.0; // f64(默认)
    let y: f32 = 3.0; // f32
    let z = 1.0e10; // 科学计数法:1.0 × 10^10
    let inf = f32::INFINITY; // 正无穷大
    let neg_inf = f32::NEG_INFINITY; // 负无穷大
    let nan = f32::NAN; // 非数字
    println!("{},{},{},{},{},{}", x, y, z, inf, neg_inf, nan);
}
fn spec_num_test() {
    let max = f32::MAX; // 最大正值:3.4028235e38
    let min = f32::MIN; // 最小负值:-3.4028235e38
    let epsilon = f64::EPSILON; // 1.0 和下一个可表示数的差值

    // 检查特殊值
    let x: f64 = 0.0 / 0.0;
    println!("{},{}", max, min);
    println!("{}", (0.1 + 0.2) - 0.3 < epsilon); //true

    println!("is NaN: {}", x.is_nan()); // true
    println!("is infinite: {}", x.is_infinite()); // false
    println!("is finite: {}", x.is_finite()); // false
}

数字的常用方法

以上代码中使用了is_nan,is_infinite等方法,js中数字和数学相关的方法在Number和Math对象上,而rust则可以直接从数字本身上调用相关方法。

image.png 所有的方法都可以从标准库文档中查阅。

和包装类的区别

这个设计有点类似js或java中的自动包装类,但它本质上完全不是包装类的实现。 在 Rust 中,所有基本类型(如 i32f64)都是直接在栈上的值,没有“包装类”的概念

你能直接在它们上面调用方法,主要归功于Rust编译器强大的 自动引用/解引用机制 和 特质(Trait)系统

这是本系列文章第一次讲到引用机制,如果不能理解下面的内容,完全没有关系,只要知道这里并不是自动包装类的实现即可。

🧠 核心机制:编译器在幕后做了什么?

当你写下 5.to_string() 时,编译器会为你自动处理以下步骤:

  1. 自动借用:由于 to_string 方法通常以 &self 作为第一个参数(需要一个引用),所以 5 会被自动借用,变为 &5

  2. 方法查找与分发:编译器知道 5 是 i32 类型。然后它会去查找 i32 类型实现的所有特质。它发现 i32 实现了 ToString 特质,而 ToString 特质里定义了 to_string(&self) 方法。于是,编译器将你的调用静态地解析为:

    <i32 as ToString>::to_string(&5)
    

    这个转换在编译时就已完成,没有任何运行时开销。

一个更清晰的例子
(-1.01_f64).floor() 的完整转换路径是:

  1. . 操作触发方法调用。
  2. 编译器发现 -1.01_f64 是 f64 类型。
  3. 查找发现,f64 实现了 std::ops::Deref(使其可以获得更多方法),并且直接定义了 floor 方法。
  4. 最终调用被静态解析,等价于 f64::floor(-1.01_f64)

布尔

rust中的bool的使用和js基本一致

 let t = true;
    let f: bool = false; // 显式类型注解

    // 逻辑运算
    let and = true && false; // false
    let or = true || false; // true
    let not = !true; // false

    // 比较运算(返回布尔值)
    let x = 5;
    let y = 10;
    let result = x < y; // true

字符

如果你还了解java,那完全可以对照java中的char来理解。

Rust中的字符类型char表示一个Unicode标量值,占用4个字节。这意味着它可以表示比ASCII更广泛的字符,包括中文、日文、韩文、emoji等。

 let c = 'z'; // 单引号,字符字面量
    let z = 'ℤ'; // Unicode 字符
    let unicode = '\u{2764}'; // Unicode:'❤'
    let hanzi = '布';
    let heart_eyed_cat: char = '😻'; // Emoji

    // 转义字符
    let newline = '\n'; // 换行
    let tab = '\t'; // 制表符
    let single_quote = '\''; // 单引号
    let backslash = '\\'; // 反斜杠
    let hex_char = '\x41'; // 十六进制:'A'

    println!(
        "{},{},{},{},{},{},{},{},{},{}",
        c, z, heart_eyed_cat, newline, tab, single_quote, backslash, hex_char, unicode, hanzi
    );

    // 字符方法
    let c = 'A';

    println!("is alphabetic: {}", c.is_alphabetic()); // 是字母,true
    println!("is digit: {}", c.is_digit(10)); // true(十进制中是数字)
    println!("to lowercase: {}", c.to_lowercase()); // 'a'

如果你更熟悉js,那么很自然地将char当成使用单引号的单个字符的字符串,虽然表现上相似,但不管怎样,不要将char看成字符串,这是两个类型。

str

这一节介绍str类型,细心的读者会发现,前面章节标题都是中文,但本节的标题是str而不是字符串。

从语言层面上来说,str就是rust的字符串类型,但在rust开发中说到字符串类型,一般是&str或String。&str和String最直接的区别是,&str不拥有数据,不可改变数据而String是拥有数据且可改变数据

The str type, also called a ‘string slice’, is the most primitive string type. It is usually seen in its borrowed form, &str. It is also the type of string literals, &'static str.

str 是 Rust 中的字符串切片类型(string slice),通常以引用的形式出现:&str。它是不可变的 UTF-8 编码字节序列的视图。——相比char,字符串中的字符所占的字节数是变化的(1 - 4) ,这样有助于大幅降低字符串所占用的内存空间。

这里出现了一个符号&,Rust中的&表示 引用(Reference),它是一个指向数据的指针,但不是拥有数据的所有权,而&'static str中的static则表示了它的生命周期

rust中的str揭开了这门语言复杂性,相对于前面的内容,字符串的掌握难度将陡然上升,但它也是进入rust世界的一个非常好的切入点。

可以先不管其他的概念,先了解如何创建一个字符串。

字符串创建

1. 字面量创建

 let s: str = "hello"; //❌ 错误
// str 通常以 &str 的形式使用(引用)
let s: &str = "Hello, Rust!"; // 字符串字面量就是 &str 类型
let multi_line = r#"这是一个
多行
原始字符串"#;

2. 从String创建(借用)

let owned = String::from("Hello");

// 整个字符串的切片
let slice1: &str = &owned; // 隐式转换
let slice2: &str = owned.as_str(); // 显式方法

// 部分字符串的切片
let slice3 = &owned[0..5]; // "Hello"
let slice4 = &owned[..]; // 整个字符串

3. 其他形式创建

    // 从 数组创建
    let vec_bytes = vec![72, 101, 108, 108, 111]; // "Hello" 的 ASCII
    let str_from_vec = std::str::from_utf8(&vec_bytes).unwrap();
    let words = vec!["Hello", "World", "Rust"];
    let sentence = words.join(" "); // 用空格连接
    let fruits = ["Apple", "Banana", "Orange"];
    let result = fruits.join(", ");
    // 从 char 创建
    let ch = 'A';
    let str_from_char = ch.to_string().as_str();  // 需要先转 String

字符串常用操作

1. 拼接

// 拼接字符串
let s1: &str = "Hello";
let s2: &str = " World";
let combined: String = s1.to_string() + s2; // 返回 String
let combined_str = combined.as_str(); //String 转&str

&str无法直接拼接,需要转为String,拼接后的字符串类型也是String。

2. 长度和容量相关

    let s = "Hello 🦀";
    
    // 字节数(UTF-8 编码)
    println!("字节数: {}", s.len());           // 10(Hello=5, 🦀=4, 空格=1)
    
    // 字符数(Unicode 标量值)
    println!("字符数: {}", s.chars().count()); // 7
    
    // 是否为空
    println!("是否为空: {}", s.is_empty());    // false
    
    // 判断是否以字节序列开始/结束
    println!("是否以'H'开始: {}", s.starts_with('H'));  // true
    println!("是否以'🦀'结束: {}", s.ends_with('🦀'));  // true

3. 搜索和查找

 let s = "Hello, Rust Programming!";

    // 查找字符
    if let Some(pos) = s.find('R') {
        println!("找到 'R' 在位置: {}", pos); // 7
    }

    // 查找字符串
    if let Some(pos) = s.find("Prog") {
        println!("找到 'Prog' 在位置: {}", pos); // 12
    }

    // 检查包含关系
    println!("包含 'Rust'?: {}", s.contains("Rust")); // true

    // 匹配模式
    let matches: Vec<_> = s.match_indices("o").collect();
    println!("所有 'o' 的位置: {:?}", matches); // [(4, "o"), (14, "o")]

4. 分割

 let s = "apple,banana,orange,grape";

// 按字符分割
let fruits: Vec<&str> = s.split(',').collect();
println!("水果: {:?}", fruits); // ["apple", "banana", "orange", "grape"]

// 按字符串分割
let s2 = "apple::banana::orange";
let fruits2: Vec<&str> = s2.split("::").collect();
for f in fruits2 {
    println!("{}", f);
}
// 按闭包条件分割
let s3 = "apple1banana2orange3";
let fruits3: Vec<&str> = s3.split(char::is_numeric).collect();
for f in fruits3 {
    println!("{}", f);
}
// 保留分割符
let s4 = "apple,banana,orange";
let parts: Vec<&str> = s4.split_inclusive(',').collect();
println!("包含分隔符: {:?}", parts); // ["apple,", "banana,", "orange"]

// 按行分割
let text = "Line 1\nLine 2\r\nLine 3";
for line in text.lines() {
    println!("行: {}", line);
}

4. 大小写转换

 // 转为大写
let upper = s.to_uppercase();  // 返回 String
println!("大写: {}", upper);   // "HELLO RUST!"

// 转为小写
let lower = s.to_lowercase();  // 返回 String
println!("小写: {}", lower);   // "hello rust!"

5. trim

 let s = "   Hello, Rust!   ";
    
println!("原始: '{}'", s);
println!("修整两边空白: '{}'", s.trim());          // "Hello, Rust!"
println!("修整左边空白: '{}'", s.trim_start());    // "Hello, Rust!   "
println!("修整右边空白: '{}'", s.trim_end());      // "   Hello, Rust!"

6. 替换和修改

替换后返回的都是String类型

let s = "I like cats and cats are cute";

// 替换所有匹配
let replaced = s.replace("cats", "dogs"); // 返回 String
println!("替换后: {}", replaced); // "I like dogs and dogs are cute"

// 只替换第一个
let replaced_n = s.replacen("cats", "dogs", 1);
println!("替换第一个: {}", replaced_n); // "I like dogs and cats are cute"

// 替换范围
let mut s_string = String::from(s);
s_string.replace_range(7..11, "dogs"); // 修改 String,不是 &str
println!("替换范围: {}", s_string);

关于字符串还有很多内容需要进一步学习,但入门的话只要大致掌握以上内容即可。等学完Rust的一些特殊语法再回头来看字符串,就会觉得它非常简单了。

切片

切片(Slice)  是 Rust 中对集合中一段连续元素序列的引用。切片没有所有权,只是数据的视图

切片是一个胖指针(fat pointer),包含两个部分:指向数据的指针和切片的长度(元素个数)。在内存中,切片引用(如&[T])占用两个机器字(在64位系统上是16字节)。

&str是就是一种特殊的切片,它保证是有效的UTF-8。字符串切片与普通切片类似,但针对字符串进行了优化。

切片的类型表示为[T],其中T是元素类型。由于切片是动态大小类型(DST),我们通常使用其引用:&[T](不可变切片)或&mut [T](可变切片)。

切片的常用操作

1. 切片创建和修改

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

// 完整切片
let full_slice: &[i32] = &arr[..]; // [1, 2, 3, 4, 5]
// 部分切片
let middle = &arr[1..4]; // [2, 3, 4](包含1,不包含4)
println!("完整: {:?}", full_slice);
println!("中间: {:?}", middle);
let vec = vec![10, 20, 30, 40, 50];
// 从 Vec 借用切片
let slice1: &[i32] = &vec; // 自动解引用转换
let slice2: &[i32] = &vec[..]; // 显式切片,结果和自动解引用一致,但过程不一样

println!("向量切片: {:?}", slice1);
println!("显式切片: {:?}", slice2);
let s = String::from("Hello, Rust!");

// 字符串切片是 &str,它是 &[u8] 的特殊形式
let str_slice: &str = &s[..]; // 整个字符串
println!("{}", str_slice);
println!("可变切片");
let mut data = vec![1, 2, 3, 4, 5];
let slice: &mut [i32] = &mut data[1..4]; // 可变切片[2,3,4]

// 可以修改元素
slice[0] = 20; // 修改第一个元素 ,[20,3,4]
slice.swap(0, 2); // 交换元素 [4,3,20]

slice.reverse(); // 反转切片[20,3,4]

println!("修改后的数据: {:?}", data); // [1, 20, 3, 4, 5]

2. 切片搜索

 let slice = &[10, 20, 30, 20, 40, 50];

// 线性搜索
if let Some(pos) = slice.iter().position(|&x| x == 20) {
    println!("第一个20在位置: {}", pos); // 1
}

// 从后向前搜索
if let Some(pos) = slice.iter().rposition(|&x| x == 20) {
    println!("最后一个20在位置: {}", pos); // 3
}

// 二分搜索(需要排序)
let sorted = &[10, 20, 30, 40, 50];
match sorted.binary_search(&30) {
    Ok(pos) => println!("30在位置: {}", pos), // 2
    Err(pos) => println!("30应该在位置: {}", pos),
}

// 查找满足条件的元素
if let Some(&value) = slice.iter().find(|&&x| x > 25) {
    println!("第一个大于25的数: {}", value); // 30
}

数组

数组是Rust中固定大小的、相同类型的元素的集合,存储在栈上。
数组的类型表示为[T; N],其中T是元素类型,N是数组长度,且N在编译时必须已知。

特点:

  1. 固定长度:一旦声明,长度不能改变。
  2. 元素类型相同。
  3. 存储在连续的内存区域(栈上)。
  4. 通过索引访问,索引从0开始。

数组和切片的区别: 数组长度固定,切片长度动态。数组存储在栈上(除非被移动),切片是引用,指向一段连续内存。

数组的常用操作

1. 数组创建和元素访问

    // 完整初始化
    let arr1 = [1, 2, 3, 4, 5];

    // 重复值初始化(最常用)
    let zeros = [0; 10]; // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

    println!("完整: {:?}", arr1);
    println!("零数组: {:?}", zeros);

    let arr = [10, 20, 30, 40, 50];

    // 读取元素
    let first = arr[0]; // 10

    // 安全访问
    if let Some(value) = arr.get(2) {
        println!("安全访问第三个元素: {}", value); // 30
    }

    let point = [10.0, 20.0, 30.0];

    // 解构数组
    let [x, y, z] = point;
    println!("坐标: x={}, y={}, z={}", x, y, z);

    // 部分解构
    let [first, .., last] = [1, 2, 3, 4, 5];
    println!("第一个: {}, 最后一个: {}", first, last);

2. 数组遍历

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

    // 1. 使用 for 循环(借用)
    println!("for 循环(借用):");
    for &item in &arr {
        print!("{} ", item); // 1 2 3 4 5
    }
    println!();

    // 2. 使用 for 循环(直接迭代)
    println!("for 循环(直接):");
    for item in arr {
        print!("{} ", item); // 1 2 3 4 5
    }
    println!();

    // 3. 使用 iter() 方法
    println!("iter() 方法:");
    for item in arr.iter() {
        print!("{} ", item); // 1 2 3 4 5
    }
    println!();

    // 4. 使用迭代器方法
    let sum: i32 = arr.iter().sum();
    let doubled: Vec<i32> = arr.iter().map(|&x| x * 2).collect();//后文将更详细介绍迭代器

    println!("总和: {}", sum); // 15
    println!("加倍后: {:?}", doubled); // [2, 4, 6, 8, 10]

元组

元组(Tuple) 是一种可以存放多个不同类型的值的数据类型,它将这些值组合成一个整体。元组的值是有顺序的,可以通过位置索引访问。

  • 语法:在小括号内用逗号分隔多个元素。例如:(i32, f64, char)
  • 元组长度是固定的,创建时就确定不可变。
  • 元组可以包含任意数量、任意类型的元素。
    let tup = (500, 6.4, 1);
    let (x, y, z) = tup; // 解构,分别绑定到 x, y, z

单元

Rust中的()居然也是一个类型,这个类型就是单元(),这个类型只有一个值,也是()

// 没有返回值的函数实际上返回 ()
fn say_hello(name: &str) {  // 等价于 -> ()
    println!("Hello, {}!", name);
    // 隐式返回 ()
}
 // 表达式语句的值是 ()
    let x = {
        let y = 5;
        y + 1  // 表达式,返回 6
    };  // x = 6
    
    let z = {
        let y = 5;
        y + 1;  // 语句,返回 ()
    };  // z = ()

never

Never 类型 ! 是 Rust 中表示永不返回的计算的类型。它是零大小类型,表示一个永远不会产生值的计算。

fn infinite_loop() -> ! {
    loop {
        println!("永远循环...");
    }
}

never类型的关键特性是可以强制转换为任何类型

fn not_implemented_yet() -> String {
    todo!("还没实现这个功能"); // 返回 !,但函数签名要求 String
}

reference和pointer

Rust 中的 Pointer(指针)和 Reference(引用)是内存操作与所有权模型的核心概念,本节简单介绍这两者含义,后续将详细解析Rust的所有权模型。

1. Reference(引用)

  • 引用是对已有值的“借用”,不会获得所有权,因此不会销毁值。
  • 语法&T(不可变引用),&mut T(可变引用)
    let x = 42;
    let r: &i32 = &x; // 不可变引用
    println!("r: {}", r);

    let mut y = 5;
    let m: &mut i32 = &mut y; // 可变引用
    *m += 1;

    println!("y: {}", y); //6

2. Pointer(指针)

指针是更底层的内存地址。Rust 有多种指针类型,各自有不同的安全级别和用途。

裸指针(raw pointer)

*const T*mut T

  • *const T:不可变原生指针(裸指针)
  • *mut T:可变原生指针(裸指针)
  • 没有自动生命周期检查、不保证安全,需要 unsafe 代码使用

智能指针(smart pointer)

智能指针是指针的升级版,也实际拥有或管理内存,常见有:

  • Box :堆分配,所有权明确,单一所有者
  • Rc (Reference Counted):引用计数,适合单线程多所有者
  • Arc (Atomic Reference Counted):多线程安全的引用计数
  • RefCellMutex 等:运行时可变性的智能指针