Rust 笔记(三)复合类型

Rust 是一门强调安全、并发、高效的系统编程语言。无 GC 实现内存安全机制、无数据竞争的并发机制、无运行时开销的抽象机制,是 Rust 独特的优越特性。 它声称解决了传统 C 语言和 C++语言几十年来饱受责难的内存安全问题,同时还保持了很高的运行效率、很深的底层控制、很广的应用范围, 在系统编程领域具有强劲的竞争力和广阔的应用前景。

在 Rust 笔记(二)中,讲了基本类型,本文就认识一下 Rust 中的复合类型。

1.字符串类型

在 JS 中:'a'、'abc' 这样的都叫字符串,数据类型是 String,但是在 Rust 中不太一样,字符串还会细分分为三种类型,上一小节的「字符类型」还有「字符串切片类型:String」和「字符串类型: &str」。

rust 复制代码
let _char: char = 'hello';
let _str: &str = "hello world";
let _string: String = String::from("hello world");

可能会比较懵逼?这 TM 不是一个类型吗?Rust 中最令人困惑的问题之一是字符串和切片(str)概念。 Rust 中的字符串是一个结构,它结合了一个指向字符串的内存分配的指针,"len" 是使用的字节数,"容量"是从内存中分配的缓冲区大小。通常得到的"容量"大于等于"len"。

rust 复制代码
fn main() {
    let _string: String = String::from("hello world");
    println!("_string 的长度: {}", _string.len());
    println!("_string 的容量: {}", _string.capacity());
}

字符串切片类型和字符串类型类似,对于字符串而言,切片就是对 String 类型中某一部分的引用:

rust 复制代码
let _s: String = String::from("Hello World");
let _hello: &str = &_s[0..5];
let _world: &str = &_s[6..11];

这其中 _hello 和 _world 就是对 _s 的部分引用,通过[开始索引..终止索引]这样的操作就是创建切片的语法。其中开始索引是切片中第一个元素的索引位置,而终止索引是最后一个元素后面的索引位置,也就是这是一个 右半开区间。在切片数据结构内部会保存开始的位置和切片的长度,其中长度是通过 终止索引 - 开始索引 的方式计算得来的。

不过在对字符串使用切片语法时需要格外小心,切片的索引必须落在字符之间的边界位置,也就是 UTF-8 字符的边界,例如中文在 UTF-8 中占用三个字节,下面的代码就会崩溃:

rust 复制代码
let s = "中国人";
let a = &s[0..2];
println!("{}",a);

因为我们只取 s 字符串的前两个字节,但是本例中每个汉字占用三个字节,因此没有落在边界处,也就是连 中字都取不完整,此时程序会直接崩溃退出,如果改成 &s[0..3],则可以正常通过编译。

其实本质上在 Rust 语法中,只有一种字符串类型,那就是 &str 也就是上面说到字符串切片,但是在标准库中还有很多其他用途的字符串类型,比如 String、OsString、OsStr、CsString、CsStr。

字符串操作

push & push_str

字符串使用 push 方法追加字符。 字符串使用 push_str 方法追加字符串。

rust 复制代码
let mut str = String::from("Hello World");
str.push('baixiaobai');
println!("push 追加字符 {}", str);
str.push_str(", hihihihi");
println!("push_str 追加字符串 {}", str);

insert & insert_str

字符串使用 insert 方法插入字符。 字符串使用 insert_str 方法插入字符串。

rust 复制代码
let mut str = String::from("Hello World");
str.insert(5, ',');
println!("insert 插入字符 {}", str);
str.insert_str(str.len(), " hihihihi");
println!("insert_str 插入字符串 {}", str);

replace & replacen & replace_range

字符串使用 replace 方法替换字符串,第一个参数是要替换的字符串,第二个参数是新的字符串,replace 是匹配到字符串全部替换。该方法返回的是新字符串。

