五、复合类型

元组

元组是将多种类型的多个值合到一个复合类型中的一种基本方式。元组的元素数量是固定的,声明之后,无法增长或缩小。可以用于函数的多值返回。

rust 复制代码
fn main() {
    // 声明一个元组类型的变量
let tup: (i32, u8, f64) = (500, 6, 1.0);
    // 元组解构,将元组的元素赋值给单个变量
let (x, y, z) = tup;
    println!("x: {}, y:{}, z:{}", x, y, z);
    // 通过索引访问
println!("{}, {}, {}", tup.0, tup.1, tup.2);
    // 特殊类型,被称为单元类型。如果表达式不返回任何其他值
let unit_tuple = ();
    println!("{:?}", unit_tuple);
    println!("{:?}", add(1, 2))
}

fn add(a: i32, b: i32) -> (i32, i32, i32) {
    return (a, b, a + b);
}

结构体

和元组类似,结构体每一部分可以是不同类型。与元组不同的是,结构体需要命名各个元素以便清楚的表明值的含义。元组的优点在于创建简单,可以快速使用,缺点就是可读性不高,访问的时候通过下标访问。

rust 复制代码
fn main() {
let mut u1 = User {
        name: String::from("name"),
        content: String::from("content"),
    };
    // 更新属性
u1.name = String::from("name2");
    u1 = newUser(u1.name, u1.content);
    // 从现有结构体值中创建新的值,并更新部分值
let u2 = User {
        name: String::from("name2"),
        ..u1 // 剩余字段未显式设置值的字段则使用 u1 的字段值
};
    // 无法使用 u1.content, 因为 u1.content 的所有权发生转移, 转移到了 u2
 // println!(
 //     "u2.name: {}, u2.content: {}, u1.name:{}, u1.content{}",
 //     u2.name, u2.content, u1.name, u1.content
 // )
println!(
        "u2.name: {}, u2.content: {}, u1.name:{}",
        u2.name, u2.content, u1.name
    )
}

// 如果参数名和结构体属性名相同,可以简化写法,避免指定属性赋值
fn newUser(name: String, content: String) -> User {
    User { name, content }
}

struct User {
    name: String,
    content: String,
}

此外,可以定义元组结构体。元组结构体有着结构体名称提供的含义,但是没有具体的字段名,只有字段类型。对于一些不需要知道结构体属性名字的场景,可以使用元组结构体,从而对元组类型进行区分。

rust 复制代码
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
    // 接收 Point 类型的函数不能接收 Color 类型的值
let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

对于没有任何字段的结构体,称之为类单元结构体。常用于在某个类型上实现 tarit, 但不需要在类型中存储具体的数据。

rust 复制代码
struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

结构体方法 :与函数类似,同样使用 fn 关键字和名称声明,可以拥有参数和返回值。与函数不同的是,他们在结构体的上下文中被定义,且它们的第一个参数总是 self,代表调用该方法的结构体实例。如下面代码所示,对于结构体方法,第一个参数必须是实例本身,可以为 self「所有权转移」、&self「不可变引用」、&mut self「可变引用」,且需要定义在 impl 中。此外,对于第一参数非 self 的函数,我们称之为关联函数,调用方式为 User::fly()。这个方法位于结构体的命名空间中,:: 语法用于关联函数和模块创建的命名空间。

rust 复制代码
fn main() {
    let mut u = User {
        name: String::from("hello"),
        content: String::from(""),
        age: 19,
    };
    // 调用 u.get_name, 会将 u 的所有权转移到函数,无法使用 u.is_adult()
 // println!("user: {} adult: {}", u.get_name(), u.is_adult());
println!("user: {} adult: {}", u.name, u.is_adult());
    u.set_age(17);
    println!("user:{} adult: {}", u.name, u.is_adult());
    // 调用关联函数
println!("fly: {}", User::fly())
}

struct User {
    name: String,
    content: String,
    age: i32,
}

impl User {
    // 非可变引用
fn is_adult(&self) -> bool {
        return self.age > 18;
    }

