2

Rust-学习笔记

 1 year ago
source link: https://mikeygithub.github.io/2022/10/01/yuque/bbzd4a/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Rust-学习笔记 - 麦奇

Rust-学习笔记 _
Mikey 2022年10月1日 晚上

36k 字

302 分钟

4 次

image.png

image.png

Rust 是一门系统编程语言,专注于安全 ,尤其是并发安全,支持函数式和命令式以及泛型等编程范式的多范式语言。Rust 在语法上和 C++类似,但是设计者想要在保证性能的同时提供更好的内存安全。 Rust 最初是由 Mozilla 研究院的 Graydon Hoare 设计创造,然后在 Dave Herman, Brendan Eich 以及很多其他人的贡献下逐步完善的。 Rust 的设计者们通过在研发 Servo 网站浏览器布局引擎过程中积累的经验优化了 Rust 语言和 Rust 编译器。

Rust 编译器是在 MIT License 和 Apache License 2.0 双重协议声明下的免费开源软件。 Rust 已经连续七年(2016,2017,2018,2019,2020, 2021, 2022)在 Stack Overflow 开发者调查的“最受喜爱编程语言”评选项目中折取桂冠。

1.进入官网下载 https://www.rust-lang.org/

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

2.查看版本

rustc -V

HelloWorld

fn main() {
println!("Hello, world!");
}

下面的关键字目前可以完成对应描述中的功能。

• as:执行基础类型转换,消除包含条目的指定 trait 的歧义,在 use 与 extern crate 语句中对条目进行重命名。
• break:立即退出一个循环。
• const:定义常量元素或不可变裸指针。
• continue:继续下一次循环迭代。
• crate:连接一个外部包或一个代表了当前包的宏变量。
• dyn:表示 trait 对象可以进行动态分发。
• else:if 和 if let 控制流结构的回退分支。
• enum:定义一个枚举。
• extern:连接外部包、函数或变量。
• false:字面量布尔假。
• fn:定义一个函数或函数指针类型。
• for:在迭代器元素上进行迭代,实现一个 trait,指定一个高 阶生命周期。
• if:基于条件表达式结果的分支。
• impl:实现类型自有的功能或 trait 定义的功能。• in:for 循环语法的一部分。
• let:绑定一个变量。
• loop:无条件循环。
• match:用模式匹配一个值。
• mod:定义一个模块。
• move:让一个闭包获得全部捕获变量的所有权。
• mut:声明引用、裸指针或模式绑定的可变性。
• pub:声明结构体字段、impl 块或模块的公共性。
• ref:通过引用绑定。
• return:从函数中返回。
• Self:指代正在其上实现 trait 的类型别名。
• self:指代方法本身或当前模块。
• static:全局变量或持续整个程序执行过程的生命周期。
• struct:定义一个结构体。
• super:当前模块的父模块。
• trait:定义一个 trait。
• true:字面量布尔真。
• type:定义一个类型别名或关联类型。
• unsafe:声明不安全的代码、函数、trait 或实现。
• use:把符号引入作用域中。
• where:声明一个用于约束类型的从句。
• while:基于一个表达式结果的条件循环。

下面的关键字目前还没有任何功能,但它们被 Rust 保留下来以备 将来使用。

• abstract
• async
• become
• box
• do
• final
• macro
• override
• priv
• try
• typeof
• unsized
• virtual
• yield

变量可变性由mut关键字修饰的变量可变,否则是常量无法修改。但是 rust 支持重复定义同名变量,会进行覆盖,如果是类型相同的我们可以使用重新定义变量覆盖

let foo = 5; // foo是不可变的
let mut bar = 5; // bar是可变的

标量类型 (scalar)和复合类型 (compound)

标量类型 (scalar)

标量 类型是单个值类型的统称。Rust 中内建了 4 种基础的标量类型:整数、浮点数、布尔值及字符。

image.png

image.png

image.png

image.png

let x = 2.0; // f64
let y: f32 = 3.0; // f32
let t = true;
let f: bool = false; //附带了显式类型标注的语句
let e = 'a';

复合类型 (compound)

复合类型 (compound type)可以将多个不同类型的值组合为一个类型。Rust 提供了两种内置的基础复合类型:元组 (tuple)和数组 (array)。

元组类型

元组是一种相当常见的复合类型,它可以将其他不同类型的多个值组合进一个复合类型中。元组还拥有一个固定的长度:你无法在声明结束后增加或减少其中的元素数量。

let tup: (i32, f64, u8) = (500, 6.4, 1);
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {}", y);

可以通过索引并使用点号(.)来访问元组中的值:

let tup: (i32, f64, u8) = (500, 6.4, 1);
println!("tup.0 : {}",tup.0);
println!("tup.1 : {}",tup.1);
println!("tup.2 : {}",tup.2);

**数组类型 **

我们同样可以在数组 中存储多个值的集合。与元组不同,数组中的每一个元素都必须是相同的类型。Rust 中的数组拥有固定的长度,一旦声明就再也不能随意更改大小,这与其他某些语言有所不同。