字符串使用 replacen 方法替换字符串,它和 replace 不同的是,它有第三个参数,表示替换的个数。该方法返回的是新字符串。

字符串使用 replace_range 方法替换字符串,第一个参数要替换字符串的范围,第二个参数是新的字符串。该方法直接操作原字符。

rust 复制代码
let mut str = String::from("Hello Rust, hello rust, Hello Rust, hello rust");
let replace_str = str.replace("Rust", "rust");
println!("replace 替换字符 {}", replace_str);
let replace_str = str.replacen("Rust", "rust", 1);
println!("replacen 替换字符 {}", replace_str);
str.replace_range(6..7, "_R");
println!("replace_range 替换字符 {}", str);

pop & remove & truncate & clear

字符串使用 pop 方法删除并返回字符串的最后一个字符串。该方法直接操作原字符串,返回值是一个 Option 类型,如果字符串为空,则返回 None。

字符串使用 remove 方法删除并返回字符串中指定位置的字符。该方法只接受一个参数,返回删除位置的字符串,该方法直接操作原字符串,但是需要注意的是,这个方法是按照字节来处理字符串的,如果参数给的位置不是合法的字符边界,就会报错。

字符串使用 truncate 方法删除从指定位置开始到结束的全部字符,没有返回值。该方法直接操作原字符串,如果参数不是合并的字符串边界会报错。

字符串使用 clear 方法清空字符串,该方法直接操作原字符。

rust 复制代码
let mut str = String::from("_Hello World!");
let str_pop = str.pop();
println!("pop 删除后字符串 {}", str);
dbg!(str_pop);
let str_remove = str.remove(0);
println!("pop 删除后字符串 {},删除的字符串 {}", str, str_remove);
str.truncate(6);
println!("pop 删除后字符串 {}", str);
str.clear();
println!("pop 删除后字符串 {}", str);

+ & +=

字符串使用 + 或者 += 来连接字符串,但是右边的参数必须是字符串切片类型。

rust 复制代码
let str1 = String::from("Hello World!"); 
let str2 = String::from(" hihihihi"); 
let res = str1 + &str2;
println!("res: {}", res);

chars 方法 & bytes 方法 字符串使用 char 方法遍历字符。 字符串使用 bytes 方法遍历字节。

rust 复制代码
let str1 = String::from("Rust 笔记(三)复合类型");  
for s in str1.chars() {
  println!("字符: {}", s);
}
for b in str1.bytes() {
  println!("字节: {}", b);
}

String 与 &str 类型转换

String 转换为 &str 很简单,取引用就行。

rust 复制代码
fn main() {
    let s: String = String::from("Hello, world!");
    con(&s);
}

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

&str 转换为 String 类型有两种方案:

  • String::from("xxx")
  • "xxx".to_string(()
rust 复制代码
let _s: String = String::from("Hello World");
let _hello: &str = &_s[0..5];
let _world: &str = &_s[6..11];
let _hello_string = String::from(_hello);
let _world_string = _world.to_string();

字符串的索引

在 JS 中使用索引来范围字符串很正常,但是,但是,但是,Rust 不太行。 原因很简单,Rust 字符串底层存储格式是 u8,举个例子:对于 "hello" 来说,它每个字母咱 UTF-8 中都只是占用一个字符,但是对于"中国"这个字符来说,它每个汉字都占三个字符。

所以使用 [0] 索引只访问到 1/3 ,而不是完整的 "中"这个汉字。

2.元组

元组就是多种类型组合在一起,长度固定,类型固定。

rust 复制代码
let _tup: (i32, f64, u8, u32) = (500, 6.4, 1, 300);

而获取元组的内容,可以使用匹配模式或者 . 来获取元组中的内容。

rust 复制代码
let _tup: (i32, f64, u8, u32) = (500, 6.4, 1, 300);
let (x, y, z, m) = _tup;
println!("x = {}, y = {}, z = {}, m = {}", x, y, z, m);
let x1 = _tup.0;
let y1 = _tup.1;
let z1 = _tup.2;
let m1 = _tup.3;
println!("x1: {}, y1: {}, z1: {}, m1: {}", x1, y1, z1, m1);

3.枚举

整体enum的定义非常简单也符合我们的直观感受。

rust 复制代码
enum Gender { 
  Unspecified = 0, 
  Female = 1, 
  Male = 2,
}

但是访问的时候稍微麻烦一点儿,因为我们需要在运行期间判断具体的类型,所以match匹配语法就成了必需品。if let是匹配语法的一种缩写形式。整体的匹配语法还是很友好的。

rust 复制代码
#[derive(Debug)]
enum Gender { 
    Unspecified, 
    Female, 
    Male,
    GenderInfo{x: i32, y: i32}
}
fn person_enum() {
    // 枚举成员
    let res1 = Gender::Unspecified;
    let res2 = Gender::Female;
    let res3 = Gender::Male;
    let mut res4 = Gender::GenderInfo {x: 1, y: 2};

    println!("{:?} {:?} {:?}", res1, res2, res3);

    // 枚举查询
    if let Gender::GenderInfo{x, y} = res4 {
        println!("x: {}, y: {}", x, y);
    }

    // 枚举更新
    match res4 {
        Gender::GenderInfo{ref mut x, ref mut y} => {
            *x = 123;
            *y = 321;
        },
        _ =>()
    }
    
    if let Gender::GenderInfo{x, y} = res4 {
        println!("x: {}, y: {}", x, y);
    }
}
// x: 1, y: 2
// x: 123, y: 321

4.数组

Rust 的数组定义/初始化/更新都十分直观,唯一需要主要的是[i32;4]语法,这里分割类型和数量使用的是分号,比较特别需要记忆一下。 在 Rust 中最常用的数组有两种:

  • 长度固定的 array,但是速度快,它称为数组,存储在栈上。
  • 长度不固定的 VEVTOR,但是性能偏弱,它称为动态数组,存储在堆上。
rust 复制代码
fn person_array() {
    // 初始化
    let mut arr: [i32; 5] = [1, 2, 3, 4, 5];

    // 访问
    println!("[0]: {}, [1]: {}, [2]: {}, [3]: {}, [4]: {}", arr[0], arr[1], arr[2], arr[3],  arr[4]);

    // 更新
    arr[0] = 100;
    arr[1] = 200;
    arr[2] = 300;
    arr[3] = 400;
    arr[4] = 500;

    // 访问
    println!("[0]: {}, [1]: {}, [2]: {}, [3]: {}, [4]: {}", arr[0], arr[1], arr[2], arr[3],  arr[4]);

    // 二维数组初始化
    let mut arr2: [[i32; 3]; 3] = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
    
    // 访问
    println!("[0][0]: {}, [0][1]: {}, [0][2]: {}", arr2[0][0], arr2[0][1], arr2[0][2]);
    println!("[1][0]: {}, [1][1]: {}, [1][2]: {}", arr2[1][0], arr2[1][1], arr2[1][2]);
    println!("[2][0]: {}, [2][1]: {}, [2][2]: {}", arr2[2][0], arr2[2][1], arr2[2][2]);

    // 更新
    arr2[0][0] = 100;
}

其实不管字符串可以切片,数组也可以切片。

rust 复制代码
let a: [i32; 5] = [1, 2, 3, 4, 5];
let slice: &[i32] = &a[1..3];
assert_eq!(slice, &[2, 3]);

示例代码中数组切片 slice 的类型是&[i32],与之对比,数组的类型是[i32;5],简单总结下切片的特点:

  • 切片的长度可以与数组不同,并不是固定的,而是取决于你使用时指定的起始和结束位置。
  • 创建切片的代价非常小,因为切片只是针对底层数组的一个引用。
  • 切片类型[T]拥有不固定的大小,而切片引用类型&[T]则具有固定的大小,因为 Rust 很多时候都需要固定大小数据类型,因此&[T]更有用,&str字符串切片也同理。

5.结构体

结构体是最为最为常用的类型,这个类型等价于其他一些面向对象语言的对象类型。结构体的定义包含几个部分:

  • struct 关键字
  • 名称
  • 结构体内容
rust 复制代码
struct Person {
  name: String,
  age: u32
}

创建结构体实例也比较简单。但是在创建的时候记得每一个字段都要初始化。

rust 复制代码
let mut person = Person {
  name: String::from("Mr. baixiaobai"),
  age: 18
};

我们可以通过 . 来访问结构体的字段。

rust 复制代码
println!("person: {}", person.name);     // person: Mr. Hello

也可以修改结构体字段内容。

rust 复制代码
 person.name = String::from("Mr. World");

根据已有的结构体实例,创建新的结构体实例,例如根据已有的 user1 实例来构建 person,

rust 复制代码
let person = Person {
    name: user1.name,
    age: user1.age,
    sex: 0
};

这里其实可以如 TS 一样,不用一个一个赋值,而是使用 ..。.. 语法表明凡是我们没有显式声明的字段,全部从 user1 中自动获取。需要注意的是 ..user1 必须在结构体的尾部使用。并且这里是两个点,不是三个。

rust 复制代码
let person = Person {
    sex: 0,
    ..user1
};

如果你想你的结构体没有具体的字段名称,那你可以使用元祖结构体,元组结构体在你希望有一个整体名称,但是又不关心里面字段的名称时将非常有用。

rust 复制代码
struct Point(i32, i32, i32);

如果你想你的结构体没有任何的字段和属性,那你可以使用单元结构体。

rust 复制代码
struct Project;

我们在做代码调试的时候,需要注意使用 #[derive(Debug)] 对结构体进行了标记,这样才能使用 println!("{:?}", s);

rust 复制代码
#[derive(Debug)]
struct Person {
    name: String,
    age: u32,
    sex: i32
}
fn main() {

    let person = Person {
        name: String::from("Mr. baixiaobai"),
        age: 18,
        sex: 0
    };
    println!("person: {:?}", person);  
}

当结构体较大时,我们可能希望能够有更好的输出表现,此时可以使用 {:#?} 来替代 {:?}。

还有一个简单的输出 debug 信息的方法,那就是使用 dbg! 宏。

rust 复制代码
dbg!(&person);

总结

本章的重点在复合类型上,复合类型是由其它类型组合而成的,最典型的就是结构体 struct 和枚举 enum。本文 Rust 中的复合类型就到这里了。

参考

相关推荐
liuyouzhang9 分钟前
将基于Archery的web数据库审计查询平台封装为jdbc接口的可行性研究(基于AI)
前端·数据库
码事漫谈6 小时前
大模型输出的“隐性结构塌缩”问题及对策
前端·后端
这儿有一堆花6 小时前
前端三件套真的落后了吗?揭开现代 Web 开发的底层逻辑
前端·javascript·css·html5
.Cnn7 小时前
JavaScript 前端基础笔记(网页交互核心)
前端·javascript·笔记·交互
醉酒的李白、7 小时前
Vue3 组件通信本质:Props 下发,Emits 回传
前端·javascript·vue.js
anOnion7 小时前
构建无障碍组件之Window Splitter Pattern
前端·html·交互设计
NotFound4867 小时前
实战分享Python爬虫,如何实现高效解析 Web of Science 文献数据并导出 CSV
前端·爬虫·python
徐小夕8 小时前
PDF无限制预览!Jit-Viewer V1.5.0开源文档预览神器正式发布
前端·vue.js·github
WangJunXiang68 小时前
Haproxy搭建Web群集
前端
吴声子夜歌8 小时前
Vue.js——自定义指令
前端·vue.js·flutter