    // 可变引用
fn set_age(&mut self, age: i32) {
        self.age = age
    }

    // 所有权发生转移
fn get_name(self) -> String {
        self.name
    }

    // 关联函数
fn fly() -> String {
        String::from("fly")
    }
}

结构体可以创建出在领域中有意义的自定义类型。通过结构体,可以将关联的数据片段和方法联系起来,使得代码更加清晰。

枚举

在上面介绍的类型中,我们可以设置一个变量为标量类型、元组、结构体。那么有没有办法设置一个变量,它的值是可列举的?这里就需要使用枚举类型。当然,在 Rust 中,枚举类型除了列举值,还可以存储数据,配置 match 进行模式匹配,使用 if let 简化枚举结构的处理。

定义枚举

下面的例子中,使用 enum 定义枚举类型,用于标识 ip 类型。使用 ::引用枚举成员。

rust 复制代码
enum IpAddrKind {
    V4,
    V6,
}

struct IpAddr {
    ip: String,
    kind: IpAddrKind,
}

fn main() {
    let v4 = IpAddr {
        ip: "127.0.0.1".to_string(),
        kind: IpAddrKind::V4,
    };
    let v6 = IpAddr {
        ip: "::1".to_string(),
        kind: IpAddrKind::V6,
    };
    process_ip(v6)
}

fn process_ip(ip: IpAddr) {}

在上面的例子中,定义了新一个新的结构体 IpAddr 来存储枚举成员和 String 值。在 Rust 中,可以将数据直接放进每一个枚举成员,而不是将枚举作为结构体的一部分。直接将数据附加到枚举成员上,无需创建一个新的结构体。

rust 复制代码
#[derive(Debug)]
enum IPAddr {
    v4(String),
    v6(String),
}
fn main() {
    let v4 = IPAddr::v4("127.0.0.1".to_string());
    let v6 = IPAddr::v6("::1".to_string());
    println!("{:?}, {:?}", v4, v6)
}

枚举的成员中可以存储任意类型的数据。上节中,我们知道结构体也可以存储任意类型的数据。那么枚举和结构体的区别是什么?什么时候使用枚举?什么时候使用结构体?枚举定义了一个变量的取值列举,即枚举类型变量的值只能是枚举值中的一个。在枚举的成员中,可以不包含任何数据、包含数据和匿名结构体。在函数传参中可以直接传递一个枚举类型,如果使用结构体,则需要将不同的结构体定义为函数的入参。在业务开发中,通常将一组互斥的行为定义为枚举。例如:消息的退出、移动、写操作。

rust 复制代码
enum Message {
    Quit,                    // 不关联任何数据
Move { x: i32, y: i32 }, // 匿名结构体
 Write(String),           // 包含 String
 Color(i32, i32, i32),    // 包含三个 i32
}

impl Message { // 使用枚举定义方法
    fn call(&self) {
        // 在这里定义方法体
    }
}

// 使用结构体定义
struct QuitMessage; // 类单元结构体
struct MoveMessage {
    // 结构体
x: i32,
    y: i32,
}
struct WriteMessage(String); // 元组结构体
struct ChangeColorMessage(i32, i32, i32); // 元组结构体

在 Rust 中,内置了一个 Option 的枚举类型,用于标识一个变量是否为空值。Option 枚举包含两个成员,一个表示空值「None」,一个用于存储具体值「Some(T)」。

rust 复制代码
pub enum Option<T> {
    None,
    Some(T),
}

fn main() {
    let someValue = Some(8);

    // let uncertainValue = None; //---- type must be known at this point
 // 对于不确定具体值的 Option 类型,需要定义存储值的类型
let uncertainValue: Option<i32> = None;
}

match 控制流运算符

上节中介绍了枚举类型的定义以及内置的 Option 类型。在 Rust 中可以使用 match 关键字对枚举类型进行模式匹配并执行相关代码。编译器确保了 match 的所有情况都应得到处理。

rust 复制代码
enum IpAddrKind {
    V4,
    V6,
}