let a = [1,2,3,4,5,6];
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
//指定类型和长度
let arr: [i32;5] = [1,2,3,4,5];
//通过下标访问元素
println!("{},{}", arr[0],arr[4]);

Rust 代码使用蛇形命名法 (snake case)来作为规范函数和变量名称的风格。蛇形命名法只使用小写的字母进行命名,并以下画线分隔单词。

fn 函数名(参数:参数类型) -> 返回类型 {
//函数体
}
fn method(num:i32)->i32{
if num == 1 { return num}
return method(num-1) * num
}
//
fn m()->i32{
5
}

if 表达式

let number = 10;
if number<5 {
println!("true");
} else {
println!("false")
}
let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
let number = if 1<2 { 5 }else { 6 };

Rust 提供了 3 种循环:loop、while 和 for。

fn loop_m(){
loop {
println!("loop")
}
let mut idx = 0;
while idx<100 {
println!("while");
idx+=1;
}
for i in 0..100 {
print!("for");
}
}
[pub] struct StructName {
fieldName: Type,
fieldName: Type,
......
}
struct Person {
name: String,
age: u8,
}
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}

struct User {
username: String,
email: String,
sign_in_count: u64,
}

使用元组定义结构体并实例化

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}

需要注意的是,一旦实例可变,那么实例中的所有字段都将是可变的。Rust 不允许我们单独声明某一部分字段的可变性。如同其他表达式一样,我们可以在函数体的最后一个表达式中构造结构体实例, 来隐式地将这个实例作为结果返回。

struct Rectangle{
width: u32,
height: u32,
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
fn main() {
let rectangle = Rectangle{
width:100,
height:20,
};
println!("rectangle area = {}",area(&rectangle))
}

格式化输出(通过 derive 注解来派生的 trait)

#[derive(Debug)]
struct Rectangle{
width: u32,
height: u32,
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
fn main() {
let rectangle = Rectangle{
width:100,
height:20,
};
println!("rectangle = {:?}",rectangle);
println!("rectangle = {:#?}",rectangle);
println!("rectangle area = {}",area(&rectangle))
}

方法与函数十分相似:它们都使用 fn 关键字及一个名称来进行声明;它们都可以拥有参数和返回值;另外,它们都包含了一段在调用时执行的代码。但是,方法与函数依然是两个不同的概念,因为方法总是被定义在某个结构体的上下文中,并且它们的第一个参数永远都是 self,用于指代调用该方法的结构体实例。

#[derive(Debug)]
struct Rectangle{
width: u32,
height: u32,
}

impl Rectangle {
pub fn area(&self) -> u32 {
self.width * self.height
}
}

fn main() {
let rectangle = Rectangle{
width:100,
height:20,
};
println!("rectangle = {:?}",rectangle);
println!("rectangle = {:#?}",rectangle);
println!("rectangle area = {}",rectangle.area());
}

我们可以在类型名称后添加::来调用关联函数,就像let sq = Rectangle:: square(3);一样。这个函数位于结构体的命名空间中,
这里的::语法不仅被用于关联函数,还被用于模块创建的命名空间。

#[derive(Debug)]
struct Rectangle{
width: u32,
height: u32,
}

impl Rectangle {
pub fn area(&self) -> u32 {
self.width * self.height
}
fn square(size: u32) -> Rectangle {
Rectangle { width: size, height: size }
}
}

fn main() {
let rectangle = Rectangle{
width:100,
height:20,
};
println!("rectangle = {:?}",rectangle);
println!("rectangle = {:#?}",rectangle);
println!("rectangle area = {}",rectangle.area());
let s = Rectangle::square(10);
}
enum IpAddrKind {
V4,
V6,
}
//使用
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
}
enum IpAddrKind {
V4,
V6,
}
struct IpAddr {
kind: IpAddrKind,
address: String,
}
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};
let loopback = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}

Option

fn main() {
let x: Option<u32> = Some(2);
assert_eq!(x.is_some(), true);
}

Match

enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u32 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}

fn main() {
let five = Some(5);
let six = plus_one(five);
println!("six={:?}",six);
let none = plus_one(None);
println!("six={:?}",none);
}

通配符(下划线)

fn main() {
let some_u8_value = 0u8;
match some_u8_value {
1 => println!("one"),
3 => println!("three"),
5 => println!("five"),
7 => println!("seven"),
_ => (),
}
if let Some(3) = some_u8_value {
println!("three");
}
}
use xxx::xxx;
use std::io;
  • Rust 中的每一个值都有一个对应的变量作为它的所有者 。
  • 在同一时间内,值有且仅有一个所有者。
  • 当所有者离开自己的作用域时,它持有的值就会被释放掉。

变量作用域

截屏2022-10-05 12.07.53.png

截屏2022-10-05 12.07.53.png

内存与分配

let s = String::from("hello");
  • :: 表示使用 String 命名空间下的相关 api
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() 函数向String空间的尾部添加了一段字面量
println!("{}", s); // 这里会输出完整的
  • 我们使用的内存是由操作系统在运行时动态分配出来的。
  • 当使用完 String 时,我们需要通过某种方式来将这些内存归还 给操作系统。

