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 中的复合类型就到这里了。

参考

相关推荐
小小竹子4 分钟前
前端vue-实现富文本组件
前端·vue.js·富文本
万物得其道者成12 分钟前
React Zustand状态管理库的使用
开发语言·javascript·ecmascript
小白小白从不日白12 分钟前
react hooks--useReducer
前端·javascript·react.js
下雪天的夏风25 分钟前
TS - tsconfig.json 和 tsconfig.node.json 的关系,如何在TS 中使用 JS 不报错
前端·javascript·typescript
diygwcom36 分钟前
electron-updater实现electron全量版本更新
前端·javascript·electron
volodyan40 分钟前
electron react离线使用monaco-editor
javascript·react.js·electron
^^为欢几何^^1 小时前
lodash中_.difference如何过滤数组
javascript·数据结构·算法
Hello-Mr.Wang1 小时前
vue3中开发引导页的方法
开发语言·前端·javascript
程序员凡尘1 小时前
完美解决 Array 方法 (map/filter/reduce) 不按预期工作 的正确解决方法,亲测有效!!!
前端·javascript·vue.js
编程零零七5 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql