Administrator
发布于 2025-12-04 / 7 阅读
0

Rust的复合类型

本文接2025-12-02发布的rust学习

字符串与切片

String和&str的区别

在其他语言中,字符串往往都是相当简单的内容,基本都是”hello world“,但是不能把同样的想法带到Rust,应当重视这个部分,否则会栽跟头

fn main() {
  let my_name = "Pascal";
  greet(my_name);
}

fn greet(name: String) {
  println!("Hello, {}!", name);
}

就比如这一段代码看似正常实际上就会报错

错误内容就是:函数需要一个String,但是却传进来一个&str类型的字符串

那么我们来看一下这两个的区别:

  • String是标准库里的堆分配、可增长、可修改、有所有权的字符串类型

  • &str是堆程序中一段只读数据的不可变借用,没有所有权(因为是引用)

str则是上一篇里就介绍的字符串字面量,他不可以修改,船舰后就写死在栈里,和常量一样

重点记忆的是:

字面量多半是&str,要堆上可变字符串就用String

什么是切片slice(以字符串为例)

切片就是对集合中连续一部分元素的引用,不是整个集合

对字符串而言,切片就是对String类型中某一部分的引用,看起来长这样

let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

字符串切片语法是:&s[start..end],讲中文就是[开始索引..终止索引]

值得注意的是,开始索引是切片中第一个元素的索引位置,而终止索引是最后一个元素后面的索引位置,也就是说,这是一个左闭右开区间

对字符串切片时,必须落在UTF-8边界上,否则会panic

就比如”中国人“这三个字,每个汉字三个字节,想要取这个”中“字,完整应该是&s[0..3],如果用&s[0..2]就会崩溃

如果像索引从0开始,可以不加这个0:&s[..3]

切片+借用规则

fn first_word(s: &String) -> &str 看这句代码,他说明:

  • 借用一个String,不拿所有权

  • 返回它里面的一段&str切片

再来看这段错误代码

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s);

    s.clear(); // error!

    println!("the first word is: {}", word);
}
fn first_word(s: &String) -> &str {
    &s[..1]
}

这段会报错的问题就是:违反了不能同时存在可变与不可变借用

因为claer需要清空改变String,因此他需要一个可变借用,这个方法声明实际上是这样的pub fn clear(&mut self),参数是对自身的可变借用

总结一句话:有切片在用,就不能随便改原字符串

其他切片

不仅仅字符串有切片,其他集合类型也有,例如数组:

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

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);

这个数组切片类型是&[i32],数组切片和字符串切片工作方式一样

字符串字面量就是切片

"Hello,world!"的类型就是&str,所以这两种写法等价

let s = "Hello, world!";
let s: &str = "Hello, world!";

它指向的是可执行文件中的一段只读内存,所以字面量是不可变

所有 "..." 字面量,都可以当作 &str 切片看

String/&str之间怎么互相转换

&str->String

  • String::from("hello,world")

  • "hello,world".to_string()

String->&str

String类型转为&str需要取引用

fn main() {
    let s = String::from("hello,world!");
    say_hello(&s);
    say_hello(&s[..]);
    say_hello(s.as_str());
}

fn say_hello(s: &str) {
    println!("{}",s);
}

记住一点:要拥有它就变成String;只借用它就保持/转成&str

字符串索引:为啥不能用整数下表直接访问String

String底层是Vec<u8>(字节数组),而不是定长char数组

就比如说,英文"hello"长度五个字节,中文"你好“实际上是6个字节

索引s[0]得到的只是第一个字节,不是一个完整的字符,语义非常奇怪,所以Rust直接禁止String的整数索引

  • 更深一层:同一个字符串可以有多种“视角”——字节序列、char 序列、按人类语义的“字形簇”(比如梵文例子)。

  • 再加上字符边界不规则,想保证 s[i] 是 O(1) 也做不到,所以干脆不让你这样用。

上面的部分总结就一句话

String 持有,用 &str 借用;用 &s[a..b] 取一段;记住 UTF-8 边界 + 借用规则,不要直接用整数索引字符串。

⭐操作字符串

1. 追加(push/push_str)

作用:在尾部加东西,修改原String(前提必须要mut关键字)

  • push(ch:char):追加一个字符

  • push_str(s:&str):追加一个字符串切片

以上两种方式都是在原有字符串上进行修改,不会返回新字符串

fn main() {
    let mut s = String::from("Hello ");

    s.push_str("rust");
    println!("追加字符串 push_str() -> {}", s);

    s.push('!');
    println!("追加字符 push() -> {}", s);
}

运行结果:

追加字符串 push_str() -> Hello rust