Rust 提供了另一套解决方案:内存会自动地在拥有它的变量离开作用域后进行释放。下面的代码类似于示例中的代码,不过我们将字符串字面量换成了 String 类型:

{
let s = String::from("hello");//从这里开始,变量s变得有效
//...对s进行操作
} //作用域到指令结束,变量s失效

有一个很适合用来回收内存给操作系统的地方:变量 s 离开作用域的地方。Rust 在变量离开作用域时,会调用一个叫作 drop 的特殊函数。String 类型的作者可以在这个函数中编写释放内存的代码。记住,Rust 会在作用域结束的地方(即}处)自动调用 drop 函数。

所有权与函数

下面是一些拥有 Copy 这种 trait 的类型:

  • 所有的整数类型,诸如 u32。
  • 仅拥有两种值(true 和 false)的布尔类型:bool。
  • 字符类型:char。
  • 所有的浮点类型,诸如 f64。
  • 如果元组包含的所有字段的类型都是 Copy 的,那么这个元组也是 Copy 的。例如,(i32, i32)是 Copy 的,但(i32, String)则不是。

将值传递给函数在语义上类似于对变量进行赋值。将变量传递给函数将会触发移动或复制,就像是赋值语句一样。

截屏2022-10-05 12.33.46.png

截屏2022-10-05 12.33.46.png
尝试在调用 takes_ownership 后使用变量 s 会导致编译时错误。这类静态检查可以使我们免于犯错。你可以尝试在 main 函数中使用 s 和 x 变量,来看一看在所有权规则的约束下能够在哪些地方合法地使用它们。

如果我们想进行函数调用后还继续使用该变量,那我们可以将它调用完进行返回

fn m2(){
let mut s = String::from("hello");
s = takes_ownership(s);
println!("s={}",s);
let x = 5;
makes_copy(x);
println!("x={}",x);
}

fn takes_ownership(s:String)->String{
println!("{}",s);
s
}

但是如果我又想当前传入函数的变量不被回收又能返回计算结果,那么我们可以同时对其进行返回(多返回值)

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

上面的方法都显得十分的繁杂,下面的这种方法更加简单,我们在传入函数的变量时,我们传入变量的地址

fn m4(){
let mut s = String::from("hello");
let l = takes_own(&s);
println!("s={},l={}",s,l);
let x = 5;
makes_copy(x);
println!("x={}",x);
}
fn takes_own(s:&String)->usize{
println!("{}",s);
s.len()
}

与使用&进行引用相反的操作被称为解引用(dereferencing), 它使用 * 作为运算符。

这里的&s1 语法允许我们在不转移所有权的前提下,创建一个指向 s1 值的引用。由于引用不持有值的所有权,所以当引用离开当前作用
域时,它指向的值也不会被丢弃。 同理,函数签名中的&用来表明参数 s 的类型是一个引用。

fn calculate_length(s: &String) -> usize { // s 是一个指向 String 的引用
s.len()
} // 到这里,s离开作用域。但是由于它并不持有自己所指向值的所有权,
//所以没有什么特殊的事情会发生

此处,变量 s 的有效作用域与其他任何函数参数一样,唯一不同的是,它不会在离开自己的作用域时销毁其指向的数据,因为它并不拥有该数据的所有权。当一个函数使用引用而不是值本身作为参数时, 我们便不需要为了归还所有权而特意去返回值,毕竟在这种情况下, 我们根本没有取得所有权。

这 种 通 过 引 用 传 递 参 数 给 函 数 的 方 法 也 被 称 为 借 用 (borrowing)。在现实生活中,假如一个人拥有某件东西,你可以从他那里把东西借过来。但是当你使用完毕时,就必须将东西还回去。

那我们是否可以在方法体里面修改该变量的值呢

fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}

此时修改是无法修改的,我们得用可变的引用才允许修改

fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}

使用拥有指针概念的语言会非常容易错误地创建出悬垂指针 。这类指针指向曾经存在的某处内存地址,但该内存已经被释放掉甚至是被重新分配另作他用了。而在 Rust 语言中,编译器会确保引用永远不会进入这种悬垂状态。假如我们当前持有某个数据的引用,那么编译器可以保证这个数据不会在引用被销毁前离开自己的作用域。

fn main(){
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}

此时无法通过编译 this function's return type contains a borrowed value, but there is no value for it to be borrowed from 由于变量 s 创建在函数 dangle 内,所以它会在 dangle 执行完毕时随之释放。但是,我们的代码依旧尝试返回一个指向 s 的引用,这个引用指向的是一个无效的 String,这可不对!Rust 成功地拦截了我们的危险代码。

解决这个问题的方法也很简单,直接返回 String 就好:

fn no_dangle() -> String {
let s = String::from("hello");
s
}

引用的规则

  • 在任何一段给定的时间里,你要么只能拥有一个可变引用,要么只能拥有任意数量的不可变引用。
  • 引用总是有效的。

