Rust学习
1.Rust编译运行
Cargo包管理
Cargo
是Rust中的包管理工具,可以帮助创建、构建和管理 Rust 项目,同时还能管理项目的依赖关系。
Cargo
一共做了四件事:
- 使用两个元数据(
metadata
)文件来记录各种项目信息 - 获取并构建项目的依赖关系
- 使用正确的参数调用
rustc
或其他构建工具来构建项目 - 为Rust生态系统开发建议了统一标准的工作流
Cargo文件:
Cargo.lock
:只记录依赖包的详细信息,不需要开发者维护,而是由Cargo自动维护Cargo.toml
:描述项目所需要的各种信息,包括第三方包的依赖
Cargo.toml(example)
[package]
name = "xdp"
version = "0.1.0"
authors = [" "]
edition = "2021"//项目的元数据信息
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0"
ctrlc = { version = "3.0", features = ["termination"] }
libc = "0.2"
libbpf-rs = "0.19"
structopt = "0.3"//运行时依赖项,可以再这里添加想依赖的包,在rs文件中使用extern create命令声明引入该包即可使用。
[build-dependencies]
libbpf-cargo = "0.13"//构建时依赖项,用于与 Linux 内核的 BPF(Berkeley Packet Filter)功能集成
cargo编译默认为Debug模式,在该模式下编译器不会对代码进行任何优化。也可以使用**–release**参数来使用发布模式。release模式,编译器会对代码进行优化,使得编译时间变慢,但是代码运行速度会变快。
官方编译器rustc,负责将rust源码编译为可执行的文件或其他文件。
使用cargo创建新包
使用new加要创建的包的名称:
cargo new hello_opensource
Created binary (application) `hello_opensource` package
它创建一个带有包名称的目录,并且在该目录内有一个存放源代码文件的 src
目录。以及我们这个使用项目的配置文件Cargo.toml。
使用cargo build构建包,再查看其目录:
编译过程产生了许多中间文件,包括上面提到的cargo.lock.
使用cargo run运行
Rust可以在Cargo.toml中的dependencies下添加想依赖的包来使用第三方包,比如:
[dependencies]
rand = "0.3.14"
再次build的时候就可以看到 关于这个依赖库的下载,可以使用cargo update
对所有依赖关系进行更新。
使用rustc直接运行
2.example(使用rust语言构建eBPF程序)
- 内核态程序
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
SEC("xdp")// 附着在网络设备的入口路径
int xdp_pass(struct xdp_md *ctx)//参数包含了数据包的元数据
{
void *data = (void *)(long)ctx->data;//获取数据包的起始地址
void *data_end = (void *)(long)ctx->data_end;//获取数据包的结束地址
int pkt_sz = data_end - data;//计算数据包的大小
bpf_printk("packet size: %d", pkt_sz);//打印日志信息的函数,它类似于 C 语言中的 printf 函数,用于打印数据包的大小。
return XDP_PASS;//返回 XDP_PASS,表示数据包应该被传递给内核网络协议栈继续处理。
}
char __license[] SEC("license") = "GPL";
- 用户态程序(RUST)
use std::sync::atomic::{AtomicBool, Ordering};//提供了原子类型和原子操作,用于在多线程环境下进行线程间的同步和通信。
use std::sync::Arc;//提供了原子引用计数类型 Arc,用于在多线程环境下共享数据
use std::{thread, time};//提供了创建和操作线程的功能以及与时间相关的功能
use anyhow::{bail, Result};//简化错误处理,Result 类型用于表示可能包含错误的结果。
use structopt::StructOpt;//解析命令行参数
mod xdppass {
include!(concat!(env!("OUT_DIR"), "/xdppass.skel.rs"));
}//定义了一个名为 xdppass 的模块,使用 include! 宏导入了一个外部生成的 Rust 骨架文件,由build.rs生成。
use xdppass::*;
#[derive(Debug, StructOpt)]
struct Command {
/// Interface index to attach XDP program
#[structopt(default_value = "0")]
ifindex: i32,
}//使用了 structopt 库来定义命令行参数结构体 Command,其中包含一个 ifindex 字段,用于指定要附加 XDP 程序的接口索引。这个字段使用 #[structopt(default_value = "0")] 注解来指定默认值为 0。
fn bump_memlock_rlimit() -> Result<()> {
let rlimit = libc::rlimit {//创建了一个 libc::rlimit 结构体实例 rlimit,设置进程的资源限制。
rlim_cur: 128 << 20,//rlim_cur 表示当前的资源限制值
rlim_max: 128 << 20,//rlim_max 表示资源限制的最大值。
};
//返回类型为 Result<()
if unsafe { libc::setrlimit(libc::RLIMIT_MEMLOCK, &rlimit) } != 0 {
bail!("Failed to increase rlimit");
}//如果 libc::setrlimit 返回的值不等于 0,表示设置资源限制失败,此时通过 anyhow crate 提供的 bail! 宏生成一个错误,并返回结果 Err。
Ok(()).//如果设置成功,则直接返回 Ok(()) 表示成功。
}
fn main() -> Result<()> {
let opts = Command::from_args();//解析命令行参数
bump_memlock_rlimit()?; // 调整内存锁定限制
// 创建并打开 XDP 程序的骨架(skeleton)
let skel_builder = XdppassSkelBuilder::default();
let open_skel = skel_builder.open()?;
let mut skel = open_skel.load()?;
// 将 XDP 程序附加到指定的网络接口
let link = skel.progs_mut().xdp_pass().attach_xdp(opts.ifindex)?;
// 更新骨架的链接信息
skel.links = XdppassLinks {
xdp_pass: Some(link),
};
// 设置 Ctrl+C 处理程序,用于退出程序
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
ctrlc::set_handler(move || {
r.store(false, Ordering::SeqCst);
})?;
// 主循环,持续运行直到 Ctrl+C 被按下
while running.load(Ordering::SeqCst) {
eprint!(".");
thread::sleep(time::Duration::from_secs(1));//一秒钟打印一个点
}
Ok(())
}
3.变量
①变量的可变性
Rust 的变量在默认情况下是不可变的。但是如果需要让这个变量可变,只要在变量名前加一个 mut
即可, 而且这种显式的声明方式还会给后来人传达这样的信息:这个变量在后面代码部分会发生改变。
使用场景:使用大型数据结构或者热点代码路径(被大量频繁调用)的情形下,在同一内存位置更新实例可能比复制并返回新分配的实例要更快。使用较小的数据结构时,通常创建新的实例并以更具函数式的风格来编写程序,可能会更容易理解,所以值得以较低的性能开销来确保代码清晰。
②使用下划线开头忽略未使用的变量
有时创建一个不会被使用的变量是有用的。
③变量解构
let
表达式不仅仅用于变量的绑定,还能进行复杂变量的解构:从一个相对复杂的变量中,匹配出该变量的一部分内容:
let (a, mut b): (bool,bool) = (true, false);
解构式赋值:
[c, .., d, _] = [1, 2, 3, 4, 5];
④变量和常量的差异
- 常量不允许使用
mut
。常量不仅仅默认不可变,而且自始至终不可变,因为常量在编译完成后,已经确定它的值。 - 常量使用
const
关键字而不是let
关键字来声明,并且值的类型必须标注。
⑤常量遮蔽(shadowing)
Rust 允许声明相同的变量名,在后面声明的变量会遮蔽掉前面声明的,花括号会创建新的作用域。
这和 mut
变量的使用是不同的,第二个 let
生成了完全不同的新变量,两个变量只是恰好拥有同样的名称,涉及一次内存对象的再分配 ,而 mut
声明的变量,可以修改同一个内存地址上的值,并不会发生内存对象的再分配,性能要更好。
4.基本类型
4.1数值类型
Rust 编译器可以根据变量的值和上下文中的使用方式来自动推导出变量的类型,但是在某些情况下,它无法推导出变量类型,需要手动去给予一个类型标注。
①整数类型
每个有符号类型规定的数字范围是 -(2n - 1) ~ 2n - 1 - 1,其中 n
是该定义形式的位长度。因此 i8
可存储数字范围是 -(27) ~ 27 - 1,即 -128 ~ 127。无符号类型可以存储的数字范围是 0 ~ 2n - 1,所以 u8
能够存储的数字为 0 ~ 28 - 1,即 0 ~ 255。
此外,isize
和 usize
类型取决于程序运行的计算机 CPU 类型: 若 CPU 是 32 位的,则这两个类型是 32 位的,同理,若 CPU 是 64 位,那么它们则是 64 位。
rust整形默认使用i32.
整形溢出
在当使用 --release
参数进行 release 模式构建时,Rust 不检测溢出。相反,当检测到整型溢出时,Rust 会按照补码循环溢出的规则处理。简而言之,大于该类型最大值的数值会被补码转换成该类型能够支持的对应数字的最小值。比如在 u8
的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是所期望的值。
②浮点类型
rust默认浮点类型是 f64
,在现代的 CPU 中它的速度与 f32
几乎相同,但精度更高。
注意精度问题,浮点数慎用std::cmp::Eq
.
NAN
Rust 的浮点数类型使用 NaN
(not a number)来处理数学上未定义的结果.
fn main() {
let x = (-42.0_f32).sqrt();
if x.is_nan() {
println!("未定义的数学行为")
}
}
③数字运算
eg:
// 对于较长的数字,可以用_进行分割,提升可读性
let one_million: i64 = 1_000_000;
println!("{}", one_million.pow(2));
// 定义一个f32数组,其中42.0会自动被推导为f32类型
let forty_twos = [
42.0,
42f32,
42.0_f32,
];
④位运算
与C语言一样。
⑤ 序列
Rust 生成连续的数值,例如 1..5
,生成从 1 到 4 的连续数字,不包含 5 ;1..=5
,生成从 1 到 5 的连续数字,包含 5,它的用途很简单,常常用于循环中,序列只允许用于数字或字符类型.
for i in 1..=5 {
println!("{}",i);
⑥有理数和复数
有理数和复数并未包含在rust标准库里,当我们要使用时,就要在cargo.toml中的[dependencies]下添加一行
num = “0.4.0”,
4.2字符、布尔、单元类型
①字符类型
Rust 的字符不仅仅是 ASCII
,所有的 Unicode
值都可以作为 Rust 字符,包括单个的中文、日文、韩文、emoji 表情符号等等,都是合法的字符类型。
字符``,字符串“”。调用该库std::mem::size_of_val(&x),可以获取其占用内存大小。
②布尔
和c相似,主要使用于流程控制。
③单元类型
fn main()
函数的使用,这个函数返回的就是单元类型()。
可以用 ()
作为 map
的值,表示我们不关注具体的值,只关注 key
。
4.3语句与表达式(易混淆)
Rust 的函数体是由一系列语句组成,最后由一个表达式来返回值,语句会执行一些操作但是不会返回一个值,而表达式会在求值后返回一个值。
①语句
let a = 8;
let b: Vec<f64> = Vec::new();
let (a, c) = ("hi", false);
② 表达式
表达式会进行求值,然后返回一个值。表达式可以成为语句的一部分,能返回值就是表达式! 且表达式不能包含分号。
4.4函数
①函数要点
- 函数名和变量名使用蛇形命名法(snake case),例如
fn add_two() -> {}
- 函数的位置可以随便放,Rust 不关心我们在哪里定义了函数,只要有定义即可
- 每个函数参数都需要标注类型
②函数参数
Rust 是强类型语言,因此需要为每一个函数参数都标识出它的具体类型。
③函数返回
函数的返回值就是函数体最后一条表达式的返回值,当然我们也可以使用 return
提前返回。
5.所有权与借用
rust引入了所有权的概念,用于管理内存的申请和释放。
5.1所有权
①栈和堆
栈中的所有数据都必须占用已知且固定大小的内存空间;与栈不同,对于大小未知或者可能变化的数据,我们需要将它存储在堆上。
当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针, 该过程被称为在堆上分配内存。
接着,该指针会被推入栈中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的指针,来获取数据在堆上的实际内存位置,进而访问该数据。
性能区别
在栈上分配内存比在堆上分配内存要快,因为入栈时操作系统无需进行函数调用(或更慢的系统调用)来分配新的空间,只需要将新数据放入栈顶即可。
②所有权原则
1.Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
2.一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
3.当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)
变量作用域
let s = "hello";
变量 s
绑定到了一个字符串字面值,该字符串字面值是硬编码到程序代码中的。s
变量从声明的点开始直到当前作用域的结束都是有效的。
string类型
上述的s是被硬编码到程序代码中的,是不可变的,其类型为&str。但是有时候字符串的值需要在程序运行当动态的输入,这种情况下就不能使用&str类型。为此,Rust 提供动态字符串类型: String
, 该类型被分配到堆上,因此可以动态伸缩,也就能存储在编译时大小未知的文本。
let s = String::from("hello");
::
是一种调用操作符,这里表示调用 String
模块中的 from
方法,由于 String
类型存储在堆上,因此它是动态的,你可以这样修改:
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() 在字符串后追加字面值
println!("{}", s); // 将打印 `hello, world!`
③变量绑定背后的数据交互
转移所有权
基本数据类型通过值拷贝的方式完成(发生在栈中),因此并不需要所有权转移。
let s1 = String::from("hello");
let s2 = s1;
String
类型是一个复杂类型,由存储在栈中的堆指针、字符串长度、字符串容量共同组成,其中堆指针是最重要的,它指向了真实存储字符串内容的堆内存,至于长度和容量,容量是堆内存分配空间的大小,长度是目前已经使用的大小。
为了避免二次释放的错误,当 s1
被赋予 s2
后,Rust 认为 s1
不再有效,因此也无需在 s1
离开作用域后 drop
任何东西,这就是把所有权从 s1
转移给了 s2
,s1
在被赋予 s2
后就马上失效了。
克隆(深拷贝)
Rust 永远也不会自动创建数据的 “深拷贝”。
如果我们确实需要深度复制 String
中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone
的方法。
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
拷贝(浅拷贝)
浅拷贝只发生在栈上,因此性能很高,在日常编程中,浅拷贝无处不在。
Rust 有一个叫做 Copy
的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 Copy
特征,一个旧的变量在被赋值给其他变量后仍然可用,也就是赋值的过程即是拷贝的过程。
任何基本类型的组合可以 Copy
,不需要分配内存或某种形式资源的类型是可以 Copy
的。如下是一些 Copy
的类型:
- 所有整数类型,比如
u32
- 布尔类型,
bool
,它的值是true
和false
- 所有浮点数类型,比如
f64
- 字符类型,
char
- 元组,当且仅当其包含的类型也都是
Copy
的时候。比如,(i32, i32)
是Copy
的,但(i32, String)
就不是 - 不可变引用
&T
,例如转移所有权中的最后一个例子,但是注意: 可变引用&mut T
是不可以 Copy的
5.2借用与借用权
rust通过借用使用某个变量的指针或引用。
①引用与解引用
常规引用是一个指针类型,指向了对象存储的内存地址。与C语言一样。
fn main() {
let x = 5;
let y = &x;//创建一个 i32 值的引用 y
assert_eq!(5, x);
assert_eq!(5, *y);//对 y 的值做出断言,使用 *y 来解出引用所指向的值。
}
②不可变引用
下面的代码,我们用 s1
的引用作为参数传递给 calculate_length
函数,而不是把 s1
的所有权转移给该函数:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);//传入的是引用而不是所有权
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {//参数类型为&String
s.len()
}
引用指向的值默认是不可变的。
③可变引用
fn main() {
let mut s = String::from("hello");//声明 `s` 是可变类型
change(&mut s);//创建一个可变的引用 `&mut s`
}
fn change(some_string: &mut String) {
some_string.push_str(", world");//接受可变引用参数 `some_string: &mut String` 的函数
}
可变引用同时只能存在一个
使用有限制:同一作用域,特定数据只能有一个可变引用。
这样做使 Rust 在编译期就避免数据竞争,数据竞争可由以下行为造成:
- 两个或更多的指针同时访问同一数据
- 至少有一个指针被用来写入数据
- 没有同步数据访问的机制
可变引用与不可变引用不能同时存在
引用的作用域 s
从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号 }
NLL
Non-Lexical Lifetimes(NLL),专门用于找到某个引用在作用域(}
)结束前就不再被使用的代码位置。
悬垂引用(Dangling References)
悬垂引用也叫做悬垂指针,意思为指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用。在 Rust 中编译器可以确保引用永远也不会变成悬垂状态:当你获取数据的引用后,编译器可以确保数据不会在引用结束前被释放,要想释放数据,必须先停止其引用的使用。
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s//不能使用引用,因为S的作用域马上结束,会被丢弃,在这里可以直接返回S,将所有权交出去。
}
6.复合类型
6.1字符串与切片
①字符串
Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4),这样有助于大幅降低字符串所占用的内存空间。
str
类型是硬编码进可执行文件,也无法被修改,但是 String
则是一个可增长、可改变且具有所有权的 UTF-8 编码字符串,当 Rust 用户提到字符串时,往往指的就是 String
类型和 &str
字符串切片类型,这两个类型都是 UTF-8 编码。
String 与 &str 的转换
fn main() {
let s = String::from("hello,world!");
say_hello(&s);//取引用
say_hello(&s[..]);
say_hello(s.as_str());
}
fn say_hello(s: &str) {
println!("{}",s);
}
字符串索引
字符串的底层的数据存储格式实际上是[ u8
],一个字节数组。对于 let hello = String::from("Hola");
这行代码来说,Hola
的长度是 4
个字节,因为 "Hola"
中的每个字母在 UTF-8 编码中仅占用 1 个字节,汉字占3个。
Rust 不允许去索引字符串。
②操作字符串
追加 (Push)
在字符串尾部可以使用 push()
方法追加字符 char
,也可以使用 push_str()
方法追加字符串字面量。这两个方法都是在原有的字符串上追加,并不会返回新的字符串。由于字符串追加操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut
关键字修饰。
let mut s = String::from("Hello ");
s.push_str("rust");
s.push('!');
插入 (Insert)
可以使用 insert()
方法插入单个字符 char
,也可以使用 insert_str()
方法插入字符串字面量,与 push()
方法不同,这俩方法需要传入两个参数,第一个参数是字符(串)插入位置的索引,第二个参数是要插入的字符(串),索引从 0 开始计数,如果越界则会发生错误。字符串变量必须由 mut
关键字修饰。
let mut s = String::from("Hello rust!");
s.insert(5, ',');
替换 (Replace)
如果想要把字符串中的某个字符串替换成其它的字符串,那可以使用 replace()
方法:
1.replace
该方法可适用于 String
和 &str
类型。replace()
方法接收两个参数,第一个参数是要被替换的字符串,第二个参数是新的字符串。该方法会替换所有匹配到的字符串。该方法是返回一个新的字符串,而不是操作原来的字符串。
let string_replace = String::from("I like rust. Learning rust is my favorite!");
let new_string_replace = string_replace.replace("rust", "RUST");
2、replacen
可适用于 String
和 &str
类型。replacen()
方法接收三个参数,前两个参数与 replace()
方法一样,第三个参数则表示替换的个数。该方法是返回一个新的字符串,而不是操作原来的字符串。
let string_replace = "I like rust. Learning rust is my favorite!";
let new_string_replacen = string_replace.replacen("rust", "RUST", 1);
3、replace_range
该方法仅适用于 String
类型。replace_range
接收两个参数,第一个参数是要替换字符串的范围(Range),第二个参数是新的字符串。该方法是直接操作原来的字符串,不会返回新的字符串。该方法需要使用 mut
关键字修饰。
let mut string_replace_range = String::from("I like rust!");
string_replace_range.replace_range(7..8, "R");
删除 (Delete)
与字符串删除相关的方法有 4 个,它们分别是 pop()
,remove()
,truncate()
,clear()
。这四个方法仅适用于 String
类型。
1、 pop
—— 删除并返回字符串的最后一个字符
跟数据结构的栈很相似,弹出最后一个字符。
let mut string_pop = String::from("rust pop 中文!");
let p1 = string_pop.pop();//删除!
let p2 = string_pop.pop();
2、 remove
—— 删除并返回字符串中指定位置的字符 移开
该方法是直接操作原来的字符串。但是存在返回值,其返回值是删除位置的字符串,只接收一个参数,表示该字符起始索引位置。remove()
方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。
let mut string_remove = String::from("测试remove方法");
string_remove.remove(0);// 删除第一个汉字
3、truncate
—— 删除字符串中从指定位置开始到结尾的全部字符
该方法是直接操作原来的字符串。无返回值。该方法 truncate()
方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。
let mut string_truncate = String::from("测试truncate");//删除试truncate
string_truncate.truncate(3);
4、clear
—— 清空字符串
该方法是直接操作原来的字符串。调用后,删除字符串中的所有字符,相当于 truncate()
方法参数为 0 的时候。
let mut string_clear = String::from("string clear");
string_clear.clear();
连接 (Concatenate)
1、使用 +
或者 +=
连接字符串
使用 +
或者 +=
连接字符串,要求右边的参数必须为字符串的切片引用(Slice)类型。其实当调用 +
的操作符时,相当于调用了 std::string
标准库中的 add()方法,这里 add()
方法的第二个参数是一个引用的类型。因此我们在使用 +
时, 必须传递切片引用类型。不能直接传递 String
类型。+
是返回一个新的字符串,所以变量声明可以不需要 mut
关键字修饰。
let string_append = String::from("hello ");
let string_rust = String::from("rust");
let result = string_append + &string_rust;
2、使用 format!
连接字符串
fn main() {
let s1 = "hello";
let s2 = String::from("rust");
let s = format!("{} {}!", s1, s2);
println!("{}", s);
}
③字符串转义
我们可以通过转义的方式 \
输出 ASCII 和 Unicode 字符。
④操作 UTF-8 字符串
想要以 Unicode 字符的方式遍历字符串,最好的办法是使用 chars
方法,例如:
for c in "中国人".chars() {
println!("{}", c);
}//输出中国人
这种方式是返回字符串的底层字节数组表现形式:
for b in "中国人".bytes() {
println!("{}", b);
}//输出字节数组表现形式
6.2元组
元组是由多种类型组合到一起形成的,因此它是复合类型,元组的长度是固定的,元组中元素的顺序也是固定的。
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
用模式匹配解构元组
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;//模式匹配解构元组
println!("The value of y is: {}", y);
}
用 . 来访问元组
如果只想要访问某个特定元素,那模式匹配就略显繁琐,对此,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;
}
6.3结构体
结构体跟元组有些相像:都是由多种类型组合而成。但是与元组不同的是,结构体可以为内部的每个字段起一个富有含义的名称。因此结构体更加灵活更加强大,无需依赖这些字段的顺序来访问和解析它们。
① 结构体语法
定义结构体
一个结构体由几部分组成:
- 通过关键字
struct
定义 - 一个清晰明确的结构体
名称
- 几个有名字的结构体
字段
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
创建结构体实例
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 不支持将某个结构体某个字段标记为可变。
简化结构体创建
fn build_user(email: String, username: String) -> User {
User {
email: email,// email
username: username,// username,
active: true,
sign_in_count: 1,
}
}
//当函数参数和结构体字段同名时,可以直接使用缩略的方式进行初始化.
结构体更新语法
在实际场景中,有一种情况很常见:根据已有的结构体实例,创建新的结构体实例,例如根据已有的 user1
实例来构建 user2
:
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
let user2 = User {
email: String::from("another@example.com"),
..user1
};//只需要对 email 进行赋值,剩下的通过结构体更新语法 ..user1 即可完成。
..
语法表明凡是我们没有显式声明的字段,全部从 user1
中自动获取。需要注意的是 ..user1
必须在结构体的尾部使用。
②元组结构体
结构体必须要有名称,但是结构体的字段可以没有名称,这种结构体长得很像元组,因此被称为元组结构体,例如:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
元组结构体在你希望有一个整体名称,但是又不关心里面字段的名称时将非常有用。
③单元结构体
定义一个类型,但是不关心该类型的内容, 只关心它的行为时,就可以使用 单元结构体
:
struct AlwaysEqual;
let subject = AlwaysEqual;
// 我们不关心 AlwaysEqual 的字段数据,只关心它的行为,因此将它声明为单元结构体,然后再为它实现某个特征
impl SomeTrait for AlwaysEqual {
}
④结构体数据的所有权
如果想在结构体中使用一个引用,就必须加上生命周期。
6.4枚举
枚举(enum 或 enumeration)允许你通过列举可能的成员来定义一个枚举类型,例如扑克牌花色:
enum PokerSuit {
Clubs,
Spades,
Diamonds,
Hearts,
}//这是一个枚举类型
①枚举值
创建 PokerSuit
枚举类型的两个成员实例:
let heart = PokerSuit::Hearts;
let diamond = PokerSuit::Diamonds;
通过 ::
操作符来访问 PokerSuit
下的具体成员,从代码可以清晰看出,heart
和 diamond
都是 PokerSuit
枚举类型的,接着可以定义一个函数来使用它们:
fn main() {
let heart = PokerSuit::Hearts;
let diamond = PokerSuit::Diamonds;
print_suit(heart);
print_suit(diamond);
}
fn print_suit(card: PokerSuit) {
// 需要在定义 enum PokerSuit 的上面添加上 #[derive(Debug)],否则会报 card 没有实现 Debug
println!("{:?}",card);
}
同一个枚举类型下的不同成员还能持有不同的数据类型:
enum PokerCard {
Clubs(u8),
Spades(u8),
Diamonds(char),
Hearts(char),
}
fn main() {
let c1 = PokerCard::Spades(5);
let c2 = PokerCard::Diamonds('A');
}
任何类型的数据都可以放入枚举成员中: 例如字符串、数值、结构体甚至另一个枚举。
②Option 枚举用于处理空值
enum Option<T> {
Some(T),
None,
}
Option
枚举包含两个成员,一个成员表示含有值:Some(T)
, 另一个表示没有值:None
。其中 T
是泛型参数,Some(T)
表示该枚举成员的数据类型是 T
,换句话说,Some
可以包含任何类型的数据。
6.5数组
在 Rust 中,最常用的数组有两种,第一种是速度很快但是长度固定的 array
,第二种是可动态增长的但是有性能损耗的 Vector
,=称 array
为数组,Vector
为动态数组。
固定长度的
①创建数组
let a: [i32; 5] = [1, 2, 3, 4, 5];
可以删去[i32; 5],这部分属于数组声明类型,i32是元素的类型,5是数组长度。
let a = [3; 5];
可以使用上面的语法初始化一个某个值重复出现 N 次的数组:a数组包含五个元素,这些元素的初始化值为3.即与数组声明相同,[类型; 长度]。
②访问数组元素
可以像C语言一样使用索引的方式来访问数组元素,let first=a[0]
.
越界访问
当尝试使用索引访问元素时,Rust 将检查你指定的索引是否小于数组长度。如果索引大于或等于数组长度,Rust 会出现 panic。
数组元素为非基础类型
let array = [String::from("rust is good!"); 8];
println!("{:#?}", array);
以上代码会出错是因为其数组元素是String,非rust基础类型,根据之前所有权部分的知识,我们知道,基本类型在rust中的创建是以copy的形式,String不是。但如果你想达到上述代码的目的,可以调用std::array::from_fn:
let array: [String; 8] = std::array::from_fn(|_i| String::from("rust is good!"));
println!("{:#?}", array);
使用 from_fn
函数创建数组,该函数接受一个闭包作为参数,闭包的参数 _i
表示数组的索引,但在闭包内部没有使用。闭包的返回值是一个包含字符串 “rust is good!” 的 String
对象。
③数组切片
数组切片允许引用集合中的部分连续片段,而不是整个集合,对于数组也是,数组切片允许引用数组的一部分:
let a: [i32; 5] = [1, 2, 3, 4, 5];
let slice: &[i32] = &a[1..3];
assert_eq!(slice, &[2, 3]);
- 切片的长度可以与数组不同,并不是固定的,而是取决于你使用时指定的起始和结束位置
- 创建切片的代价非常小,因为切片只是针对底层数组的一个引用
- 切片类型[T]拥有不固定的大小,而切片引用类型&[T]则具有固定的大小,因为 Rust 很多时候都需要固定大小数据类型,因此&[T]更有用,
&str
字符串切片也同理
7.流程控制
即熟知的循环、分支等流程控制方式。
7.1 if分支控制
fn main() {
let condition = true;
let number = if condition {
5
} else {
6
};
println!("The value of number is: {}", number);
}
与c语言相似,但是要注意的是要保证每个分支返回的类型一样。
7.2 else if多重条件处理
可以将 else if
与 if
、else
组合在一起实现更复杂的条件分支判断,和c语言一致。
7.3 for循环
使用方法:
for 元素 in 集合 {
// 具体操作
}
但是,使用for循环的时候应该使用集合的引用形式,否则,其使用权会被转移,以后就不许再使用这个集合了。
如果想在循环中,修改该元素的值,可以使用mut关键字。
使用方法 | 所有权 |
---|---|
for item in collection | 转移所有权 |
for item in &collection | 不可变借用 |
for item in &mut collection | 可变借用 |
如果想在循环中获取元素的索引:
fn main() {
let a = [4, 3, 2, 1];
// `.iter()` 方法把 `a` 数组变成一个迭代器
for (i, v) in a.iter().enumerate() {
println!("第{}个元素是{}", i + 1, v);
}
}
for (i, v) in a.iter().enumerate() { ... }
:使用 for
循环对数组 a
中的每个元素执行迭代。a.iter()
调用将数组转换为一个迭代器,enumerate()
方法对迭代器进行枚举,返回一个元组,其中第一个元素是索引,第二个元素是值。这样,在每次迭代中,(i, v)
都是一个元组,其中 i
表示元素的索引,v
表示元素的值。
不声明新变量控制一个循环
for _ in 0..10 {
// ...
}
即可控制该过程循环执行十次。在 Rust 中 _
的含义是忽略该值或者类型的意思,如果不使用 _
,那么编译器会给你一个 变量未使用的
的警告。
在rust中,直接循环集合中的元素要比通过索引下标去访问集合快,由于索引访问时会触发rust编译器的临界检查。
7.4 continue和break
与c语言一致,continue用于跳出本次循环,break用于跳出整个循环。
7.5 while循环
如果需要一个条件来循环,当该条件为 true
时,继续循环,条件为 false
,跳出循环,那么 while
就非常适用:
fn main() {
let mut n = 0;
while n <= 5 {
println!("{}!", n);
n = n + 1;
}
println!("我出来了!");
}
7.6 loop循环
loop循环是适用面最高的,它可以适用于所有循环场景,由于 loop
就是一个简单的无限循环,可以在内部实现逻辑通过 break
关键字来控制循环何时结束。
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {}", result);
}
当达到退出条件时,就会通过break退出循环。这里的break可以单独使用,也可以带一个返回值,如上述的counter*2,loop 是一个表达式,因此也可以返回一个值,故该程序的输出为20.