Rust复合类型 四大军阀:数、元、切、串

在Rust编程中,数组、元组、切片和字符串是最基础且高频使用的复合类型。数组主打固定长度的同类型集合,元组专注于异构元素的固定组合,切片作为无所有权的动态引用视图衔接各类连续数据,字符串则因UTF-8编码特性有独特的双类型设计(&str与String)。本文将从定义语法、核心用法、特性差异、进阶拓展四个维度,结合详细示例代码,帮你彻底掌握这四种类型的实战技巧。

一、数组(Array):固定长度的同类型集合

Rust中的数组是长度固定、元素类型相同的连续内存集合,编译期就确定长度,无法动态增减。其内存布局紧凑,所有元素直接存储在栈上,适合存储数量固定、访问频繁的小型数据集合。

1.1 数组的 定义 与 初始化

数组定义有两种核心语法,均需明确长度(编译期常量),类型可显式指定或通过编译器推断。

rust 复制代码
fn main() {
    // 1. 显式指定类型和长度,全量初始化
    let arr1: [i32; 5] = [1, 2, 3, 4, 5];
    println!("显式初始化数组:{:?}", arr1); // 输出:[1, 2, 3, 4, 5]

    // 2. 类型推断(由初始化元素推导类型)
    let arr2 = [10, 20, 30]; // 推断为 [i32; 3]
    println!("类型推断数组:{:?}", arr2);

    // 3. 重复值初始化:[值; 长度],适用于元素相同的场景
    let arr3 = [0; 4]; // 4个0组成的数组,类型为 [i32; 4]
    println!("重复值数组:{:?}", arr3); // 输出:[0, 0, 0, 0]

    // 4. 部分初始化(需显式指定类型,剩余元素用默认值填充)
    // 注意:仅支持Copy类型(如基础数据类型),非Copy类型无法部分初始化
    let arr4: [i32; 5] = [1, 2; 5]; // 错误!重复语法是[值; 长度],不是[值1,值2;长度]
    let arr5: [i32; 5] = [1, 2, 3]; // 错误!部分初始化需用特定语法(Rust 1.59+支持)
    // 正确部分初始化方式(Rust 1.59+):
    let arr6: [i32; 5] = [1, 2, 3, ..]; // 前3个元素为1、2、3,后2个用i32默认值0填充
    println!("部分初始化数组:{:?}", arr6); // 输出:[1, 2, 3, 0, 0]
}

关键注意点:数组长度必须是编译期常量,不能用变量指定;非Copy类型(如String)无法使用重复值初始化([String::new(); 5] 错误),需通过循环或迭代器创建。

1.2 数组的核心使用方法

1.2.1 元素访问与遍历

通过索引访问元素(索引从0开始),支持for循环遍历,也可通过迭代器处理。

rust 复制代码
fn main() {
    let arr = [1, 2, 3, 4, 5];

    // 1. 索引访问
    println!("第一个元素:{}", arr[0]); // 输出:1
    println!("第三个元素:{}", arr[2]); // 输出:3

    // 索引越界会直接触发panic(编译期无法检查,运行时崩溃)
    // println!("第六个元素:{}", arr[5]); // 运行时错误:index out of bounds

    // 2. for循环遍历(直接遍历元素)
    println!("遍历数组元素:");
    for num in arr {
        print!("{} ", num); // 输出:1 2 3 4 5
    }
    println!();

    // 3. 遍历索引和元素(使用enumerate)
    println!("遍历索引和元素:");
    for (index, num) in arr.iter().enumerate() {
        println!("索引{}:{}", index, num);
    }

    // 4. 可变数组(mut修饰)与元素修改
    let mut mut_arr = [10, 20, 30];
    mut_arr[1] = 200; // 修改第二个元素
    println!("修改后的可变数组:{:?}", mut_arr); // 输出:[10, 200, 30]
}

1.2.2 数组的切片转换(核心拓展)

数组长度固定,灵活性较低,实际开发中常转换为 切片(&[T]) 使用。

切片是对数组的 引用,长度可变,支持动态访问,具体用法可参考后续切片章节的详细讲解,此处仅介绍数组与切片的基础转换。

rust 复制代码
fn main() {
    let arr = [1, 2, 3, 4, 5];

    // 1. 转换为完整切片
    let slice1: &[i32] = &arr;
    println!("完整切片:{:?}", slice1); // 输出:[1, 2, 3, 4, 5]

    // 2. 截取部分切片(左闭右开区间,索引不能越界)
    let slice2 = &arr[1..3]; // 截取索引1到2的元素(不含3)
    println!("部分切片:{:?}", slice2); // 输出:[2, 3]

    // 3. 切片的常用方法
    println!("切片长度:{}", slice2.len()); // 输出:2
    println!("切片是否为空:{}", slice2.is_empty()); // 输出:false
    println!("切片第一个元素:{:?}", slice2.first()); // 输出:Some(2)
    println!("切片最后一个元素:{:?}", slice2.last()); // 输出:Some(3)

    // 4. 切片作为函数参数(支持任意长度的数组)
    fn print_slice(slice: &[i32]) {
        println!("函数接收的切片:{:?}", slice);
    }
    print_slice(&arr); // 传入完整数组切片
    print_slice(&arr[2..]); // 传入从索引2开始的切片
}