除了引用,Rust 还有另外一种不持有所有权的数据类型:切片(slice)。切片允许我们引用集合中某一段连续的元素序列,而不是整个集合。

编写一个搜索函数,它接收字符串作为参数,并将字符串中的首个单词作为结果返回。如果字符串中不存在空格,那么就意味着整个字符串是一个单词,直接返回整个字符串作为结果即可。

fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}

这段代码首先使用 as_bytes 方法将 String 转换为字节数组,因为我们的算法需要依次检查 String 中的字节是否为空格。接着,我们通过 iter 方法创建了一个可以遍历字节数组的迭代器。

fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // 索引5会被绑定到变量word上
s.clear(); // 这里的clear方法会清空当前字符串,使之变为""
// 虽然word依然拥有5这个值,但因为我们用于搜索的字符串发生了改变,
//所以这个索引也就没有任何意义了,word到这里便失去了有效性
println!("idx={}",word);
println!("s={}",s);
}

这种 API 的设计方式使我们需要随时关注 word 的有效性,确保它与 s 中的数据是一致的,类似的工作往往相当烦琐且易于出错。这种情况对于另一个函数 second_word 而言更加明显。

字符串切片:字符串切片是指向 String 对象中某个连续部分的引用,它的使用方式如下所示:

fn slice(){
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
let full = &s[..];
// let full = &s[0..];
let hi = &s[..5];
println!("{}",hello);
println!("{}",hi);
println!("{}",world);
println!("{}",full);
}

重构 first_word 函数

fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
// first_word 可以接收String对象的切片作为参数
let word = first_word(&my_string[..]);
let my_string_literal = "hello world";
// first_word 可以接收字符串字面量的切片作为参数
let word = first_word(&my_string_literal[..]);
// 由于字符串字面量本身就是切片,所以我们可以在这里直接将它传入函数,
// 而不需要使用额外的切片语法!
let word = first_word(&my_string_literal[..]);
}

fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}

其他类型的切片

从名字上就可以看出来,字符串切片是专门用于字符串的。但实际上,Rust 还有其他更加通用的切片类型,以下面的数组为例:

let a = [1, 2, 3, 4, 5];

就像我们想要引用字符串的某个部分一样,你也可能会希望引用数组的某个部分。这时,我们可以这样做:

let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];

这里的切片类型是&[i32],它在内部存储了一个指向起始元素的引用及长度,这与字符串切片的工作机制完全一样。你将在各种各样的集合中接触到此类切片

闭包(closure),一个类似于函数且可以存储在变量中的结构。

Rust 中的闭包是一种可以存入变量或作为参数传递给其他函数的 匿名函数。你可以在一个地方创建闭包,然后在不同的上下文环境中 调用该闭包来完成运算。和一般的函数不同,闭包可以从定义它的作 用域中捕获值。

我们希望在程序中将代码定义在一处,但只在真正需要结果时才执行 相关代码。而这正是闭包的用武之地!

迭代器(iterator),一种处理一系列元素的方法。

  • 包(package) :一个用于构建、测试并分享单元包的 Cargo 功能。
  • 单元包(crate) :一个用于生成库或可执行文件的树形模块结构。
  • 模块(module) 及 use 关键字: 它们被用于控制文件结构、 作用域及路径的私有性。
  • 路径(path) :一种用于命名条目的方法,这些条目包括结构体、函数和模块等。

包与单元包

1.rust 中声明包的关键字是 mod,如果是公共的,则需要声明为 pub mod。

pub mod game {
use std::cmp::Ordering;
use std::io;
use rand::Rng;
use crate::example;//使用另一个包下的mod方法
pub fn guess() {
example::example::say_hello();
println!("guess number 1..10");
println!("please enter you number:");
let mut cnt = 0;
loop {
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {}", guess);
let secret_number = rand::thread_rng().gen_range(1..=10);
println!("The secret number is: {}", secret_number);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
cnt+=1;
if cnt==10 {
println!("game over");
break
}
}
}
}

rust 中,每个文件都是一个包,文件名就是包名。如果是和 main.rs 同级的文件,可以直接使用文件名作为包名。如果有多级目录,那么每个目录下,都要有一个 mod.rs 作为包的入口,rust 的 mod.rs 文件中需要显式地说明当前目录下的包名,以及是否是 pub 类型的。

pub mod guess;//声明一个guess模块(文件名)

2.use 关键字用于引用,当需要使用其他模块的方法或者结构体时需要先进行导入

use xxx::xxx
//案例
use std::io;
use rand::Rng;
use std::cmp::Ordering;

3.导入后进行调用

mod chapter4;
mod chapter3;
mod example;
mod guess;

use guess::guess::game as g;

fn main() {
guess::guess::game::guess();
//等同于
g::guess();
}

4.关于导入的路径问题

  • use::crate:: 来引用上级的模块

  • 如果是 main 文件直接使用 mod 声明模块就行,main 文件比较特殊

    5.相同模块了怎么办?

    不担心 rust 提供了 as 关键字来别名模块