追加字符 push() -> Hello rust!

总结:往后面加 → push / push_str,原地改,所以要 mut

2. 插入(insert / insert_str)

作用:在中间插东西(按照字节位置下标,越界会发生错误,必须mut

  • insert(idx,ch):在位置idx处插入一个char

  • insert_str(idx,s):在位置idx处插入一个&str

fn main() {
    let mut s = String::from("Hello rust!");
    s.insert(5, ',');
    println!("插入字符 insert() -> {}", s);
    s.insert_str(6, " I like");
    println!("插入字符串 insert_str() -> {}", s);
}

运行结果:

插入字符 insert() -> Hello, rust!

插入字符串 insert_str() -> Hello, I like rust!

总结:下标按 字节 算,中文要小心 UTF-8 边界

3. 替换(replace/replacen/replace_range)

替换有三种方式,依次来讲

3.1 replace

作用:接收两个参数,第一个是要被替换的字符串,第二个是新的字符串;把所有匹配字符串都替换掉,返回新的String不改原字符串,适用于String/&str

fn main() {
    let string_replace = String::from("I like rust. Learning rust is my favorite!");
    let new_string_replace = string_replace.replace("rust", "RUST");
    dbg!(new_string_replace);
}

运行结果:

new_string_replace = "I like RUST. Learning RUST is my favorite!"

3.2 replacen

作用:接收三个参数,前两个和replace一样,第三个表示替换的个数;只替换前N个匹配字符串,返回新的String不改原字符串,适用于String/&str

fn main() {
    let string_replace = "I like rust. Learning rust is my favorite!";
    let new_string_replacen = string_replace.replacen("rust", "RUST", 1);
    dbg!(new_string_replacen);
}

运行结果:

new_string_replacen = "I like RUST. Learning rust is my favorite!"

3.3 replace_range

作用:接收两个参数,第一个是要替换字符串的范围(Range),第二个参数是新的字符串;替换掉一定范围内匹配的字符串,直接操作原来的字符串,不会返回新的字符串,所以还要使用mut关键字,仅使用于String类型

fn main() {
    let mut string_replace_range = String::from("I like rust!");
    string_replace_range.replace_range(7..8, "R");
    dbg!(string_replace_range);
}

运行结果:

string_replace_range = "I like Rust!"

上述三个方法总结:

要返回新字符串就用replace/replacen

想修改原字符串用replace_range(要mut关键字)

4. 删除(pop / remove / truncate / clear)

上述四种方式都只适用于String,都会直接修改原字符串

4.1 pop——删除并返回字符串最后一个字符

返回值是一个Option类型,如果字符串为空,返回None

fn main() {
    let mut string_pop = String::from("rust pop 中文!");
    let p1 = string_pop.pop();
    let p2 = string_pop.pop();
    dbg!(p1);
    dbg!(p2);
    dbg!(string_pop);
}

运行结果:

p1 = Some(
   '!',
)
p2 = Some(
   '文',
)
string_pop = "rust pop 中"

4.2 remove——删除并返回字符串中指定位置的字符

只接收一个参数,表示该字符起始索引位置;存在返回值;按照字节处理字符串,如果参数不合法会发生错误

fn main() {
    let mut string_remove = String::from("测试remove方法");
    println!(
        "string_remove 占 {} 个字节",
        std::mem::size_of_val(string_remove.as_str())
        //这里的::是rust里的路径分隔符,就是为了写一个完整的路径
        //读出来就是”调用标准库里mem模块里的size_of_val这个函数
    );
    // 删除第一个汉字
    string_remove.remove(0);
    // 下面代码会发生错误
    // string_remove.remove(1);
    // 直接删除第二个汉字
    // string_remove.remove(3);
    dbg!(string_remove);
}

因为删除的是中文,一个汉字三个字节,所以索引值为1不合法,会发生错误

运行结果:

string_remove 占 18 个字节
string_remove = "试remove方法"

4.3 truncate——删除字符串中指定位置开始到结尾的全部字符

和上面类似,只接收一个参数,表示该字符起始索引位置;这个方法没有返回值,按照字节处理字符串,如果位置不合法也会发生错误

fn main() {
    let mut string_truncate = String::from("测试truncate");
    string_truncate.truncate(3);
    dbg!(string_truncate);
}

运行结果:

string_truncate = "测"

4.4 clear——清空字符串

删除字符串中的所有字符,相当于truncate(0)

fn main() {
    let mut string_clear = String::from("string clear");
    string_clear.clear();
    dbg!(string_clear);
}

运行结果:

string_clear = ""

5. 连接字符串(+/+=/format!)

5.1 +/+=

当使用+这个方法时,实际上就是用std::string 标准库中的 add() 方法

add(self, s: &str) -> String

左边:String(会被move)

右边:必须是&str&String会自动变成&str

+会返回一个新的字符串,变量声明可以不需要mut关键字

+=则是在原字符串上做修改,需要mut关键字

fn main() {
    let string_append = String::from("hello ");
    let string_rust = String::from("rust");
    // &string_rust会自动解引用为&str
    let result = string_append + &string_rust;
    let mut result = result + "!"; // `result + "!"` 中的 `result` 是不可变的
    result += "!!!";

    println!("连接字符串 + -> {}", result);
}

运行结果:

连接字符串 + -> hello rust!!!!

5.2 format!

这种方法使用于String&str,使用方式和print!类似

fn main() {
    let s1 = "hello";
    let s2 = String::from("rust");
    let s = format!("{} {}!", s1, s2);
    println!("{}", s);
}

运行结果:

hello rust!

字符串转义

我们可以通过转义的方式 \ 输出 ASCII 和 Unicode 字符

fn main() {
    // 通过 \ + 字符的十六进制表示,转义输出一个字符
    let byte_escape = "I'm writing \x52\x75\x73\x74!";
    println!("What are you doing\x3F (\\x3F means ?) {}", byte_escape);

    // \u 可以输出一个 unicode 字符
    let unicode_codepoint = "\u{211D}";
    let character_name = "\"DOUBLE-STRUCK CAPITAL R\"";

    println!(
        "Unicode character {} (U+211D) is called {}",
        unicode_codepoint, character_name
    );

    // 换行了也会保持之前的字符串格式
    // 使用\忽略换行符
    let long_string = "String literals
                        can span multiple lines.
                        The linebreak and indentation here ->\
                        <- can be escaped too!";
    println!("{}", long_string);
}

某些情况下,想要保持字符串原样,不要转义

fn main() {
    println!("{}", "hello \\x52\\x75\\x73\\x74");
    let raw_str = r"Escapes don't work here: \x3F \u{211D}";
    println!("{}", raw_str);

    // 如果字符串包含双引号,可以在开头和结尾加 #
    let quotes = r#"And then I said: "There is no escape!""#;
    println!("{}", quotes);

    // 如果字符串中包含 # 号,可以在开头和结尾加多个 # 号,最多加255个,只需保证与字符串中连续 # 号的个数不超过开头和结尾的 # 号的个数即可
    let longer_delimiter = r###"A string with "# in it. And even "##!"###;
    println!("{}", longer_delimiter);
}

上面这些我看的不太懂,扩展一下知识:

字符、ASCII、Unicode 分别是什么

字符(character)

字母、数字、标点、汉字甚至emoji表情,这些都是字符

但是电脑里只能存0/1,所以需要一套规则把“字符-数字-字节”对应起来,这个规则就叫字符编码

ASCII

这时最早的一套编码标准

它只规定了128个字符(0-127):

  • 英文字母A-Z/a-z

  • 数字0-9

  • 常见标点

  • 加上一些控制符(换行、回车等看不见的字符)

每个ASCII字符用1个字节的低7位表示

简单理解来说,就是只够英文用,什么汉语、日文表情都不够

Unicode

这是一个大一统标准

世界上所有的文字、符号都给一个统一的编号,叫做码点(code point),就像“中”这个字就写成U+4E2D

编号只是抽象概念,落到字节上,还要有具体的编码方式

常见:UTF-8、UTF-16、UTF-32

Rust字符串默认用UTF-8编码

Unicode是个巨大的全集,ASCII的0-127也是它的一部分

学到这里,我发现我学这个并没什么用,因为这tm是编译器干的事...

元组

元组是由多个类型组合在一起的复合类型

元组的长度是固定的,元组中的元素顺序也是固定的

创建元组语法:

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

变量tup被绑定了一个元组值(500,6.4,1),这个元素类型是(i32, f64, u8)

元组就是把多个类型用括号括在一起

可以使用模式匹配或者.操作符来获取元组中的值

用模式匹配解构元组

fn main(){
  let tup = (500, 6.4, 1);
  let (x,y,z) = tup;
  println!("这个y的值是{}",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;
}

元组的使用场景

元组常用在函数返回值,可以使用元组返回多个值

fn main() {
    let s1 = String::from("hello");

    let (s2, len) = calculate_length(s1);

    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() 返回字符串的长度

    (s, length)
}

这里函数接收了s1字符串的所有权,然后计算长度,最后把字符串所有权和字符串长度返回给s2和len变量

结构体

结构体和元组类似,都是由多种类型组合而成,不一样的是,结构体可以为内部的每一个字段起一个名称,无需再依赖字段的顺序访问解析,因此结构体更加灵活和强大

结构体语法

定义结构体:

一个结构体由三个部分组成:

  1. 通过关键字struct定义

  2. 一个清晰明确的结构体名称

  3. 几个有名字的结构体字段

创建结构体实例

为了使用上述结构体,需要创建User的结构体实例

    let user1 = User {
        active: true,
        username: String::from("张三"),
        email: String::from("lvdl0835@gmail.com"),
        sign_in_count: 1,
    };

要注意的是:

初始化实例时,每个字段都需要进行初始化

初始化时的字段顺序不需要和结构体定义的顺序一样

访问结构体字段

    let mut user1 = User {
        active: true,
        username: String::from("张三"),
        email: String::from("lvdl0835@gmail.com"),
        sign_in_count: 1,
    };
    user1.username = String::from("李四");

通过.操作符可以访问结构体实例内部的字段值,也可以修改(前提是结构体实例声明为可变的,要有mut

rust不支持将某个字段标记为可变

简化结构体创建

可以写一个构建函数简化创建

fn build_user(email: String, username: String) -> User {
    User {
        email: email,
        username: username,
        active: true,
        sign_in_count: 1,
    }
}

这个函数接收两个参数email和username

struct User {
    email: String,
    username: String,
    active: bool,
    sign_in_count: u64,
}

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}
fn main() {
    let user1 = build_user(String::from("lvdl0835@gmail.com"), String::from("张三"));
    println!("{}", user1.username);
}

调用这个函数并传入email和username直接就能创建一个结构体

结构体更新语法

实际场景中,常见一种情况就是,根据已有的结构实例,创建一个新的

就比如根据user1创建一个新的user2

  let user2 = User {
        active: user1.active,
        username: user1.username,
        email: String::from("another@example.com"),
        sign_in_count: user1.sign_in_count,
    };

这种写法可以简化,因为除了email做了修改,其他都一样

简化版:

  let user2 = User {
        email: String::from("another@example.com"),
        ..user1
    };

结构体更新语法和赋值语句=很像,因此在代码中,user1的部分字段所有权被转移到了user2中,username字段发生了所有权转移,user1无法再被使用

为什么只有username发生了所有权转移?因为剩下两个字段都是简单类型,只用copy给user2就可以了

注意,虽然username的所有权转移给了user2,导致user1无法再被使用,但是不代表其他字段不能再被使用

let user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};
let user2 = User {
    active: user1.active,
    username: user1.username,
    email: String::from("another@example.com"),
    sign_in_count: user1.sign_in_count,
};
println!("{}", user1.active);
// 下面这行会报错
println!("{:?}", user1);//用调试格式打印整个结构体

结构体的内存排列

#[derive(Debug)]
 struct File {
   name: String,
   data: Vec<u8>,
 }

 fn main() {
   let f1 = File {
     name: String::from("f1.txt"),
     data: Vec::new(),
   };

   let f1_name = &f1.name;
   let f1_length = &f1.data.len();

   println!("{:?}", f1);
   println!("{} is {} bytes long", f1_name, f1_length);
 }

在这段代码中,上面定义的File结构体在内存中的排列方式如下

元组结构体

结构体必须要有名称,但结构体的字段可以没有名称,这种结构体长得很像元组,所以称为元组结构体

    struct Color(i32, i32, i32);
    struct Point(i32, i32, i32);

    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);

这个主要用在希望有一个整体名称,但是不关心字段名称,就比如Point元组结构体,可以用来描述3D点(x,y,z)形式的坐标点,无需给里面逐一命名成下x,y,z

单元结构体

这种结构体没有任何字段,只有个名字,长成下面这样

struct MyType;

问题来了,他没有任何字段,存不了东西,那他是干嘛的?

  1. 作为标记类型/标签

比如你要实现一个 特征,但这个特征的实现不需要额外数据,只是提供一组函数:

struct MyHasher; // 单元结构体,只表示一种“算法”

impl MyHasher {
    fn hash(s: &str) -> u64 {
        // ...这里写算法...
        0
    }
}

fn main() {
    let result = MyHasher::hash("hello");
}

MyHasher 本身不需要存任何状态,只是一个「名字 + 函数集合」

  1. 作为配置 / 策略占位——这个后续用到在学

  2. 作为「错误类型 / 自定义类型」但不需要额外信息

use std::fmt;

#[derive(Debug)]
struct MyError; // 不携带具体信息,只表示「发生了某种错误」

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "something went wrong")
    }
}

这个目前了解一下就好