likes
comments
collection
share

五、复合类型

作者站长头像
站长
· 阅读数 11

  元组

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

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);
}

  结构体

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

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,
}

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

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, 但不需要在类型中存储具体的数据。

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

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

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 类型。使用 ::引用枚举成员。

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 中,可以将数据直接放进每一个枚举成员,而不是将枚举作为结构体的一部分。直接将数据附加到枚举成员上,无需创建一个新的结构体。

#[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)
}

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

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)」。

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 的所有情况都应得到处理。

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 中的一个分支,简化重复代码。

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 中,数组是直接分配到栈上的,读写速度比较快。然而,在真实业务场景中,往往会对数组进行裁剪,追加。由于数组的大小是固定的,只能创建新的数组,并将元素赋值到新的数组,增加了处理复杂度。那有没有其他方案呢?

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 加一。从而避免不断在栈上复制数组。

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 进行编码,一个字符占用四个字节

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

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

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 的引用类型。

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

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。

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 一样久。

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)
    }
}