Cargo

rust 使用 cargo 来管理项目依赖

Rust's package manager

USAGE:
cargo [+toolchain] [OPTIONS] [SUBCOMMAND]

OPTIONS:
-V, --version Print version info and exit
--list List installed commands
--explain <CODE> Run `rustc --explain CODE`
-v, --verbose Use verbose output (-vv very verbose/build.rs output)
-q, --quiet Do not print cargo log messages
--color <WHEN> Coloring: auto, always, never
--frozen Require Cargo.lock and cache are up to date
--locked Require Cargo.lock is up to date
--offline Run without accessing the network
--config <KEY=VALUE> Override a configuration value
-Z <FLAG> Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for
details
-h, --help Print help information

Some common cargo commands are (see all commands with --list):
build, b Compile the current package
check, c Analyze the current package and report errors, but don't build object files
clean Remove the target directory
doc, d Build this package's and its dependencies' documentation
new Create a new cargo package
init Create a new cargo package in an existing directory
add Add dependencies to a manifest file
run, r Run a binary or example of the local package
test, t Run the tests
bench Run the benchmarks
update Update dependencies listed in Cargo.lock
search Search registry for crates
publish Package and upload this package to the registry
install Install a Rust binary. Default location is $HOME/.cargo/bin
uninstall Uninstall a Rust binary

See 'cargo help <command>' for more information on a specific command.

查看包文档

cargo doc --open
  • 动态数组(vector)可以让你连续地存储任意多个值。
  • 字符串(string)是字符的集合。
  • 哈希映射(hash map)可以让你将值关联到一个特定的键上,它是另外一种数据结构—映射 (map)的特殊实现。
pub fn m1(){
let mut v:Vec<i32> = Vec::new();
println!("v={:?}",v);
for i in 0..10 {
v.push(i);
}
println!("v={:?}",v);
let third: &i32 = &v[2];
println!("The third element is {}", third);
match v.get(2) {
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),
}
//动态数组一旦离开作用域就会被立即销毁

//遍历数组
for i in &v {
println!("{}", i);
}
}

String 实际上是一个基于 Vec的封装类型。

pub struct String {
vec: Vec<u8>,
}
pub fn m1(){
//定义一个字符串
let mut s = String::new();
let data = "initial contents";
let s = data.to_string();
// 这个方法同样也可以直接作用于字面量
let s = "initial contents".to_string();
//支持多种编码
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");
//更新字符串
let mut s = String::from("foo");
s.push_str("bar");
println!("s={}",s);//foobar

let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // 注意这里的s1已经被移动且再也不能被使用了

// println!("s1={}",s1);//error value borrowed here after move
println!("s2={}",s2);
println!("s2={}",s3);

//format
let s1 = String::from("hello");
let s2 = String::from("world");
let s3 = format!("{}-{}",s1,s2);
println!("{}",s3);

//len
let len = s1.len();
println!("hello len = {}",len);
}

字符串切片

let hello = "hello rust";
let slide = &hello[1..5];
println!("slide = {}",slide);//ello
//遍历
for c in "hello world".chars() {
println!("{}", c);
}
for c in "hello world".as_bytes() {
println!("{}", c);
}

创建哈希映射(首次插入会确定其 kv 值)

use std::collections::HashMap;
let mut scores = HashMap::new();//首次插入会确定其kv值
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

let mut map = HashMap::new();
map.insert(1,1);//k=i32,v=i32
//生命周期
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(field_name, field_value);
// filed_name和field_value从这一刻开始失效,若尝试使用它们则会导致编译错误!
pub fn m1(){
use std::collections::HashMap;
//创建方式1
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
println!("{:?}",scores);
//创建方式2
let teams = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];
let scores2: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
//
let len = scores.len();
println!("hashmap length = {}",len);
//api
let blue = scores.get("Blue");
println!("blue = {:?}",blue);
//生命周期
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(field_name, field_value);
// filed_name和field_value从这一刻开始失效,若尝试使用它们则会导致编译错误!

//获取
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
let team_name = String::from("Blue");
let score = scores.get(&team_name);
println!("score = {:?}",score);
//遍历
for (key, value) in &scores {
println!("{}: {}", key, value);
}
//检查是否存在,不存在则插入
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
println!("{:?}", scores);

//案例统计单词出现的次数
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
println!("{:?}", map);
}

在 Rust 中,我们将错误分为两大类:可恢复 错误与不可恢复 错误。对于可恢复错误,比如文件未找到等,一般需要将它们报告给用户并再次尝试进行操作。而不可恢复错误往往就是 bug 的另一种说法, 比如尝试访问超出数组结尾的位置等。虽然 Rust 没有类似的异常机制,但它提供了用于可恢复错误的类型Result<T, E>,以及在程序出现不可恢复错误时中止运行的 panic! 宏。

不可恢复错误与 panic!

程序会在panic!宏执行时打印出一段错误提示信息,展开并清理当前的调用栈,然后退出程序。这种情况大部分都发生在某个错误被检测到,但程序员却不知该如何处理的时候。

当 panic 发生时,程序会默认开始栈展开。这意味着 Rust 会沿着调用栈的反向顺序遍历所有调用函数,并依次清理这些函数中的数据。但是为了支持这种遍历和清理操作,我们需要在二进制中存储许多额外信息。除了展开,我们还可以选择立即终止程序,它会直接结束程序且不进行任何清理工作,程序所使用过的内存只能由操作系统来进行回收。假如项目需要使最终二进制包尽可能小,那么你可以通过在Cargo.toml 文件中的[profile]区域添加panic = 'abort'来将 panic 的默认行为从展开切换为终止。例如,如果你想要在发布模式中使用终止模式,那么可以在配置文件中加入:

[profile.release]
panic = 'abort'
pub fn m1(){
// panic!("test panic");//手动触发panic!
let v = vec![1, 2, 3];
v[99];//尝试越界访问动态数组中的元素,这会导致panic!
}

可恢复错误与 Result

大部分的错误其实都没有严重到需要整个程序停止运行的地步。 函数常常会由于一些可以简单解释并做出响应的原因而运行失败。例如,尝试打开文件的操作会因为文件不存在而失败。你也许会在这种情形下考虑创建该文件而不是终止进程。

pub fn m(){
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => {
panic!("There was a problem opening the file: {:?}", error)
},
};
}

匹配不同的错误

pub fn m(){
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {//如果没有则创建
Ok(fc) => fc,
Err(e) => panic!("Tried to create file but there was a problem:{:?}", e),
},
other_error => panic!("There was a problem opening the file: {:?}", other_error),
},
};
}

用不同的方式处理不同的错误类型

pub fn m(){
let f = File::open("hello.txt").map_err(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Tried to create file but there was a problem: {:?}", error);
})
} else {
panic!("There was a problem opening the file: {:?}", error);
}
});
}

虽然使用 match 运行得很不错,但使用它所编写出来的代码可能会显得有些冗长,且无法较好地表明其意图。类型 Result<T, E>本身也定义了许多辅助方法来应对各式各样的任务。其中一个被称为 unwrap 的方法实现了我们在示例 9-4 中编写的 match 表达式的效果。当 Result 的返回值是 Ok 变体时,unwrap 就会返回 Ok 内部的值。而当 Result 的返回值是 Err 变体时,unwrap 则会替我们调用 panic! 宏。下面是一个在实际代码中使用 unwrap 的例子:

fn main() {
let f = File::open("hello.txt").unwrap();
}

还有另外一个被称作 expect 的方法,它允许我们在 unwrap 的基础上指定 panic! 所附带的错误提示信息。使用 expect 并附带上一段清晰的错误提示信息可以阐明你的意图,并使你更容易追踪到 panic 的起源。下面演示了 expect 的使用语法:

use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}

什么时候应该使用 panic!,而什么时候又应该返回 Result 呢?代码一旦发生 panic,就再也没有恢复的可能了。只要你认为自己可以代替调用者决定某种情形是不可恢复的,那么就可以使用 panic!,而不用考虑错误是否存在可以恢复的机会。

fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}

泛型解决的其实就是代码的复用性问题,针对一些可以共有的代码(入参类型不同但是逻辑一样)可以采用泛型来进行

在函数中定义

fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}

在结构体中定义

struct Point<T> {
x: T,
y: T,
}

fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}

在枚举中定义

enum Option<T> {
Some(T),
None,
}

在方法中定义

struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}

当你使用泛型参数时,你也许会好奇这种机制是否存在一定的运行时消耗。好消息是,Rust 实现泛型的方式决定了使用泛型的代码与使用具体类型的代码相比不会有任何速度上的差异。 为了实现这一点,Rust 会在编译时执行泛型代码的单态化(monomorphization)。单态化 是一个在编译期将泛型代码转换为特定代码的过程,它会将所有使用过的具体类型填入泛型参数从而得到有具体类型的代码。

在这个过程中,编译器所做的工作与我们在创建泛型函数时相反:它会寻找所有泛型代码被调用过的地方,并基于该泛型代码所使用的具体类型生成代码。

trait:定义共享行为

trait(特征)被用来向 Rust 编译器描述某些特定类型拥有的且能够被其他类型共享的功能,它使我们可以以一种抽象的方式来定义共享行为。我们还可以使用 trait 约束来将泛型参数指定为实现了某些特定行为的类型。

注意 :trait 与其他语言中常被称为接口(interface)的功能类似,但也不尽相同。

定义 trait

类型的行为由该类型本身可供调用的方法组成。当我们可以在不同的类型上调用相同的方法时,我们就称这些类型共享了相同的行为。trait 提供了一种将特定方法签名组合起来的途径,它定义了为达成某种目的所必需的行为集合。

打个比方,假如我们拥有多个结构体,它们分别持有不同类型、 不同数量的文本字段:其中的 NewsArticle 结构体存放了某地发生的新 闻故事,而 Tweet 结构体则包含了最多 280 个字符的推文,以及用于描述该推文是一条新推文、一条转发推文还是一条回复的元数据。