1.3 数组的进阶特性与注意事项

  • 内存布局:数组元素在栈上连续存储,例如[i32; 3]占用12字节(每个i32占4字节),访问效率极高。

  • Copy特性:若元素类型实现Copy trait(如基础数据类型、数组本身),数组赋值时会触发浅拷贝(栈上数据直接复制);若元素为非Copy类型(如String),数组赋值会触发移动(原数组不可再使用)。

  • 与Vec的区别 :Vec是动态数组(堆分配,长度可变),数组是固定长度栈数组;Vec可通过into_iter()转换为数组(需确保长度匹配),数组可通过Vec::from()转换为Vec。

1.3.1 数组实战案例:固定配置项存储

在开发中,数组常用于存储固定数量的配置项(如端口列表、权限等级),利用其栈上存储、访问高效的特性,配合切片实现灵活传递。

rust 复制代码
// 定义应用固定端口配置(不可修改,编译期确定长度)
const APP_PORTS: [u16; 3] = [8080, 8081, 8082];

// 检查端口是否在允许列表中(接收切片参数,兼容数组/ Vec)
fn is_port_allowed(port: u16, allowed_ports: &[u16]) -> bool {
    allowed_ports.contains(&port)
}

// 筛选可用端口(排除已占用端口)
fn filter_available_ports(allowed_ports: &[u16], used_ports: &[u16]) -> Vec<u16> {
    allowed_ports.iter()
        .filter(|&p| !used_ports.contains(p))
        .cloned()
        .collect()
}

fn main() {
    // 模拟已占用端口
    let used_ports = [8081];
    
    // 检查端口是否允许
    let target_port = 8080;
    if is_port_allowed(target_port, &APP_PORTS) {
        println!("端口{}允许使用", target_port);
    } else {
        println!("端口{}禁止使用", target_port);
    }

    // 筛选可用端口
    let available_ports = filter_available_ports(&APP_PORTS, &used_ports);
    println!("可用端口:{:?}", available_ports); // 输出:[8080, 8082]
}
      

二、元组(Tuple):异构元素的固定组合

元组是Rust中灵活的复合类型,可包含不同类型的元素,长度固定(编译期确定),适合临时组合多个不同类型的值(如函数返回多个结果)。

元组本身无名称,元素需通过 索引解构访问。

2.1 元组的定义与初始化

元组用括号包裹元素,元素间用逗号分隔,类型可显式指定或由编译器推断,单元素元组需在元素后加逗号(避免与表达式括号混淆)。

rust 复制代码
fn main() {
    // 1. 显式指定类型
    let tuple1: (i32, &str, bool) = (100, "Rust", true);
    println!("显式类型元组:{:?}", tuple1); // 输出:(100, "Rust", true)

    // 2. 类型推断
    let tuple2 = (3.14, 'a', String::from("Tuple")); // 推断为 (f64, char, String)
    println!("类型推断元组:{:?}", tuple2);

    // 3. 单元素元组(必须加逗号)
    let tuple3 = (5,); // 正确,单元素元组
    let tuple4 = (5); // 错误,这是i32类型,不是元组
    println!("单元素元组:{:?}", tuple3); // 输出:(5,)

    // 4. 嵌套元组
    let nested_tuple = (1, ("a", 3.14), [true, false]);
    println!("嵌套元组:{:?}", nested_tuple); // 输出:(1, ("a", 3.14), [true, false])

    // 5. 空元组(单元元组):表示无值,常用于函数无返回值(等价于())
    let unit_tuple = ();
    println!("空元组:{:?}", unit_tuple); // 输出:()
}

2.2 元组的核心使用方法

2.2.1 元素访问与解构

元组元素可通过点索引访问,更推荐使用解构赋值(代码更简洁,支持部分解构、忽略元素)。

rust 复制代码
fn main() {
    let tuple = (10, "Hello", 3.14, true);

    // 1. 点索引访问
    println!("索引0元素:{}", tuple.0); // 输出:10
    println!("索引2元素:{}", tuple.2); // 输出:3.14

    // 2. 完整解构
    let (num, msg, pi, flag) = tuple;
    println!("解构后:num={}, msg={}, pi={}, flag={}", num, msg, pi, flag);

    // 3. 部分解构(只获取需要的元素)
    let (_, msg2, _, flag2) = tuple; // 用_忽略不需要的元素
    println!("部分解构:msg={}, flag={}", msg2, flag2);

    // 4. 嵌套元组解构
    let nested = (1, (2.2, "abc"));
    let (a, (b, c)) = nested;
    println!("嵌套解构:a={}, b={}, c={}", a, b, c); // 输出:a=1, b=2.2, c=abc

    // 5. 可变元组与元素修改
    let mut mut_tuple = (100, "Rust");
    mut_tuple.0 = 200; // 修改第一个元素
    mut_tuple.1 = "Programming"; // 修改第二个元素(&str是不可变引用,此处是重新赋值)
    println!("可变元组修改后:{:?}", mut_tuple); // 输出:(200, "Programming")
}