fn process(kind: IpAddrKind) {
    match kind {
        // 需要处理所有的枚举类型
        IpAddrKind::V4 => {
            println!("connect v4")
        }
        IpAddrKind::V6 => {
            println!("connect v6")
        }
    }
}

enum IPAddr {
    v4(String),
    v6(String),
}

fn processIP(ip: IPAddr) {
    match ip {
        // 获取枚举中包含的值
        IPAddr::v4(v4) => {
            println!("v4: {}", v4)
        }
        // 使用 _ 占位符忽略枚举中包含的值
        IPAddr::v6(_) => {
            println!("not support v6")
        }
    }
}

fn main() {
    let k = 9;
    match k {
        1 => {}
        // 满足穷举性, 前面都匹配不到会走到这个 case, other 表示具体的值
other => {
            println!("math without 1 value: {}", other)
        }
    }
    match k {
        1 => {}
        // 满足穷举性, 在最后的分支中忽略值
_ => {
            println!("math without 1")
        }
    }
}

If let 简单控制流

在使用 Option 时,可以使用 if let 来简化代码。可以将 if let 看做是 match 中的一个分支,简化重复代码。

rust 复制代码
fn main() {
    let someValue = Some(8);
    match someValue {
        None => {}
        Some(v) => {
            println!("{}", v)
        }
    }
    // 使用 if let 简化代码, 其实就是 match 的语法糖, 只走了 Some(v) 这个分支
if let Some(v) = someValue {
        println!("{}", v)
    }
    
}

数组

数组是编程中经常用到的数据结构,它能够存储多个同类型的数据。数组有以下特点:固定大小、存储同类型元素、随机访问。在 Rust 中,数组是直接分配到栈上的,读写速度比较快。然而,在真实业务场景中,往往会对数组进行裁剪,追加。由于数组的大小是固定的,只能创建新的数组,并将元素赋值到新的数组,增加了处理复杂度。那有没有其他方案呢?

rust 复制代码
fn main() {
    // 初始化一个数组, 大小为 3, 初始值为 5
let arr = [3; 5];
    println!("arr: {:?}", arr);
    // 初始化大小为 5 的数组
let arr: [i32; 5] = [1, 2, 3, 4, 5];
    println!("arr: {}", arr.len());
    // 遍历
for a in arr {
        println!("{}", a)
    }
    // /索引, 如果索引溢出会直接退出函数
let mut arr1 = [0; 6];
    for (i, a) in arr.iter().enumerate() {
        arr1[i] = *a
    }
    arr1[5] = 6;
    println!("{:?}", arr1)
}

可以使用 vector 来解决数组固定大小的问题。vector 其实是一个结构体,它由三个字段组成 ptr: 指向堆内存的连续空间、len: 空间使用的长度、capacity: 空间总容量。往 vector 里面 push 数组时, 判断容量是否使用完,如果使用完则申请新的连续空间并拷贝数据,否则将 len 加一。从而避免不断在栈上复制数组。

rust 复制代码
fn main() {
    // 初始化
let mut v: Vec<i32> = Vec::new();
    println!("{}, {}", v.len(), v.capacity());
    v.push(8); // 扩容,容量为 4
v.push(9);
    println!("{}, {}", v.len(), v.capacity());
    // 遍历
for i in &v {
        println!("{}", i);
    }
    // 遍历更新
for i in &mut v {
        *i += 50
    }
    // 索引
println!("{}", v[1]);
    // 索引溢出,直接退出程序
 // println!("{}", v[9]);
    // 使用 get 方法,返回一个 Option
if let Some(v) = v.get(9) {
        println!("{}", v);
    } else {
        println!("can not get value with index 9")
    }
}

字符、字符串、切片

介绍字符相关操作之前,需要先介绍一下编码的相关概念。编码是信息从一种格式转化到另一种格式的过程。我们知道, 计算机数据存储时, 都是使用二进制的形式。编码 : 就是将字符「a」转换到二进制「1100001」进行存储; 解码: 就是将二进制「1100001」转换为字符「a」。那如何确定字符 「a」 要转换成 「1100001」 而不是 「1100000」 呢, 这就需要大家约定一个规则「对照表」, 来保证能够正常编码和解码。