此时,我们想要创建一个多媒体聚合库,用来显示存储在 NewsArticle 或 Tweet 结构体实例中的数据摘要。为了达到这一目标, 我们需要为每个类型都实现摘要行为,从而可以在实例上调用统一的 summarize 方法来请求摘要内容。展示了用于表达这一行为的 Summary trait 定义。

pub trait Summary {
fn summarize(&self) -> String;
}

一个 trait 可以包含多个方法:每个方法签名占据单独一行并以分号结尾。

为类型实现 trait

pub trait Summary {
fn summarize(&self) -> String;
}

pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
pub fn m() {
let n = NewsArticle{
headline: "123".to_string(),
location: "456".to_string(),
author: "789".to_string(),
content: "0".to_string()
};
let t = Tweet{
username: "1".to_string(),
content: "2".to_string(),
reply: false,
retweet: false
};
println!("summarize = {}",n.summarize());
println!("summarize = {}",t.summarize());
}

提供默认实现的 trait

pub trait Summary {
// fn summarize(&self) -> String;
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}

使用 trait 作为参数

pub fn notify(item: impl Summary) {
println!("Breaking news! {}", item.summarize());
}

使用 trait 作为泛型约束

pub fn notify<T: Summary>(item: T) {
println!("Breaking news! {}", item.summarize());
}

通过+语法来指定多个 trait 约束

pub fn notify<T: Summary + Display>(item: T) {

}

使用 where 从句来简化 trait 约束

fn some_function0<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {
0
}

// 我们可以使用where从句改写为:
fn some_function1<T, U>(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
{
0
}

使用 trait 来修饰重写largest函数

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}

使用 trait 约束来有条件地实现方法

通过在带有泛型参数的 impl 代码块中使用 trait 约束,我们可以单独为实现了指定 trait 的类型编写方法

struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self {
x,
y,
}
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}

只有在内部类型 T 实现了 PartialOrd(用于比较)与 Display(用于打印)这两个 trait 的前提下,才会实现 cmd_display 方法

使用生命周期保证引用的有效性

Rust 的每个引用都有自己的生命周期(lifetime),它对应着引用保持有效性的作用域。在大多数时候,生命周期都是隐式且可以被推导出来的,就如同大部分时候类型也是可以被推导的一样。当出现了多个可能的类型时,我们就必须手动声明类型。类似地,当引用的生命周期可能以不同的方式相互关联时,我们就必须手动标注生命周期。

Rust 需要我们注明泛型生命周期参数之间的关系,来确保运行时实际使用的引用一定是有效的。

生命周期的概念不同于其他编程语言中的工具,从某种意义上说,它也是 Rust 最与众不同的特性。

使用生命周期来避免悬垂(空)引用

pub fn m() {
let r;
{
let x = 5;
r = &x;
}
println!("r = {}",r);
// 变量x的存活周期不够长。这是因为x 在到达第7行,也就是内部作用域结束时离开了自己的作用域。而r对 于整个外部作用域始终是有效的,它的作用域要更大一些,也就是我
// 们所说的“存活得更久一些”。假如Rust允许这段代码运行,r就会引用到在x离开作用域时已经释放的内存,这时任何基于r所进行的操作 都无法正确地进行。
}

借用检查器

Rust 编译器拥有一个借用检查器 (borrow checker),它被用于比较不同的作用域并确定所有借用的合法性。

函数签名中的生命周期标注

生命周期的标注使用了一种明显不同的语法:它们的参数名称必须以撇号(')开头,且通常使用全小写字符。与泛型一样,它们的名称通常也会非常简短。'a被大部分开发者选择作为默认使用的名称。我们会将生命周期参数的标注填写在&引用运算符之后,并通过一个空格符来将标注与引用类型区分开来。

&i32 //引用
&'a i32//拥有显式生命周期的引用
&'a mut i32//拥有显式生命周期的可变引用

单个生命周期的标注本身并没有太多意义,标注之所以存在是为了向 Rust 描述多个泛型生命周期参数之间的关系。例如,假设我们编写了一个函数,这个函数的参数 first 是一个指向i32的引用,并且拥有生命周期'a。它的另一个参数 second 同样也是指向 i32 且拥有生命周期'a的引用。这样的标注就意味着:first 和 second 的引用必须与这里的泛型生命周期存活一样长的时间。

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}

pub fn m(){
let s1 = "abc";
let s2 = "opqsdf";
let l = longest(&s1,&s2);
println!("the longest is : {}",l)
// error : 我们需要给返回类型标注一个
// 泛型生命周期参数,因为Rust并不能确定返回的引用会指向x还是指向
// y。实际上,即便是编写代码的我们也无法做出这个判断。因为函数体
// 中的if代码块返回了x的引用,而else代码块则返回了y的引用
}

结构体定义中的生命周期标注

struct ImportantExcerpt<'a> {
part: &'a str,
}

