文章目录
- 复合类型元组
- 用模式匹配解构元组
- 用点来访问元组
- 元组的使用示例
- 复合类型结构体
- 结构体定义
- 结构体语法
- 创建结构体实例
- 访问结构体字段
- 简化结构体创建
- 结构体更新语法
- 结构体的内存排列
- 元组结构体(Tuple Struct)
- 单元结构体(Unit-like Struct)
- 结构体数据的所有权
- 打印结构体
- 复合类型枚举
复合类型元组
元组是由多种类型组合到一起形成的,因此它是复合类型,元组的长度是固定的,元组中元素的顺序也是固定的。
可以通过以下语法创建一个元组:
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!("The value of y is: {}", y);
}
上述代码首先创建一个元组,然后将其绑定到 tup
上,接着使用 let (x, y, z) = tup;
来完成一次模式匹配,因为元组是 (n1, n2, n3)
形式的,因此我们用一模一样的 (x, y, z)
形式来进行匹配,元组中对应的值会绑定到变量 x
, y
, z
上。
这就是解构:用同样的形式把一个复杂对象中的值匹配出来。
用点来访问元组
模式匹配可以让我们一次性把元组中的值全部或者部分获取出来,如果只想要访问某个特定元素,那模式匹配就略显繁琐,对此,Rust 提供了 .
的访问方式:
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;
}
和其它语言的数组、字符串一样,元组的索引从 0 开始。
元组的使用示例
元组在函数返回值场景很常用,例如下面的代码,可以使用元组返回多个值:
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)
}
calculate_length
函数接收 s1
字符串的所有权,然后计算字符串的长度,接着把字符串所有权和字符串长度再返回给 s2
和 len
变量。
在其他语言中,可以用结构体来声明一个三维空间中的点,例如 Point(10, 20, 30)
,虽然使用 Rust 元组也可以做到:(10, 20, 30)
,但是这样写有个非常重大的缺陷:
不具备任何清晰的含义,在下一章节中,会提到一种与元组类似的结构体,元组结构体
,可以解决这个问题。
复合类型结构体
Rust 中的结构体(Struct)与元组(Tuple)都可以将若干个类型不一定相同的数据捆绑在一起形成整体,但结构体的每个成员和其本身都有一个名字,这样访问它成员的时候就不用记住下标了。元组常用于非定义的多值传递,而结构体用于规范常用的数据结构。结构体的每个成员叫做"字段"。
结构体定义
Rust 中的结构体(Struct)与元组(Tuple)都可以将若干个类型不一定相同的数据捆绑在一起形成整体,但结构体的每个成员和其本身都有一个名字,这样访问它成员的时候就不用记住下标了。元组常用于非定义的多值传递,而结构体用于规范常用的数据结构。结构体的每个成员叫做"字段"。
这是一个结构体定义:
struct Site {
domain: String,
name: String,
nation: String,
found: u32,
}
注意:如果你常用 C/C++
,请记住在 Rust 里 struct
语句仅用来定义,不能声明实例,结尾不需要 ; 符号,而且每个字段定义之后用 , 分隔。
结构体语法
创建结构体实例
为了使用上述结构体,我们需要创建 User
结构体的实例:
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
有几点值得注意:
- 初始化实例时,每个字段都需要进行初始化
- 初始化时的字段顺序不需要和结构体定义时的顺序一致
访问结构体字段
通过 .
操作符即可访问结构体实例内部的字段值,也可以修改它们:
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
需要注意的是,必须要将结构体实例声明为可变的,才能修改其中的字段,Rust 不支持将某个结构体某个字段标记为可变。
简化结构体创建
下面的函数类似一个构建函数,返回了 User
结构体的实例:
fn build_user(email: String, username: String) -> User {
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
}
}
它接收两个字符串参数: email
和 username
,然后使用它们来创建一个 User
结构体,并且返回。可以注意到这两行: email: email
和 username: username
,非常的扎眼,因为实在有些啰嗦,如果你从 TypeScript 过来,肯定会鄙视 Rust 一番,不过好在,它也不是无可救药:
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
如上所示,当函数参数和结构体字段同名时,可以直接使用缩略的方式进行初始化,跟 TypeScript 中一模一样。
结构体更新语法
在实际场景中,有一种情况很常见:根据已有的结构体实例,创建新的结构体实例,例如根据已有的 user1
实例来构建 user2
:
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
老话重提,如果你从 TypeScript 过来,肯定觉得啰嗦爆了:竟然手动把 user1
的三个字段逐个赋值给 user2
,好在 Rust 为我们提供了 结构体更新语法
:
let user2 = User {
email: String::from("another@example.com"),
..user1
};
因为 user2
仅仅在 email
上与 user1
不同,因此我们只需要对 email
进行赋值,剩下的通过结构体更新语法 ..user1
即可完成。
..
语法表明凡是我们没有显式声明的字段,全部从 user1
中自动获取。需要注意的是 ..user1
必须在结构体的尾部使用。
结构体更新语法跟赋值语句
=
非常相像,因此在上面代码中,user1
的部分字段所有权被转移到user2
中:username
字段发生了所有权转移,作为结果,user1
无法再被使用。聪明的读者肯定要发问了:明明有三个字段进行了自动赋值,为何只有
username
发生了所有权转移?仔细回想一下所有权那一节的内容,我们提到了
Copy
特征:实现了Copy
特征的类型无需所有权转移,可以直接在赋值时进行 数据拷贝,其中bool
和u64
类型就实现了Copy
特征,因此active
和sign_in_count
字段在赋值给user2
时,仅仅发生了拷贝,而不是所有权转移。值得注意的是:
username
所有权被转移给了user2
,导致了user1
无法再被使用,但是并不代表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
结构体在内存中的排列如下图所示:
从图中可以清晰地看出 File
结构体两个字段 name
和 data
分别拥有底层两个 [u8]
数组的所有权(String
类型的底层也是 [u8]
数组),通过 ptr
指针指向底层数组的内存地址,这里你可以把 ptr
指针理解为 Rust 中的引用类型。
该图片也侧面印证了:把结构体中具有所有权的字段转移出去后,将无法再访问该字段,但是可以正常访问其它的字段。
元组结构体(Tuple Struct)
有一种更简单的定义和使用结构体的方式:元组结构体。
元组结构体是一种形式是元组的结构体。
与元组的区别是它有名字和固定的类型格式。它存在的意义是为了处理那些需要定义类型(经常使用)又不想太复杂的简单数据:
struct Color(u8, u8, u8);
struct Point(f64, f64);
let black = Color(0, 0, 0);
let origin = Point(0.0, 0.0);
"颜色"和"点坐标"是常用的两种数据类型,但如果实例化时写个大括号再写上两个名字就为了可读性牺牲了便捷性,Rust 不会遗留这个问题。元组结构体对象的使用方式和元组一样,通过 . 和下标来进行访问:
实例
fn main() {
struct Color(u8, u8, u8);
struct Point(f64, f64);
let black = Color(0, 0, 0);
let origin = Point(0.0, 0.0);
println!("black = ({}, {}, {})", black.0, black.1, black.2);
println!("origin = ({}, {})", origin.0, origin.1);
}
运行结果:
black = (0, 0, 0)
origin = (0, 0)
单元结构体(Unit-like Struct)
结构体可以只作为一种象征而无需任何成员:
struct UnitStruct;
我们称这种没有身体的结构体为单元结构体(Unit Struct)。
结构体数据的所有权
在之前的 User
结构体的定义中,有一处细节:我们使用了自身拥有所有权的 String
类型而不是基于引用的 &str
字符串切片类型。这是一个有意而为之的选择:因为我们想要这个结构体拥有它所有的数据,而不是从其它地方借用数据。
打印结构体
使用#[derive(Debug)]
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:?}", rect1);
println!("rect1 is {:#?}", rect1);
}
使用{:?}
与{:#?}
打印结构体的区别
rect1 is Rectangle { width: 30, height: 50 }
rect1 is Rectangle {
width: 30,
height: 50,
}
使用dbg!
打印调试信息
下面的例子中清晰的展示了 dbg!
如何在打印出信息的同时,还把表达式的值赋给了 width
:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
最终的 debug 输出如下:
$ cargo run
[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle {
width: 60,
height: 50,
}
可以看到,我们想要的 debug 信息几乎都有了:代码所在的文件名、行号、表达式以及表达式的值,简直完美!
复合类型枚举
枚举类在 Rust 中并不像其他编程语言中的概念那样简单,但依然可以十分简单的使用:
#[derive(Debug)]
enum Book {
Papery, Electronic
}
fn main() {
let book = Book::Papery;
println!("{:?}", book);
}
运行结果:
Papery
书分为纸质书(Papery book)和电子书(Electronic book)。
如果你现在正在开发一个图书管理系统,你需要描述两种书的不同属性(纸质书有索书号,电子书只有 URL),你可以为枚举类成员添加元组属性描述:
enum Book {
Papery(u32),
Electronic(String),
}
let book = Book::Papery(1001);
let ebook = Book::Electronic(String::from("url://..."));
如果你想为属性命名,可以用结构体语法:
enum Book {
Papery { index: u32 },
Electronic { url: String },
}
let book = Book::Papery{index: 1001};
虽然可以如此命名,但请注意,并不能像访问结构体字段一样访问枚举类绑定的属性。访问的方法在 match 语法中。