Unicode ****是一个编解码对照表, 它收集了世界上所有的字符, 并为每一个字符分配了一个唯一的 Unicode 码点。对字符进行存储的时候, 可以将一个字符使用 int32「四个字节, 支持 2^32 个字符」 。

统一使用四个字节表示一个字符的方式比较简单。但是会浪费太多的存储的空间, 那有没有一种更好的编码方式呢?UTF-8 是一个将 Unicode 码点编码为字节序列的变长编码。在 UTF-8 编码中, 使用 1 到 4 个字节来表示每个Unicode 码点。每个符号编码后第一个字节的高端 bit 位用于表示编码总共有多少个字节。如果第一个字节的高端bit为 0,则表示对应 7bit 的 ASCII 字符; 如果第一个字节的高端 bit 是110, 则说明需要2个字节, 后续的每个高端bit都以 10 开头。更大的 Unicode 码点也是采用类似的策略处理。使用 UTF-8 可以节省存储空间, 但无法直接判断字节序列包含的字符个数, 也无法直接通过下标访问第 n 个字符。

字符「char」是 Rust 的标量类型,使用 Unicode 进行编码,一个字符占用四个字节

rust 复制代码
fn main() {
    println!("Size of char: {}", std::mem::size_of::<char>());
}

字符串 「String」是字符组成的连续集合,封装了各种对字符串处理的方法。字符串是使用 UTF-8 编码,即字符串中的字符所占字节数是变化的「1-4」。在 Rust 中,String 是一个结构体,里面包含三个字段 ptr、len、capacity,用于处理字符串相关操作。

rust 复制代码
fn main() {
    // len 表示 s1 持有 vec 使用的空间,capacity 持有 vec 的总容量,当容量不足时需要进行扩容
let str = String::from("hello");
    println!("str len: {}, cap: {}", str.len(), str.capacity());
    // 使用 utf-8 编码
let (str1, str2) = (String::from("中"), String::from("a"));
    println!(
        "str1 len: {}, cap: {}, str1 len: {}, cap: {}",
        str1.len(),
        str1.capacity(),
        str2.len(),
        str2.capacity()
    );
    for c in str.chars() {
        print!("{}", c)
    }
}

对于字符串而言,切片「&str」是对 String 类型中某一部分的引用。在下面的例子中,strString 类型,helloword&str 类型,也就是 str 的引用类型。

rust 复制代码
fn main() {
    let str = String::from("hello world");
    let hello = &str[0..5];
    let world = &str[6..11];
    println!("{} {}", hello, world)
}

从上面的 case 介绍了字符字符串字符串``切片,它们分别属于不同的数据类型「char、String、&str」。char 是一个 uncode 码点,占用四个字节。String 是一个可变大小的结构体,用于字符的拼接、替换等操作,编码后的数据存储在堆中。str是字符串字面值,编译时就知道其内容,最终被直接硬编码到可执行文件中。字符串字面值是不可变的,通常以引用「&str」的形式出现。

在具体开发中,可以使用 &str作为函数的入参和出参进行处理。使用 &str 不会对所有权进行转移,且可以转换为 String

rust 复制代码
fn main() {
    // "initial contents" 是一个字面值直接编译到可执行文件中, data 类型为 &str
let data = "initial contents";
    // copy 一份到 s 上
let mut s1 = data.to_string();
    let s2 = String::from("initial contents");
    s1.push_str(" * ");
    println!("{}", s1);
    // 字符串拼接, + 调用了 String 的 add 函数, 底层调用的 push 函数
let s3 = s1 + &s2; // 这里 s1 所有权移交,不能继续使用
println!("{}; {}", s3, s2);
    // len 返回占用字节数
println!("{}", String::from("中国").len());
    // 由于字符串使用的 utf-8 编码,因此无法直接通过索引来查看对应的字符
for c in String::from("中国").chars() {
        print!("{}", c)
    }
}

哈希 map

哈希 map 用来存储键值对。通过一个哈希函数来实现映射,决定如何将键值对放入内存中。数组是将相同类型的元素存储在连续的内存中,可通过索引进行访问。map 存储的是相同类型键值对「key, value」,可以直接通过 key 进行索引,快速找到对应的 value。

rust 复制代码
fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("zhang"), 10);
    scores.insert(String::from("wang"), 11);
    // get
let k = String::from("zhang");
    if let Some(v) = scores.get(&k) {
        println!("key: {}, value: {}", k, v)
    }
    //遍历
for (k, v) in &scores {
        println!("key: {}, value:{}", k, v)
    }
    // 覆盖
scores.insert(String::from("zhang"), 20);
    //遍历
for (k, v) in &scores {
        println!("key: {}, value:{}", k, v)
    }
    // 键不存在时插入
scores.entry(String::from("zhang")).or_insert(30);
    //遍历
for (k, v) in &scores {
        println!("key: {}, value:{}", k, v)
    }
    // 根据旧值更新新值
for (_, v) in &mut scores {
        *v += 1;
    }
    for (k, v) in &scores {
        println!("key: {}, value:{}", k, v)
    }
}

map 中,所有权仍然遵守 rust 的所有权机制。在 map 存储的 k, v 中,如果类型实现了 Copy,insert 时直接 copy 数据,否在将所有权转移到 map 中。当然,你也可以选择存储引用,但是必须要保证引用的生命周期至少要和 map 一样久。

rust 复制代码
fn main() {
    let k = String::from("k");
    let mut m = HashMap::new();
    // 所有权移交到 m
    m.insert(k, 0);
    // println!("k: {}, v: {}", k, 0) ^ value borrowed here after move
println!("{:?}", m);

    let mut m: HashMap<&String, i32> = HashMap::new();

    {
        let k = String::from("k");
        // m.insert(&k, 0); ^^ borrowed value does not live long enough
        // k 被回收
}
    println!("{:?}", m)
}

fn m() {
    let mut scores = HashMap::new();
    scores.insert(String::from("zhang"), 10);
    scores.insert(String::from("wang"), 11);
    // get
let k = String::from("zhang");
    if let Some(v) = scores.get(&k) {
        println!("key: {}, value: {}", k, v)
    }
    //遍历
for (k, v) in &scores {
        println!("key: {}, value:{}", k, v)
    }
    // 覆盖
scores.insert(String::from("zhang"), 20);
    //遍历
for (k, v) in &scores {
        println!("key: {}, value:{}", k, v)
    }
    // 键不存在时插入
scores.entry(String::from("zhang")).or_insert(30);
    //遍历
for (k, v) in &scores {
        println!("key: {}, value:{}", k, v)
    }
    // 根据旧值更新新值
for (_, v) in &mut scores {
        *v += 1;
    }
    for (k, v) in &scores {
        println!("key: {}, value:{}", k, v)
    }
}
相关推荐
2401_895521344 小时前
SpringBoot Maven快速上手
spring boot·后端·maven
disgare4 小时前
关于 spring 工程中添加 traceID 实践
java·后端·spring
ictI CABL4 小时前
Spring Boot与MyBatis
spring boot·后端·mybatis
小江的记录本6 小时前
【Linux】《Linux常用命令汇总表》
linux·运维·服务器·前端·windows·后端·macos
lUie INGA7 小时前
rust web框架actix和axum比较
前端·人工智能·rust
yhole9 小时前
springboot三层架构详细讲解
spring boot·后端·架构
香香甜甜的辣椒炒肉9 小时前
Spring(1)基本概念+开发的基本步骤
java·后端·spring
白毛大侠10 小时前
Go Goroutine 与用户态是进程级
开发语言·后端·golang
ForteScarlet10 小时前
从 Kotlin 编译器 API 的变化开始: 2.3.20
android·开发语言·后端·ios·开源·kotlin
大阿明10 小时前
SpringBoot - Cookie & Session 用户登录及登录状态保持功能实现
java·spring boot·后端