pub fn m() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.')
.next()
.expect("Could not find a '.'");
let i = ImportantExcerpt { part: first_sentence };
println!("{}", i.part);
}
// 结构体中持有了引用,所以它的定义中需要添加生命周期标注这个结构体仅有一个字段part,用于存储一个字符串切片,
// 也就 是一个引用。如同泛型数据类型一样,为了在结构体定义中使用生 命周期参数,我们需要在结构体名称后的尖括号内声明泛型生命周期参数的名字。
// 这个标注意味着ImportantExcerpt实例的存活时间不能超过存储在part字段中的引用的存活时间。

静态生命周期

Rust 中还存在一种特殊的生命周期’static,它表示整个程序的执行期。所有的字符串字面量都拥有’static 生命周期,我们可以像下面一样显式地把它们标注出来:

let s: &'static str = "I have a static lifetime.";

字符串的文本被直接存储在二进制程序中,并总是可用的。因此,所有字符串字面量的生命周期都是’static。

同时使用泛型参数、trait 约束与生命周期

fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str where T: Display {
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}
#[cfg(test)]
mod tests {
use crate::gp::gp::longest;
use super::*;

#[test]
fn it_works() {
let s1 = "hello world";
let s2 = "hello rust";
let result = longest(&s1,&s2);
assert_eq!(result, s1);
}
}

运行cargo test 会执行当前项目下所有的测试用例

running 2 tests
test gp::traited::tests::test_same ... ok
test gp::traited::tests::it_works ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

should_panic


pub struct Guess {
value: u32,
}
impl Guess {
pub fn new(value: u32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}
Guess {
value
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]//标记了这个属性的测试函数会在代码发生panic时顺利通过,而在代码不发生panic时执行失败。
fn greater_than_100() {
Guess::new(200);
}
}

使用 Result<T, E>编写测试

#[cfg(test)]
mod tests {
#[test]
fn it_works() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}

控制测试的运行方式

并行或串行地进行测试

当你尝试运行多个测试时,Rust 会默认使用多线程来并行执行它 们。这样可以让测试更快地运行完毕,从而尽早得到代码是否能正常 工作的反馈。但由于测试是同时进行的,所以开发者必须保证测试之 间不会互相依赖,或者依赖到同一个共享的状态或环境上,例如当前工作目录、环境变量等。

cargo test -- --test-threads=1

显示函数输出

cargo test -- --nocapture

只运行部分特定名称的测试

cargo test 函数名称

通过过滤名称来运行多个测试

cargo test 方法关键字

通过显式指定来忽略某些测试

#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
#[test]
#[ignore]
fn expensive_test() {
// 需要运行一个小时的代码
}

1.创 建 一 个 tests 文 件 夹 , 并 创 建 文 件 tests/integration_test.rs (对应的测试文件),将示例 11-13 中的代码输入其中。

use adder;
#[test]
fn it_adds_two() {
assert_eq!(4, adder::add_two(2));
}

与单元测试不同,集成测试需要在代码顶部添加语句 use adder。 这是因为 tests 目录下的每一个文件都是一个独立的包,所以我们需要将目标库导入每一个测试包中。 我们不需要为tests/integration_test.rs 中的任何代码标注# [cfg(test)]。Cargo 对 tests 目录进行了特殊处理,它只会在执行 cargo test 命令时编译这个目录下的文件。

use std::io;
use rand::Rng;
use std::cmp::Ordering;

fn main() {
guess()
}

fn guess() {
println!("guess number 1..10");
println!("please enter you number:");
loop {
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("Failed to read line");
let guess: u32 = match guess.trim().parse(){
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {}", guess);
let secret_number = rand::thread_rng().gen_range(1..=10);
println!("The secret number is: {}", secret_number);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break
},
}
}
}

minigrep

use std::error::Error;
use std::{env, fs};

pub fn run(config:Config) -> Result<(), Box<dyn Error>>{

let contents = fs::read_to_string(config.filename)?;
let results = if config.case_sensitive {
search(&config.query, &contents)
} else {
search_case_insensitive(&config.query, &contents)
};
for line in results {
println!("{}", line);
}
Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut ret = Vec::new();
for line in contents.lines() {
// do something with line
if line.contains(query) {
ret.push(line);
}
}
ret
}

pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}

pub struct Config {
pub query: String,
pub filename: String,
pub case_sensitive: bool,
}

impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();

let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
Ok(Config { query, filename,case_sensitive })
}
}


#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(
vec!["safe, fast, productive."],
search(query, contents)
);
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
use std::{env, process};
use minigrep::Config;
use minigrep::run;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {}", err);
process::exit(1)
});

println!("Searching for = {}", config.query);
println!("In file = {}", config.filename);

if let Err(e) = run(config) {
eprintln!("Application error: {}", e);
process::exit(1);
}
}

官网:https://www.rust-lang.org/
视频:https://www.bilibili.com/video/BV1hp4y1k7SV/
案例:https://doc.rust-lang.org/stable/rust-by-example/
书籍:rust 权威指南.pdf
教程:https://course.rs/about-book.html


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK