结构体
初始化
Rust 的结构体类似于 C,使用关键字 struct
声明。
struct User {
active: bool,
sign_in_count: u32,
username: String,
email: String
}
结构体中的每个元素称为“域”(field),域是可修改的(mutable),使用 .
来访问域的值。
创建实例
为了使用结构体,需要根据结构体创建一个实例(instance),并给该结构体的域成员赋值,赋值的顺序可以不同于结构体定义的顺序。
使得域可修改,必须给实例添加 mut
关键字,Rust 不允许给某一个或几个域添加 mut
关键字。
struct User {
active: bool,
sign_in_count: u32,
username: String,
email: String
}
fn main() {
let mut user1 = User {
active: false,
sign_in_count: 1,
username: String::from("someusername"),
email: String::from("someuseremail"),
};
user1.email = "anotheremail";
}
可以使用 结构体更新语法 ..
来从其他实例来创建新实例:
struct User {
active: bool,
sign_in_count: u32,
username: String,
email: String
}
fn main() {
let user1 = User {
active: false,
sign_in_count: 1,
username: String::from("someusername"),
email: String::from("someuseremail"),
};
/*
// regular
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("[email protected]"),
sign_in_count: user1.sign_in_count,
};
*/
let user_2 = User {
active: true,
..user1
}
}
上面的代码表示,除了域 active
之外,user_2
的其他域值和 user1
相等。
注:..user1
后没有 ,
,而且必须放在最后一行。
元组结构体
元组结构体(tuple struct) 类似于元组。可以理解为给元组分配了有意义的名称,但是并没有确切的域成员,只有域成员的类型。
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let red = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
上面的两个实例虽然有着相同的域成员类型以及域成员值,但依然是不同的实例。
每个结构体实例的类型就是其定义的类型,即便它们有完全相同的域成员且域成员的类型一致。
类单元结构体
类单元结构体(unit-like struct)指的是不含任何数据的结构体。类似于不含成员的元组--单元(unit) ()
。
struct ULS;
fn main() {
let subject = ULS;
}
函数 VS 方法
关联和区别
在一些编程语言中,函数(function)和方法(method)通常有着相同的含义。在 Rust 中,两者的关联和区别如下:
- 关联
- 都使用
fn
关键字声明 - 都有参数和返回值
- 都可以被调用
- 都使用
- 区别
- 方法的第一个参数永远是
self
,表示被调用的方法作用的实例(instance) - 方法通常被定义在一个结构体、枚举或者 trait 对象的上下文(context)下,而函数通常没有具体的上下文
- 方法的第一个参数永远是
Rust 使用方法的原因是提高代码的组织性。impl
紧紧关联着作用的结构体。
定义方法
方法使用 fn
关键字声明,通常写在 impl(implementation)
块(block)中。
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("The are of rectangle {}", rect1.area());
}
方法的第一个参数是 self
,其实是 self: Self
的简洁表示。如果不希望方法带走 ownership,应该使用 &self
,如果希望更改数据,使用 &mut self
。
类似函数,方法同样使用
.
运算符调用。与 C、C++ 等语言不同,Rust 不支持使用->
运算符来调用方法,而是通过被称为 自动引用和解引用 的方式来调用方法。大致原理为:当调用object.method()
时,Rust 会自动添加&
,&mut
,*
,因此object
匹配了方法的签名以下两行代码作用相同:
p1.distance(&p2); (&p1).distance(&p2);
getters
如果对结构体实现了同名的域成员和方法,那么 object.field
表示访问域成员,object.method()
表示调用方法。
通常,调用同名的方法表示希望获取其同名的域成员的值,这类方法被称为 getters。一些编程语言会自动实现 getters,但是 Rust 并非如此。
结合函数
定义在 impl
块下的函数都被称为 结合函数(Associated Function),因为它们作用于 impl
后的结构体。
也可以定义第一个参数不为 self
的结合函数,这类函数通过 ::
作用,例如:String::from()
。
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
let sq = Rectangle::square(3);
所以 ::
语法同时用于结合函数和模块(module)的命名空间。
多个参数的方法
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
Rust 允许使用多个 impl
块声明方法,但是在本例中,两个方法放在一个 impl
中可读性更好。
枚举
枚举类型(enumerations / enums)定义穷举所有可能的值的数据类型。
定义枚举
例如下面的代码:
use std::cmp::Ordering;
use std:io;
fn main() {
println!("This is a guessing game");
const SECRET_NUMBER: u32 = 12;
let mut guess: String = String::new();
println!("Enter your guess: ");
io::stdin.read_line(&mut guess).expect("Failed to read line");
let mut guess = match guess.parse().wrap() {
Ok(num) => num,
Err(_) => {
println!("You should enter a number!");
}
}
match guess.cmp::Ordering {
Less => println!("Too small!"),
Greater => println!("Too big!"),
Equal => println!("You are the winner!");
}
}
match
关键字用于开始匹配枚举。
这段代码中用到了两个枚举类型,分别是:
- 对
guess
的类型判断会返回枚举类型Result
,它有Ok
和Err
两个枚举值,分别表示成功和错误两种情况, - 对
guess
和SECRET_NUMBER
两个值之间的比较会返回枚举类型Ordering
,它有Less
、Greater
和Equal
三种情况,分别表示小于,大于和等于。
模式匹配
在 Rust 中,使用 match
来对某个值和一系列模式进行匹配。模式可以是字面量、变量以及其他类型。
每个匹配(arm)都由模式和代码组成,每个 arm 之间用 ,
分隔。模式和代码之间用 =>
相连。
如果代码为多行,需要放入括号 {}
。代码由表达式组成,如果匹配成功,该表达式的值作为整个 match
的返回值。
例如:
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
/*
Coin::Penny => {
println!("That's an penny!");
1
},
*/
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
match
和 if
的不同在于:if
的条件结果必须是 bool
,而 match
可以是任意类型。
if let
if let
语法提供了一种更简洁的方式来处理某种模式匹配成功并忽略其他选项的情况。
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {}", max),
_ => (),
}
// same as
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {}", max);
}
模块系统
Rust 的模块系统(Module System)包括:
- 包(package):构建、测试、共享 crates。
- crates:可生成库(library)或者可执行程序的模块树。
- 模块(module):控制路径的组织方式、作用域以及私有性。
- 路径(path):命名一个实体的方式,例如:结构体、函数、模块。
包
包 是一系列 crates 的集合。包中有名为 Cargo.toml
的文件定义了如何构建这些 crates。
使用 cargo new
命令后,Rust 会在当前目录创建一个包。
包中至少要包含一个 crate。包可以包含多个二进制 crates,但是最多只能包含一个库 crate。
Cargo 是最常用的包,其默认把 src/main.rs
和 src/lib.rs
作为二进制 crate 和库 crate,并把两者作为 crate root。当使用 rustc
时,Rust 把这两个文件(如果存在)编译。
Crate
在 Rust 中,crate 指的是编译器所编译的源文件,是编译器一次编译时的最小单位。
crate 包含多个模块。
crate 分为二进制 crate(binary crate)和库(library crate)两种:
-
二进制 crate 是由 Rustaceans 所编写的代码,每个二进制 crate 必须包含一个
main
函数。 -
库 crate 不含
main
函数,不能被编译为可执行程序,而是作为一种共享方式在项目中。
两种 crate 分别在 src
路径下以 main.rs
和 lib.rs
两种文件名称存在。
crate root 指的是 Rust 编译器编译的源文件,以及 crate 的根模块。
模块
总览
假设有以下文件结构:
backyard
|--Cargo.lock
|--Cargo.toml
|--src
|--garden
| |--vegetable.rs
|--garden.rs
|--main.rs
// src/main.rs
use crate::garden::vegetables::Asparagus;
pub mod garden;
fn main() {
let plant = Asparagus {};
println!("I'm growing {:?}!", plant);
}
// garden.rs
pub mod vegetables;
// vegetables.rs
#[derive(Debug)]
pub struct Asparagus {}
- backyard 是 crate 目录
src/main.rs
是 crate rootpub mod garden
表示 garden 是一个模块,可见性为pub
。这行代码表示把garden.rs
里的内容引入pub mod vegetables
作用同上
所以,模块的工作原理:
-
从 crate root 开始:当编译 crate 时,编译器首先找到 crate root 文件(通常是
main.rs
或者lib.rs
)来编译 -
定义模块:在 crate root 文件中,可以用
mod
关键字声明新的模块,例如:mod garden
,编译器会在以下目录寻找该模块的代码:- 行内
src/garden.rs
src/garden/mod.rs
-
定义子模块:在 除 crate root 的文件里还可以定义子模块,例如:
mod vegetables
,编译器会在其父模块的目录下寻找子模块的代码:- 行内
src/garden/vegetables.rs
src/garden/vegetables/mod.rs
-
模块中的路径:一旦声明模块后,可以通过路径引入模块。例如:在
vegetables
模块内声明了Asparagus
,引入路径为:crate::garden::vegetables::Asparagus
-
私有 vs 共有:默认情况下,子模块的内容对父模块是私有的,使用
pub
关键字使其公有化 -
use
关键字:使用use
来简化引用。
优势
模块的优势:
- 提高代码的可读性和可重用性
- 隐私性
路径
类似于文件系统,有绝对路径和相对路径两种方式来表示层级关系:
- 绝对路径:指的是从 crate root 开始的完整路径。对于外部 crate 来说,绝对路径以 crate 的名称为开始;对内部 crate 来说,绝对路径以字面量
crate
开始 - 相对路径:从当前模块开始,通常包含
self
、super
等关键字
绝对路径和相对路径都使用 ::
表示层级间的分隔符。
绝对和相对路径各有优劣,可以根据个人偏好进行选择。在 Rust 中,一般使用绝对路径,原因是这样使得依赖相对独立。
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
私有、公有
在 Rust 乃至整个计算机领域都有内部实现对外部不可见的原则。
私有的概念通常和作用域(scope)相关。例如:定义在函数 A 内部定义了子函数 B。对该子函数 B 来说,函数 A 对其是可见的。但对函数 A 的其他部分来说,子函数 B 是不可见的,
通常来讲,Rust 把对象(items)设置为私有(privacy),或者称为不可见的。如果调用了不可见的对象,编译器会弹出错误。
在 Rust 中,默认对父模块私有的对象(items)包括模块、函数、方法、结构体、枚举、常量。
可以使用 pub
关键字使对象变为可见、公有的。
注:把某个外部对象标识为 pub
并不意味着其内部对象也被标识为 pub
(枚举类型除外,如果枚举类型使用了 pub
,那么枚举的所有结果默认也为 pub
),例如:
fn main() {
pub fn outer_function() {
fn inner_function() {
// --snip--
}
}
outer_function(); // OK
inner_function(); // Error, because the function sign has no **pub**
}
pub enum IPAddr {
V4(String), // also **pub**
V6(String), // also **pub**
}
最佳实践
一般来说,一个包同时包含二进制 crate
src/main.rs
以及库 cratesrc/lib.rs
。两者默认都含有包名。常用的范式是:在
src/lib.rs
中定义模块树,这样,在二进制 crate 中调用任何公有的对象(items)都可以以该包名为开始作为路径。
super
使用 super
关键字来引用父级路径,这类似于文件系统中的 ..
。
fn deliver_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::deliver_order();
}
fn cook_order() {}
}
use、as
use
使用 use
关键字来引入路径。
Rust 的惯例是:在调用某个函数时,其路径应该引入到父级。虽然引入到当前级效果相同,但是前者使得函数定义更加清晰。例如:
use crate::galaxy::solar_system::earth;
earth();
use crate::galaxy::solar_system;
solar_system.earth(); // same thing, but this one is better.
再导出
再导出(re-exporting) 使得当前作用域引入的对象也可以用于其他作用域。因为默认情况下,使用 use
关键把某个名称引入当前作用域后,该名称对其他作用域是私有的。
使用 pub use
实现再导出:
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
- 如果没有再导出,外部代码想调用
add_to_waitlist()
函数必须使用路径:restaurant::front_of_house::hosting::add_to_waitlist()
- 再导出后,使用
restaurant::hosting::add_to_waitlist()
即可
嵌套路径
为了避免使用多个 use
的多行引入导致代码可读性变差,可以把同一父对象下的子对象用花括号在同一行中。
use std::{io, fmt};
// same as
use std::io;
use std::fmt;
如果同时引入了父对象和其子对象,使用 self
关键字表示该父对象。
use std::io::{self, Result};
// same as
use std::io;
use std::io::Result;
Glob 运算符
如果要引入全部对象,使用全局 glob 运算符 *
。
use std::io::*;
as
假如引入的对象名称过长,可以使用 as
关键字来通过别名来引入。
use this_is_a_very_long_function_name as lfn;
lfn(); // much simpler
集合
Rust 的标准库中包含了一系列常用的数据结构被称为集合(collection)。最常用的是:
- 向量 Vector
- 字符串 String
- 哈希表 HashMap
这些结构的特点是:存储在堆中,可变长,使用泛型实现。这意味着在编译时,编译器并不知道这些结构的大小。
初始化集合的通用方法是 ::new()
向量
向量中的元素在内存中紧挨着彼此存储。
向量只能存储同种类型的数据,但是可以借助枚举来存储不同类型的数据。
初始化
向量 Vec<T>
的初始化,可以用 ::new()
来初始化一个空向量,也可以使用 macro vec![]
显式把向量的成员列出来初始化向量:
fn main() {
let v = vec![1, 2, 3, 4, 5];
let ano_v: Vec<i32> = Vec::new();
}
注:在第二种声明中指明了存储元素的类型,否则 Rust 并不知道 Vector 要存储什么类型的数据。
读写
写
使用 push
方法给向量添加元素:
let mut v = Vec::new();
v.push(1);
v.push(2);
读
使用 .get()
或者括号索引 []
的方式来访问向量元素:
let third: &i32 = &v[2]; // 3
let two: Option<&i32> = v.get(2); // Some(2)
如上面的代码所示,使用 .get()
方法得到的是 Option<T>
数据类型,而不是向量元素 <T>
的类型。
由于 .get()
方法得到的是 Option<T>
类型,因此可以使用 match
来对取得的值做判断。
let two = v.get(2);
match two {
Some(two) => println!("The element is {}", two),
None => println!("No such element"),
}
越界
两种访问方式对于向量越界有着不同的处理方式:
let third = &v[100]; // index out of bound
let two = v.get(100); // None
使用 []
索引访问可以通过编译,但在运行时会出现 index out of bound
索引越界的错误;使用 .get()
方法会得到 None
。
下面的例子说明了向量的工作方式:
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("{}", first); // Error
上面的代码不能通过编译,原因是:由于向量的元素在内存中是紧挨着彼此存储的。因此,给向量中添加新元素时,如果当前的内存空间位置不能容下新加入的元素,就需要分配一块新的内存空间并拷贝旧的元素到该空间。而引用访问向量元素也许会导致访问到已经被解除分配的空间。
遍历
使用 for in
来遍历向量元素:
for i in &v {
println!("{}", i);
}
使用 mut
引用来遍历并修改向量元素:
for i in &mut v {
*i += 50;
}
字符串
两类字符串的比较
Rust 语言的核心中只有一种字符串:字符串切片 str
,通常以 &str
形式出现。
而 String
类型由 Rust 标准库实现的。
两者都是 UTF-8 编码的。
初始化
字符串 String
的初始化,可以用 ::new()
来初始化一个空字符串,也可以用 ::from()
显式初始化字符串,或者先声明字符串字面量,然后转化为 String
类型的字符串:
fn main() {
let mut s = String::from("Hello");
let mut ano_s = String::new();
let s_in_stack: &str = "Hello World";
let s_in_heap: String = s_in_stack.to_string();
}
读写
写
使用 push_str()
把字符串拼接至另一字符串尾:
s.push_str(", World");
println!("{}", s) // Hello, World
使用 push()
拼接一个字符到字符串尾:
let mut s = String::from("lo");
s.push('l');
println!("{}", s) // "lol"
使用 +
来拼接已有字符串:
let s1 = String::from("Hello, ");
let s2 = String::from("World");
let s = s1 + &s2;
println!("{}", s1); // Error
println!("{}", s2); // "World"
println!("{}", s); // "Hello, World"
注:拼接之后,s1
的 ownership 被转移给 s
,所以 s1
不能再被使用。这是因为 add
函数的签名:
fn add(self, s: &str) -> String {
决定了 self
位置的变量的 ownership 被夺取。第二个变量需要使用 &
引用形式,而不是直接把两个字符串的值相加。
这里编译器使用 coerce 把 String
类型转化为 &str
,当调用 add
时,Rust 会使用 deref coercion
把 &s2
转化为 &s2[..]
。
如果不希望 s1
的 ownership 发生变化,可以使用 format!
macro 来拼接字符串:
let s1 = String::from("Hello, ");
let s2 = String::from("World");
let s = format!("{s1}{s2}");
println!("{}", s1); // "Hello, "
println!("{}", s2); // "World"
println!("{}", s); // "Hello, World"
读
Rust 不支持索引访问字符串中的字符。
let s1 = String::from("Code");
println!("{}", s1[0]); // Error
上面的代码将不能通过编译,原因和 String
类型的内部实现有关:
String
类型是对 Vec<u8>
的包装,所以:
let hello = String::from("Hola");
let ano_hello = String::from("Здравствуйте");
hello
的 len
为 4
,因为在 UTF-8 编码中每个字符占用 1
个字节,而 ano_hello
的 len
为 24
,而非 12
,因为在 UTF-8 编码中,每个 Unicode scalar 值占用 2
个字节。因此,如果使用索引访问,将返回无意义的值。
可以使用 [..]
创建字符串切片:
let ano_hello = "Здравствуйте";
// let ano_hello = String::from("Здравствуйте"); // also Ok
let s = &ano_hello[0..4]; // Зд
s
是 ano_hello
的前 4
个字节,而非字符。
类型转换
使用 to_string
把其他类型转化为字符串:
let i: i32 = 2;
let i_s: String = i.to_string(); // "2";
遍历
使用 .chars()
获得字符串的序列,并用 for in
来遍历以输出字符串的字符:
let s = String::from("Hello");
for c in s.chars() {
println!("{}", c);
}
// H
// e
// l
// l
// o
类似地,使用 .bytes()
或者字符对应的字节序列,并用 for in
来遍历以输出字符串的字符:
let s = String::from("Hello");
for b in s.bytes() {
println!("{}", b);
}
// 72
// 101
// 108
// 108
// 111
哈希表
使用哈希表 HashMap<K, V>
前需要用 use
关键字引入:
use std::collections::HashMap;
哈希表的键类型为 String i32
,键和值必须为相同类型。
初始化
可以用 ::new()
来初始化一个空哈希表:
fn main() {
let hm = HashMap::new();
}
读写
写
使用 .insert()
添加键值对到哈希表(注意:mut
关键字):
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 30);
scores.insert(String::from("Black"), 20);
读
使用 .get()
根据键获取值:
println!("the blue: {:?}", scores.get("Blue").unwrap()); // 10
println!("the blue: {:?}", scores.get("Yellow")); // Some(30)
println!("the blue: {:?}", scores.get("Red").copied().unwrap_or(0)); // None
和向量类似,获取的值是 Option<&V>
类型,可以使用 unwrap()
获取 <T>
类型。
遍历
使用元组遍历哈希表:
for (key, value) in &scores {
println!("{} {}", key, value);
}
// One Possible Outcome:
// Blue 10
// Yellow 30
// Black 20
注:遍历的结果是无序的。
更新
哈希表的更新有几种不同的方式:
如果给同一个键添加多个值,结果是只保留最后一个值:
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(String::from("One"), 1);
map.insert(String::from("One"), 2);
println!("{:?}", map); // {"One": 2};
}
如果不存在键,使用 entry()
和 or_insert()
添加键值对:
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(String::from("One"), 1);
map.entry(String::from("One")).or_insert(2);
map.entry(String::from("Two")).or_insert(2);
println!("{:?}", map); // {"One": 1, "Two": 2};
}
entry()
API 返回 Entry
枚举类型,该枚举类型返回指定的键是否存在,or_insert()
构建在 Entry
之上,如果键存在就不做修改,如果不存在就添加该键值对。