2.2.2 元组作为函数返回值

元组最常用的场景之一是让函数返回多个不同类型的结果,替代其他语言的"输出参数"或"结构体包装"。

rust 复制代码
// 计算两个数的和与积,返回元组
fn add_and_multiply(a: i32, b: i32) -> (i32, i32) {
    (a + b, a * b)
}

// 解析字符串为数字,返回(是否成功,结果)
fn parse_int(s: &str) -> (bool, i32) {
    match s.parse() {
        Ok(num) => (true, num),
        Err(_) => (false, 0),
    }
}

fn main() {
    let (sum, product) = add_and_multiply(3, 5);
    println!("3+5={}, 3×5={}", sum, product); // 输出:3+5=8, 3×5=15

    let (success, num) = parse_int("123");
    if success {
        println!("解析成功:{}", num); // 输出:解析成功:123
    } else {
        println!("解析失败");
    }
}

2.3 元组的进阶特性与注意事项

  • 不可变性:默认元组不可变,需加mut修饰才能修改元素;但修改时需注意元素类型的可变性(如&str只能重新赋值,不能修改其内容)。

  • Copy与移动:若所有元素都实现Copy trait,元组赋值时触发浅拷贝;若存在非Copy元素(如String),赋值时触发移动(原元组不可再使用)。

  • 遍历限制:元组元素类型不同,无法直接用for循环遍历,需通过解构或转换为数组(仅当元素类型相同时)遍历。

  • 与结构体的区别:元组无字段名,元素靠位置区分,适合临时组合;结构体有字段名,可读性更强,适合长期复用的复合类型。

2.3.1 元组实战案例:多结果处理与模式匹配

元组在处理"操作结果+附加信息"场景中极具优势,配合模式匹配可清晰区分不同结果,比单独返回多个值更简洁。

rust 复制代码
// 模拟读取文件:返回(是否成功,内容/错误信息,读取字节数)
fn read_file_simulate(filename: &str) -> (bool, String, u64) {
    match filename {
        "config.toml" => (true, String::from("port=8080\nlog_level=info"), 23),
        "data.txt" => (true, String::from("hello rust"), 10),
        _ => (false, String::from("文件不存在"), 0),
    }
}

// 模拟计算:返回(结果,是否溢出,计算耗时)
fn calculate(a: i32, b: i32) -> (i32, bool, u32) {
    let result = a.checked_add(b);
    match result {
        Some(val) => (val, false, 1), // 无溢出,耗时1ms
        None => (0, true, 1), // 溢出,耗时1ms
    }
}

fn main() {
    // 处理文件读取结果
    let (read_ok, content, size) = read_file_simulate("config.toml");
    match (read_ok, size) {
        (true, size) if size > 0 => {
            println!("读取成功,文件大小:{}字节,内容:\n{}", size, content);
        }
        (true, 0) => println!("读取成功,但文件为空"),
        (false, _) => println!("读取失败:{}", content),
    }

    // 处理计算结果
    let (sum, overflow, cost) = calculate(i32::MAX, 1);
    if overflow {
        println!("计算溢出,耗时{}ms", cost);
    } else {
        println!("计算结果:{},耗时{}ms", sum, cost);
    }
}
      

三、切片(Slice):动态长度的引用视图

切片(Slice)是Rust中一种无所有权的复合类型,本质是对一段连续内存(如数组、String、Vec)的引用 ,能动态截取部分数据,长度在运行时确定。

它不存储数据本身,仅保存指向数据起始位置的 指针 和 长度,是连接固定长度类型(数组)与动态操作的核心桥梁,常作为函数参数实现对不同长度数据的通用处理。

3.1 切片的核心概念与分类

切片的类型格式为&[T](不可变切片)或&mut [T](可变切片),其中T为引用数据的类型。根据引用对象的不同,常见切片分为两类:

  • 数组切片 :对数组截取得到的切片,类型为&[T],对应数组[T; N]N为编译期常量)。

  • 字符串切片 :对String或字符串字面量截取得到的切片,类型为&str,本质是&[u8]的UTF-8安全封装(后续字符串章节会联动讲解)。

关键特性:切片无所有权,其生命周期依赖于被引用的数据(原数据存活,切片才有效);不可变切片&[T]禁止修改数据,可变切片&mut [T]允许修改数据,但需保证唯一引用(符合Rust借用规则)。

3.2 切片的创建与初始化

切片通过"&原数据[起始索引...结束索引]"语法创建,左闭右开区间,索引省略时默认取边界(起始索引省略为0,结束索引省略为原数据长度)。

rust 复制代码
fn main() {
    // 1. 数组切片的创建
    let arr = [1, 2, 3, 4, 5];
    let full_slice = &arr[..]; // 完整数组切片,等价于&arr
    let part_slice = &arr[1..4]; // 截取索引1-3的元素(不含4)
    let start_slice = &arr[2..]; // 从索引2截取到末尾
    let end_slice = &arr[..3]; // 从开头截取到索引2(不含3)
    println!("完整数组切片:{:?}", full_slice); // 输出:[1, 2, 3, 4, 5]
    println!("部分数组切片:{:?}", part_slice); // 输出:[2, 3, 4]

    // 2. 可变数组切片的创建(需原数组为mut,且切片为&mut [T])
    let mut mut_arr = [10, 20, 30, 40];
    let mut_slice = &mut mut_arr[1..3]; // 可变切片,指向索引1-2的元素
    mut_slice[0] = 200; // 通过可变切片修改原数组数据
    println!("修改后原数组:{:?}", mut_arr); // 输出:[10, 200, 30, 40]

    // 3. 字符串切片的创建(后续字符串章节将详细展开)
    let s = String::from("Hello Rust");
    let str_slice = &s[0..5]; // 截取前5个字节(对应"Hello")
    println!("字符串切片:{}", str_slice); // 输出:Hello

    // 错误示范:索引越界(运行时panic)
    // let invalid_slice = &arr[5..]; // 索引超出数组长度,运行时崩溃
}

3.3 切片的核心使用方法

3.3.1 切片的基础操作

切片支持长度获取、元素访问、遍历等操作,语法与数组类似,但长度可动态变化。

rust 复制代码
fn main() {
    let arr = [1, 2, 3, 4, 5];
    let slice = &arr[1..4];

    // 1. 获取长度
    println!("切片长度:{}", slice.len()); // 输出:3
    println!("切片是否为空:{}", slice.is_empty()); // 输出:false

    // 2. 元素访问(索引访问、first/last方法)
    println!("切片第二个元素:{}", slice[1]); // 输出:3
    println!("切片第一个元素:{:?}", slice.first()); // 输出:Some(2)(返回Option,避免越界)
    println!("切片最后一个元素:{:?}", slice.last()); // 输出:Some(4)

    // 3. 遍历切片
    println!("遍历切片元素:");
    for num in slice {
        print!("{} ", num); // 输出:2 3 4
    }
    println!();

    // 遍历可变切片并修改元素
    let mut mut_arr = [10, 20, 30, 40];
    let mut_slice = &mut mut_arr[..];
    for num in mut_slice {
        *num *= 2; // 每个元素翻倍
    }
    println!("修改后可变切片:{:?}", mut_slice); // 输出:[20, 40, 60, 80]
}

3.3.2 切片作为函数参数(核心场景)

切片最大的价值是作为函数参数,实现对不同长度的数组、Vec等连续数据的通用处理,无需为每种长度单独定义函数。

rust 复制代码
// 通用函数:计算切片中所有元素的和(支持任意长度的i32数组/ Vec)
fn sum_slice(slice: &[i32]) -> i32 {
    let mut total = 0;
    for &num in slice {
        total += num;
    }
    total
}

// 通用函数:打印切片内容(支持任意类型的切片,需实现Display trait)
fn print_slice<T: std::fmt::Display>(slice: &[T]) {
    print!("[");
    for (index, item) in slice.iter().enumerate() {
        if index == slice.len() - 1 {
            print!("{}", item);
        } else {
            print!("{}, ", item);
        }
    }
    println!("]");
}

fn main() {
    let arr1 = [1, 2, 3];
    let arr2 = [4, 5, 6, 7, 8];
    let vec = vec![9, 10]; // Vec转换为切片(零成本)

    println!("arr1总和:{}", sum_slice(&arr1)); // 输出:6
    println!("arr2总和:{}", sum_slice(&arr2)); // 输出:30
    println!("vec总和:{}", sum_slice(&vec)); // 输出:19

    print_slice(&arr1); // 输出:[1, 2, 3]
    print_slice(&vec); // 输出:[9, 10]
}

3.4 切片的进阶特性与注意事项

  • 生命周期约束 :切片的生命周期'a必须小于等于被引用数据的生命周期,否则会出现"悬垂切片"(原数据释放后切片仍被使用),编译期会报错。
    // 错误示范:悬垂切片 fn invalid_slice() -> &[i32] { let arr = [1, 2, 3]; &arr[..] // 错误:arr在函数结束后释放,切片无有效引用 }

  • 与原容器的关联:切片修改会同步影响原容器(如可变切片修改元素),但切片无法改变原容器的长度(如添加/删除元素)。

  • 零成本抽象:切片的创建和使用无额外内存开销,仅传递指针和长度,效率与直接操作原数据一致。

  • 切片与数组/ Vec的转换 :数组/ Vec可通过&[..]零成本转换为切片;切片可通过TryInto转换为数组(需长度匹配),或通过Vec::from()转换为Vec(需分配内存)。

    `fn main() {

    let arr = [1, 2, 3];

    let slice = &arr[...];

    // 切片转数组(长度必须匹配,否则返回Err)

    let arr_from_slice: Result<[i32; 3], _> = slice.try_into();

    println!("切片转数组:{:?}", arr_from_slice.unwrap()); // 输出:[1, 2, 3]

    // 切片转Vec(分配内存)

    let vec_from_slice: Vec = slice.into();

    println!("切片转Vec:{:?}", vec_from_slice); // 输出:[1, 2, 3]
    }`

3.4.1 切片实战案例:数据筛选与原地排序

切片可直接操作原容器数据,适合实现数据筛选、排序等中间处理逻辑,无需拷贝数据,提升性能。

rust 复制代码
// 原地排序切片(修改原数组/ Vec)
fn sort_slice(slice: &mut [i32]) {
    slice.sort(); // 升序排序
}

// 筛选切片中符合条件的元素(返回新Vec,不修改原数据)
fn filter_slice(slice: &[i32], condition: fn(i32) -> bool) -> Vec<i32> {
    slice.iter()
        .filter(|&x| condition(*x))
        .cloned()
        .collect()
}

// 切片分段:将切片按指定长度拆分(最后一段长度可不足)
fn split_slice<T>(slice: &[T], chunk_size: usize) -> Vec<&[T]> {
    let mut chunks = Vec::new();
    let mut start = 0;
    while start < slice.len() {
        let end = std::cmp::min(start + chunk_size, slice.len());
        chunks.push(&slice[start..end]);
        start = end;
    }
    chunks
}

fn main() {
    // 原地排序案例
    let mut numbers = [3, 1, 4, 1, 5, 9, 2, 6];
    println!("排序前:{:?}", numbers);
    sort_slice(&mut numbers[..]); // 对整个数组切片排序
    println!("排序后:{:?}", numbers); // 输出:[1, 1, 2, 3, 4, 5, 6, 9]

    // 筛选案例:筛选偶数
    let evens = filter_slice(&numbers, |x| x % 2 == 0);
    println!("筛选出的偶数:{:?}", evens); // 输出:[2, 4, 6]

    // 分段案例:按3个元素分段
    let chunks = split_slice(&numbers, 3);
    println!("切片分段结果:");
    for (i, chunk) in chunks.iter().enumerate() {
        println!("第{}段:{:?}", i+1, chunk);
    }
    // 输出:
    // 第1段:[1, 1, 2]
    // 第2段:[3, 4, 5]
    // 第3段:[6, 9]
}
      

四、字符串(String & str):UTF-8编码的文本处理

Rust的字符串体系分为两种核心类型:&str字符串切片)和 String动态字符串),两者均基于UTF-8编码,且相互关联但特性不同。理解两者的区别与转换是掌握Rust字符串的关键。

4.1 字符串的核心概念

  • &str(字符串切片):不可变的字符串引用,指向一段UTF-8编码的字节序列,长度固定(编译期或运行时确定),存储在栈上(引用)和数据段/堆上(字节序列)。字符串字面量("abc")的类型就是&'static str(生命周期为整个程序)。

  • String(动态字符串) :可变的动态字符串,基于堆分配存储字节序列,长度可动态增减,支持修改、拼接、删除等操作,本质是对 Vec<u8> 的封装(优化了UTF-8合法性检查)。

4.2 字符串的定义与初始化

4.2.1 &str的定义

rust 复制代码
fn main() {
    // 1. 字符串字面量(类型为 &'static str)
    let s1: &str = "Hello, Rust!";
    println!("字符串字面量:{}", s1);

    // 2. 从String切片获取&str
    let s2 = String::from("Dynamic String");
    let s3: &str = &s2; // 对String取引用,得到&str
    println!("从String切片获取:{}", s3);

    // 3. 截取字符串切片(左闭右开,基于字节索引,需注意UTF-8编码)
    let s4 = "中国 Rust";
    let s5 = &s4[0..3]; // 中文"中"占3个字节(UTF-8)
    println!("截取切片:{}", s5); // 输出:中
    // let s6 = &s4[0..2]; // 错误!截取部分UTF-8字节,导致无效编码,运行时panic
}

4.2.2 String的定义

String有多种创建方式,可根据场景选择(new()、from()、with_capacity()等)。

rust 复制代码
fn main() {
    // 1. new():创建空String,需后续添加内容
    let mut s1 = String::new();
    s1.push_str("Hello");
    println!("new()创建:{}", s1); // 输出:Hello

    // 2. from():从&str创建String(最常用)
    let s2 = String::from("Hello, World!");
    println!("from()创建:{}", s2);

    // 3. with_capacity(n):预分配n字节容量的空String,优化性能(避免频繁扩容)
    let mut s3 = String::with_capacity(10);
    s3.push_str("Rust");
    println!("with_capacity创建:{},容量:{},长度:{}", s3, s3.capacity(), s3.len());
    // 输出:with_capacity创建:Rust,容量:10,长度:4

    // 4. 从字符迭代器创建
    let s4: String = "abc123".chars().collect();
    println!("从迭代器创建:{}", s4); // 输出:abc123

    // 5. 拼接创建(format!宏结果转换为String)
    let name = "Rust";
    let s5 = format!("Hello, {}", name);
    println!("format!创建:{}", s5); // 输出:Hello, Rust
}

4.3 字符串的核心使用方法

4.3.1 字符串拼接与修改

String支持多种修改操作,&str因不可变,无法直接修改,需转换为String后操作。

rust 复制代码
fn main() {
    // 1. 拼接操作(+运算符、format!宏)
    let s1 = String::from("Hello");
    let s2 = String::from(" Rust");
    let s3 = s1 + &s2; // +运算符:左侧是String,右侧是&str,返回新String(s1被移动,不可再使用)
    println!("+运算符拼接:{}", s3); // 输出:Hello Rust

    let s4 = String::from("a");
    let s5 = String::from("b");
    let s6 = String::from("c");
    let s7 = format!("{}+{}+{}", s4, s5, s6); // format!宏:支持多个String/&str,不移动原字符串
    println!("format!宏拼接:{}", s7); // 输出:a+b+c

    // 2. 追加字符/字符串
    let mut s8 = String::from("Hello");
    s8.push('!'); // push:追加单个char
    s8.push_str(" World"); // push_str:追加&str
    println!("追加后:{}", s8); // 输出:Hello! World

    // 3. 插入字符/字符串
    let mut s9 = String::from("Heo");
    s9.insert(2, 'l'); // 在索引2处插入char(基于UTF-8字符索引,非字节索引)
    s9.insert_str(3, "ll"); // 在索引3处插入&str
    println!("插入后:{}", s9); // 输出:Hello

    // 4. 替换与删除
    let mut s10 = String::from("Hello, Rust!");
    let s11 = s10.replace("Rust", "Programming"); // replace:返回新String,原字符串不变
    println!("replace后新字符串:{}", s11); // 输出:Hello, Programming!

    s10.retain(|c| c != ' '); // retain:保留满足条件的字符,修改原字符串
    println!("retain后原字符串:{}", s10); // 输出:Hello,Rust!

    s10.truncate(5); // truncate:从字节索引5处截断,保留前5个字节(需注意UTF-8)
    println!("truncate后:{}", s10); // 输出:Hello

    s10.clear(); // clear:清空字符串,容量不变,长度变为0
    println!("clear后:{},容量:{},长度:{}", s10, s10.capacity(), s10.len());
}

4.3.2 字符串遍历与索引问题

由于Rust字符串基于UTF-8编码,字符长度不固定(1-4字节),无法直接通过整数索引访问字符(会导致panic),需通过特定方式遍历。

rust 复制代码
fn main() {
    let s = "中国 Rust 编程";

    // 1. 字节遍历(迭代每个UTF-8字节,不推荐处理中文)
    println!("字节遍历:");
    for byte in s.bytes() {
        print!("{} ", byte); // 输出中文对应的字节序列
    }
    println!();

    // 2. 字符遍历(迭代每个UTF-8字符,推荐)
    println!("字符遍历:");
    for ch in s.chars() {
        print!("{} ", ch); // 输出:中 国   R u s t   编 程
    }
    println!();

    // 3.  grapheme 集群遍历(处理多字符组合,如 emoji、重音字符,需引入第三方库)
    // 需在Cargo.toml添加:unicode-segmentation = "1.10"
    use unicode_segmentation::UnicodeSegmentation;
    println!("grapheme集群遍历:");
    for grapheme in s.graphemes(true) {
        print!("{} ", grapheme); // 与chars()类似,但支持复杂字符
    }
    println!();

    // 错误示范:直接索引访问字符
    // println!("第一个字符:{}", s[0]); // 错误!s[0]是字节,不是字符,中文场景下无效
    // 正确获取第一个字符
    let first_char = s.chars().next().unwrap();
    println!("第一个字符:{}", first_char); // 输出:中
}

4.4 字符串的进阶拓展

4.4.1 &str与String的转换

rust 复制代码
fn main() {
    // 1. String -> &str(直接取引用,零成本转换)
    let s1 = String::from("Hello");
    let s2: &str = &s1;
    let s3 = s1.as_str(); // 显式转换,与&s1等价

    // 2. &str -> String(需分配内存,非零成本)
    let s4: &str = "Rust";
    let s5 = s4.to_string(); // 方式1
    let s6 = String::from(s4); // 方式2
    let s7 = s4.into(); // 方式3(通过Into trait)
}

4.4.2 字符串的常用工具方法

rust 复制代码
fn main() {
    let s = "  Hello, Rust!  ";

    // 1. 去除前后空白
    let trimmed = s.trim();
    println!("去除空白:{}", trimmed); // 输出:Hello, Rust!

    // 2. 判断前缀/后缀
    println!("是否以\"Hello\"开头:{}", trimmed.starts_with("Hello")); // 输出:true
    println!("是否以\"!\"结尾:{}", trimmed.ends_with("!")); // 输出:true

    // 3. 查找子串
    match trimmed.find("Rust") {
        Some(index) => println!("子串\"Rust\"在索引{}处", index), // 输出:子串"Rust"在索引7处
        None => println!("未找到子串"),
    }

    // 4. 分割字符串
    let parts: Vec<&str> = trimmed.split(',').collect();
    println!("按逗号分割:{:?}", parts); // 输出:["Hello", " Rust!"]

    // 5. 判断空值
    let empty_str: &str = "";
    let empty_string = String::new();
    println!("&str是否为空:{}", empty_str.is_empty()); // 输出:true
    println!("String是否为空:{}", empty_string.is_empty()); // 输出:true
}

4.4.3 字符串的内存与编码注意事项

核心要点:1. Rust字符串强制要求UTF-8编码,无法存储无效UTF-8字节序列(若需处理非UTF-8数据,可使用Vec或OsString);2. String的len()方法返回字节数,而非字符数,chars().count()可获取字符数;3. 字符串切片截取必须基于有效UTF-8字节边界,否则触发panic,可使用unicode-slice等库安全截取。

4.4.4 字符串实战案例:用户输入处理与格式化

字符串常用于处理用户输入、日志格式化等场景,需解决空格清理、编码校验、动态拼接等问题,结合切片实现高效处理。

rust 复制代码
use std::io;

// 处理用户名输入:去空格、校验长度、转换为统一格式
fn process_username(input: &str) -> Result<String, &'static str> {
    let trimmed = input.trim(); // 去除前后空格
    // 校验:非空且长度在3-20字符(按UTF-8字符计数)
    if trimmed.is_empty() {
        return Err("用户名不能为空");
    }
    let char_count = trimmed.chars().count();
    if char_count < 3 || char_count > 20 {
        return Err("用户名长度需在3-20个字符之间");
    }
    // 首字母大写,其余小写(兼容中英文)
    let mut chars = trimmed.chars();
    let first_char = chars.next().unwrap().to_ascii_uppercase();
    let rest_chars: String = chars.map(|c| c.to_ascii_lowercase()).collect();
    Ok(format!("{}{}", first_char, rest_chars))
}

// 格式化日志信息:拼接时间、级别、内容
fn format_log(level: &str, content: &str) -> String {
    let time = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
    // 按级别添加颜色标记(简化版)
    let level_tag = match level.to_uppercase().as_str() {
        "INFO" => "[INFO] ",
        "WARN" => "[WARN] ",
        "ERROR" => "[ERROR]",
        _ => "[DEBUG]",
    };
    format!("[{}] {} {}", time, level_tag, content)
}

fn main() {
    // 处理用户输入
    let mut input = String::new();
    println!("请输入用户名:");
    io::stdin().read_line(&mut input).expect("读取输入失败");
    
    match process_username(&input) {
        Ok(username) => println!("处理后的用户名:{}", username),
        Err(e) => println!("用户名校验失败:{}", e),
    }

    // 格式化日志
    let info_log = format_log("info", "应用启动成功");
    let error_log = format_log("error", "数据库连接失败");
    println!("{}", info_log);
    println!("{}", error_log);
}
      

依赖说明:上述日志格式化案例使用了chrono库处理时间,需在Cargo.toml中添加依赖:chrono = "0.4"。实际开发中,可结合logenv_logger等库实现更完整的日志系统。

五、综合示例:组合使用数组、元组、切片与字符串

下面通过一个实战场景,展示如何组合使用这四种类型:存储用户信息(姓名、年龄、爱好数组),通过切片实现动态处理,再结合字符串格式化输出结果。

rust 复制代码
// 综合实战:用户信息管理系统(简化版)
// 涵盖数组、元组、切片、字符串的联动使用
fn main() {
    // 1. 定义用户数据:数组存储多个用户,每个用户用元组表示(ID、姓名、年龄、爱好数组)
    let mut users = [
        (1, String::from("张三"), 25, ["篮球", "编程", "阅读"] as [&str; 3]),
        (2, String::from("李四"), 30, ["跑步", "摄影"] as [&str; 2]),
        (3, String::from("王五"), 22, ["游戏", "音乐", "旅行"] as [&str; 3]),
    ];

    // 2. 遍历用户数组(通过切片实现通用遍历)
    println!("=== 原始用户列表 ===");
    print_users(&users[..]);

    // 3. 新增用户(替换数组最后一位,实际开发用Vec更合适,此处演示数组用法)
    users[2] = (3, String::from("赵六"), 28, ["烹饪", "爬山"] as [&str; 2]);
    println!("\n=== 修改后用户列表 ===");
    print_users(&users[..]);

    // 4. 筛选用户:年龄大于25岁的用户
    let adult_users = filter_users_by_age(&users[..], 25);
    println!("\n=== 年龄大于25岁的用户 ===");
    for user in adult_users {
        println!("ID:{},姓名:{},年龄:{}", user.0, user.1, user.2);
    }

    // 5. 处理用户爱好:统计所有爱好并去重
    let all_hobbies = collect_all_hobbies(&users[..]);
    println!("\n=== 所有用户的爱好(去重后) ===");
    println!("{:?}", all_hobbies);

    // 6. 生成用户简介字符串
    let user_intro = generate_user_intro(&users[0]);
    println!("\n=== 第一个用户简介 ===");
    println!("{}", user_intro);
}

// 打印用户列表(接收切片参数,兼容任意长度的用户数组)
fn print_users(users: &[(i32, String, u8, [&str; 3])]) {
    for user in users {
        let (id, name, age, hobbies) = user;
        let hobby_str = hobbies.iter()
            .filter(|&h| !h.is_empty()) // 过滤空爱好
            .collect::<Vec<&&str>>()
            .join("、");
        println!("ID:{} | 姓名:{} | 年龄:{} | 爱好:{}", id, name, age, hobby_str);
    }
}

// 按年龄筛选用户(返回新的用户Vec)
fn filter_users_by_age(users: &[(i32, String, u8, [&str; 3])], min_age: u8) -> Vec<(i32, String, u8, [&str; 3])> {
    users.iter()
        .filter(|&user| user.2 > min_age)
        .cloned()
        .collect()
}

// 收集所有用户的爱好并去重
fn collect_all_hobbies(users: &[(i32, String, u8, [&str; 3])]) -> Vec<&str> {
    let mut hobbies = Vec::new();
    for user in users {
        for hobby in &user.3 {
            if !hobby.is_empty() && !hobbies.contains(hobby) {
                hobbies.push(*hobby);
            }
        }
    }
    hobbies.sort();
    hobbies
}

// 生成用户简介字符串
fn generate_user_intro(user: &(i32, String, u8, [&str; 3])) -> String {
    let (id, name, age, hobbies) = user;
    let hobby_str = hobbies.iter()
        .filter(|&h| !h.is_empty())
        .collect::<Vec<&&str>>()
        .join("、");
    format!("用户ID:{},姓名:{},今年{}岁,平时喜欢{}。性格开朗,热爱生活!", 
            id, name, age, hobby_str)
}
      

输出结果:

plain 复制代码
=== 原始用户列表 ===
ID:1 | 姓名:张三 | 年龄:25 | 爱好:篮球、编程、阅读
ID:2 | 姓名:李四 | 年龄:30 | 爱好:跑步、摄影
ID:3 | 姓名:王五 | 年龄:22 | 爱好:游戏、音乐、旅行

=== 修改后用户列表 ===
ID:1 | 姓名:张三 | 年龄:25 | 爱好:篮球、编程、阅读
ID:2 | 姓名:李四 | 年龄:30 | 爱好:跑步、摄影
ID:3 | 姓名:赵六 | 年龄:28 | 爱好:烹饪、爬山

=== 年龄大于25岁的用户 ===
ID:2,姓名:李四,年龄:30
ID:3,姓名:赵六,年龄:28

=== 所有用户的爱好(去重后) ===
["篮球", "编程", "阅读", "跑步", "摄影", "烹饪", "爬山"]

=== 第一个用户简介 ===
用户ID:1,姓名:张三,今年25岁,平时喜欢篮球、编程、阅读。性格开朗,热爱生活!
          

输出结果:

plain 复制代码
原始用户信息:
姓名:张三,年龄:25,爱好:["篮球", "编程", "阅读"]

修改后用户信息:
姓名:张三(昵称:小三),年龄:26
爱好:篮球、Rust编程、阅读

用户简介:大家好,我是张三(昵称:小三),今年26岁,喜欢篮球、Rust编程、阅读。

六、总结

数组、元组、切片、字符串是Rust基础且核心的复合类型,各自有明确的适用场景,且相互联动紧密:

  • 数组:适合存储固定长度、同类型的小型数据,栈上存储,访问高效,常通过切片转换提升灵活性。

  • 元组:适合临时组合不同类型的值(如函数多返回值),长度固定,解构赋值便捷,无需定义结构体。

  • 字符串:&str本质是字符串切片,适合引用不可变文本;String是动态堆字符串,适合修改操作,需注意UTF-8编码特性。

  • 切片:无所有权的动态引用视图,衔接固定长度类型(数组)与动态操作,是实现通用函数、高效处理连续数据的核心。

掌握这四种类型的特性、用法及相互配合,能为后续Rust编程(如集合、结构体、函数开发)打下坚实基础。实际开发中,需根据数据的可变性、长度是否固定、类型是否一致等因素,选择合适的类型,同时规避索引越界、UTF-8编码错误、悬垂切片等常见问题。

相关推荐
我不是8神2 小时前
字节跳动 Eino 框架(Golang+AI)知识点全面总结
开发语言·人工智能·golang
kong79069283 小时前
Python核心语法-Python自定义模块、Python包
开发语言·python·python核心语法
爱敲代码的小鱼3 小时前
事务核心概念与隔离级别解析
java·开发语言·数据库
小冷coding4 小时前
【Java】遇到微服务接口报错导致系统部分挂掉时,需要快速响应并恢复,应该怎么做呢?如果支付服务出现异常如何快速处理呢?
java·开发语言·微服务
星火开发设计4 小时前
二维数组:矩阵存储与多维数组的内存布局
开发语言·c++·人工智能·算法·矩阵·函数·知识
夜勤月4 小时前
彻底终结内存泄漏与悬挂指针:深度实战 C++ 智能指针底层原理与自定义内存池,打造稳如泰山的系统基石
开发语言·c++
+VX:Fegn08954 小时前
计算机毕业设计|基于springboot + vue酒店预订系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
HeisenbergWDG4 小时前
线程实现runnable和callable接口
java·开发语言
少控科技4 小时前
QT新手日记028 QT-QML所有类型
开发语言·qt