21知识拾遗

Formatted print

打印信息,是由std::fmt 包中的几个宏来处理的

  • format!:将格式化的内容转为 String;
  • print!: 与format!类似,但是将内容打印到终端(io::stdout);
  • println!:与print!类似,会换行;
  • eprint!:与format!类似,不过是将文本打印到标准错误(io:stderr);
  • eprintln!:与eprint!类似,会换行。

std::fmt 中包含了很多控制文本显示的 trait,下面列举两个重要的基本内容:

  • fmt::Debug:使用{:?}标记,格式化文本以便调试;

  • fmt::Debug,还可以使用{:#?}标记,使文本打印更好看一些;

    • 所有std标准库下得类型都实现了这个 trait。

    • 如果想使用在通用情况下,就使用#[derive(Debug)];

    • 对于自定义的struct,fmt::Debug 是派生trait,所以不需要手动实现,仅需要在struct上添加#[derive(Debug)] 注解即可。

  • fmt::Display:格式化文本使其以更优雅,更友好的方式展示;

    • 提供{}标记,按照实现fmt::Display 实现的自定义方式打印文本;

    • 实现fmt::Display trait 的类型会自动实现ToString trait,这会允许我们将类型转换为String

fmt::Display 实现的方式

需要实现 fmt 方法,同时需要利用 write!宏来处理

1
2
3
4
5
6
7
// Similarly, implement `Display` for `Point2D`
impl fmt::Display for Point2D {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Customize so only `x` and `y` are denoted.
write!(f, "x: {}, y: {}", self.x, self.y)
}
}

write!宏是将后面的参数写入 f 中,

  • write! 类似于 format!,不过它会将格式化内容写入buffer中(即第一个参数中)

使用时,println!("{}",xx) ,即可。

println! 使用注意

注意:println! 语句每次打印都会将内容 flush 到终端,因为通常需要打印新行,所以如果在意程序性能,需要谨慎使用println!

格式化文本的方式

格式化是通过格式字符串指定的。

元组tuples

元组(tuples)是一个类型不同的值的集合

与 数组相反,数组是类型相同的值的集合

使用圆括号声明

(T1,T2,...)

数组和切片

数组(array)是相同类型的值的集合,存储在连续的内存中

使用方括号[]声明,

编译时就已经知道其长度,

可以用[T;length]的方式声明类型和长度,

  • 也可以通过直接赋值的方式声明,这样编译器也可以知道其类型和长度;

自定义类型

struct:定义一个结构体;

enum: 定义一个枚举;

常量(constants)可以通过 conststatic关键字来定义;

  • 生命周期都是整个程序执行周期;
  • const,关键字定义的常量不可修改
  • static ,支持可变,它的'static生命周期可以被推断出来,不需要显示指定。同时访问或修改一个可变的static变量是unsafe的;
  • 无论使用const 还是 static 定义常量,都需要声明变量类型;

enum,允许我们创建一种有可能有多个变体的类型。

  • struct 可以作为 enum的变体之一。
  • 每种变体的类型都是该enum本身。
  • enum 也可以创建方法。
1
2
3
4
5
6
7
8
9
10
enum WebEvent {
// An `enum` may either be `unit-like`,
PageLoad,
PageUnload,
// like tuple structs,
KeyPress(char),
Paste(String),
// or c-like structures.
Click { x: i64, y: i64 },
}

在enum 中使用别名

  1. 如果枚举名太长或太通用,可以通过type 的方式创建别名,可以使用别名引用枚举中的每一个变体。
  2. 最常见的情况是在impl块中使用Self别名。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
enum VeryVerboseEnumOfThingsToDoWithNumbers {
Add,
Subtract,
}

// Creates a type alias
type Operations = VeryVerboseEnumOfThingsToDoWithNumbers;

fn main() {
// We can refer to each variant via its alias, not its long and inconvenient
// name.
let x = Operations::Add;
}


// 使用 Self 做impl 块中做别名的情况。
enum VeryVerboseEnumOfThingsToDoWithNumbers {
Add,
Subtract,
}

impl VeryVerboseEnumOfThingsToDoWithNumbers {
fn run(&self, x: i32, y: i32) -> i32 {
match self {
Self::Add => x + y,
Self::Subtract => x - y,
}
}
}

1
2
// An attribute to hide warnings for unused code.
#![allow(dead_code)]

枚举也可以用作类C(C-like)的枚举???

  • 没懂有什么不同,感觉差不多,不过样例中有注释显示/隐式的标识符。
  • 可能需要先了解一下C语言中的enum是什么样子的。

explicit 显示的

implicit 隐式的

& vs ref

涉及到 ref 的内容,我们通常是在 match 表达式中使用。

默认情况下,match 表达式会消耗匹配中的值,即匹配到的值的所有权会发生转移;

这种情况下,后面就不能再次使用这些值。因为被匹配的值的所有权已经发生move

如果匹配的类型实现了 Copy trait,则不会发生move,而是 copy。不会出现上面的问题。

当后面需要使用该值的情况下,可能发生的遇到的问题,

  • 怎么解决?
  • 使用ref

ref 使用在模式匹配中,可以使其变为借用(borrow)而不是move

ref 使用在 match 中,并不会影响值是否被匹配,只会影响匹配的方式;

样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 默认情况下的情况
let maybe_name = Some(String::from("Alice"));
// The variable 'maybe_name' is consumed here ...
match maybe_name {
Some(n) => println!("Hello, {}", n),
_ => println!("Hello, world"),
}
// ... and is now unavailable.
println!("Hello again, {}", maybe_name.unwrap_or("world".into()));


// 使用了 `ref` 关键字的效果
let maybe_name = Some(String::from("Alice"));
// Using `ref`, the value is borrowed, not moved ...
match maybe_name {
Some(ref n) => println!("Hello, {}", n),
_ => println!("Hello, world"),
}
// ... so it's available here!
println!("Hello again, {}", maybe_name.unwrap_or("world".into()));

& vs ref

  • ref ,表示我们仅引用该值,
  • &,表示我们期望使用一个对象的引用,
    • &FooFoo 是不一样的。

ref - Rust (rust-lang.org)

关键字 ref 与 主类型 reference 的区别

变量绑定

这种行为称为 variable bindings,变量绑定;

编译器会警告没有使用的变量绑定。此时,可以通过使用带有下划线(underscore)的变量名的形式消除警告。

let _unused_variable = 3u32;

block,在 Rust 中,变量绑定是由作用域的(语言都有),并且被限制在一个block中;

  • block 是一个用花括号括起来的语句(statements)的集合;

变量 shadow

变量 freezing

  • 在 shadow 的时候,如果使用不可变变量覆盖可变可变量,那么原来的可变变量就会发生freezing,即不能发生修改,直到不可变绑定离开作用域。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let mut _mutable_integer = 7i32;

{
// Shadowing by immutable `_mutable_integer`
let _mutable_integer = _mutable_integer;

// Error! `_mutable_integer` is frozen in this scope
_mutable_integer = 50;
// FIXME ^ Comment out this line

// `_mutable_integer` goes out of scope
}

// Ok! `_mutable_integer` is not frozen in this scope
_mutable_integer = 3;
}

类型 type

基础数据类型间的转换

Casting

Rust 不提供基础数据类型间的隐式转换(强制类型转换)

但是,可以使用 as 关键字执行显式类型转换(类型转换 casting)。

  • 整型(integer)之间的转换通常遵循 C 语言的约定
  • 只有 u8 类型可以转为 char 类型,其他类型不可以;

任何有符号的值转换为无符号的值T,都有:

原值 +/- (T::MAX +1) 直到结果能填充到新的类型中。

但是对于底层,则是通过而二进制的截取或补充而的到的最终结果。

高位-> 低位,就是截掉高位,仅保留低位的数。

1
2
3
// -1 + 256 = 255
println!(" -1 as a u8 is : {}", (-1i8) as u8);

  • float 转 int,在 Rust 1.45之后,使用 as 关键字转换时,如果浮点数超过了目标类型可以表示的数字范围,那么返回的结果就是最靠近的边界的值

    1
    2
    3
    4
    5
    6
    // 300 超过了u8的最大值255,所以返回值为255;
    println!("300.0 is {}", 300.0f32 as u8);
    // 同理,返回值为 0
    println!("-100.0 as u8 is: {}", -100.0_f32 as u8);
    // 返回值为0,NAN 即 not a number
    println!("nan as u8 is : {}", f32::NAN as u8);

From and Into

实现这两个 trait ,允许我们方便的将 类型A 与 类型B 互转。

实现了 FromInto trait 的类型,会分别实现 frominto 两个关联函数。

我们也可以为自定义类型实现 FromInto 两个 trait,以实现指定类型的互转。

注意,通常,我们只需要为自定义类型 B 实现 From trait ,即 type A -> type B。

当我们需要 type B -> type A 的时候,不需要为type A 实现 Into trait,只需要在使用时显示的声明变量类型即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 为 i32 -> Number 实现了 from
impl From<i32> for Number {...}

let int = 5;
// 这里显示声明 Number类型即可。
let num: Number = int.into();

// 类型参数为 &str
impl From<&str> for Method {
xxx
}
// 类型参数为 String
impl From<String> for HttpRequest {
xxx
}

转换为 String

将一个类型转为 String 需要实现 ToString trait,同时更简单方法是为其实现fmt::Display trait,该 trait会自动实现 ToString trait,并且允许打印

TryFrom 与 From

在实现类型转换时,除了可以实现 From trait 外,还可以实现 TryFrom trait,但是这两个 trait 同时只能实现一个,否则会有冲突。

如何选择?

  • 转换时不会出错,实现 From trait;
  • 转换时可能会出错,实现 TryFrom trait;
    • 注意,TryFrom,实现时需要提供一个错误类型;

指针的解构和解引用

对于指针来说,destructuring 和 dereferencing 是不同的概念。

引用是指针的一种。

dereferencing(解引用)

  • 使用 *;追踪引用所指向的数据。

destructuring(解构/析构)

  • 使用 &refref mut

迭代器

下面三个与迭代器有关的方法的使用提示。

  • x.iter()

  • x.iter_mut()

  • x.into_iter()

x.iter(),返回类型为Iter<'_, T> ,其被迭代元素的返回类型(即,type Item)是 &T

x.iter_mut(),返回类型为IterMut<'_, T>,其被迭代元素的返回类型(即,type Item)是&mut T

  • 容易搞错的部分是看到返回类型中的T,就以为迭代元素的类型也是T,其实不是。

x.into_iter(),返回类型为Iterator<Item = Self::Item>,,其被迭代元素的返回类型(即,type Item)是T

样例:

1
2
3
4
let vec1 = vec![1,2,3];
let vec2 = vec![4,5,6];
println!("2 in vec1: {}", vec1.iter().any(|&x| x == 2));
println!("2 in vec2: {}", vec2.into_iter().any(|x| x == 2));

上面代码表示

同时可以通过for循环的方式来查看迭代器迭代元素的类型,

样例:

1
2
3
4
// IDE中会显示 v的数据类型
for v in vec1.iter() {
println!("{}", v);
}

为了在常见情况下使用,for 结构体默认会使用 .into_iter() 方法将集合转换为迭代器。

当然也可以指定其他方法来转换为迭代器。

泛型中的 trait bounds

在使用泛型作为类型参数时,通常会使用 trait bounds 来约束类型可以实现的功能。

1
2
3
fn printer<T: Display>(t: T) {
println!("{}", t);
}

注意: 即使 trait 不包含任何东西(空 trait),我们依然可以将它作为 trait bounds。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct Cardinal;
struct BlueJay;
struct Turkey;

trait Red {}
trait Blue {}

impl Red for Cardinal {}
impl Blue for BlueJay {}

// These functions are only valid for types which implement these
// traits. The fact that the traits are empty is irrelevant.
fn red<T: Red>(_: &T) -> &'static str { "red" }
fn blue<T: Blue>(_: &T) -> &'static str { "blue" }

fn main() {
let cardinal = Cardinal;
let blue_jay = BlueJay;
let _turkey = Turkey;

// `red()` won't work on a blue jay nor vice versa
// because of the bounds.
println!("A cardinal is {}", red(&cardinal));
println!("A blue jay is {}", blue(&blue_jay));
//println!("A turkey is {}", red(&_turkey));
// ^ TODO: Try uncommenting this line.
}

作用域之部分转移

partial move ,如果一个struct的存在没有实现 Copy trait 的字段的时候,那么当这个类型实例通过解构,将字段赋值给其他变量后,没有实现 Copy trait的字段就会发生所有权的转移,那么该类型的实例就不能再作为一个整体来使用了,仅能使用未发生所有权转移的部分。

参考下面样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
fn main() {
#[derive(Debug)]
struct Person {
name: String,
age: u8,
}

let person = Person {
name: String::from("Alice"),
age: 20,
};

// `name` is moved out of person, but `age` is referenced
let Person { name, ref age } = person;

println!("The person's age is {}", age);

println!("The person's name is {}", name);

// Error! borrow of partially moved value: `person` partial move occurs
//
//println!("The person struct is {:?}", person);

// `person` cannot be used but `person.age` can be used as it is not moved
println!("The person's age from person struct is {}", person.age);
}

解决方法

  1. 一个 struct 为所有可能发生所有权转移的字段类型实现 Copy trait;(成本高)
  2. (推荐)当以引用的方式使用时,例如 &ref
  3. 如果发生在 match 表达式中,可以使用 _ 不绑定值,即不获取所有权;

借用,ref 模式

1
2
3
4
let c = 'Q';
// 在赋值语句中,`ref` 在左侧,与使用`&`在右侧是相等的。
let ref ref_c1 = c;
let ref_c2 = &c;

Copy & Clone

CloneCopy 的supertrait,即要实现Copy trait 一定要先实现Clone trait。

Copy 与 Clone 的不同

Copy 是隐式执行的,

Clone 是显示执行的,显示调用 x.clone();

有两种方式实现Copy trait

方法一,通过#[derive(Clone,Copy)] 的方式派生;

方法二,分别手动实现CloneCopy trait。

注意:为 struct 实现 Copy 时,只有当struct的所有字段都实现了 Copy trait 时,才能为struct 整体实现 Copy ,否则不可以。

更多内容参考标准库内容。

Static

Rust 有几个保留字用于生命周期命名。

'static

有两种情况会遇到'static,

  1. 作为引用的 static 生命周期;
  2. 作为 trait bound 的一部分;
1
2
3
4
// 作为引用的生命周期
let s: &'static str = "hello world";
// 作为trait bound 的一部分
fn generic<T>(x: T) where T: 'static {}

1. 引用的作为生命周期

有两种方式使得一个变量获取'static 的生命周期。它们都存在二进制的只读内存中???

There are two ways to make a variable with 'static lifetime, and both are stored in the read-only memory of the binary:

  • 使用 static 声明一个常量;
  • 创建一个字符串字面值,即 &'static str
1
2
static NUM: i32 = 18;
let static_string = "I'm in read-only memory";

2. 作为 trait bound 的一部分

作为 trait bound 时,意味着类型 T 不能有非静态的引用(即全部都要时静态的static)。

注意:有一点比较重要,任何拥有所有权的数据永远可以传递'static 条件的生命周期,但是引用通常不行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::fmt::Debug;

fn print_it( input: impl Debug + 'static ) {
println!( "'static value passed in is: {:?}", input );
}

fn main() {
// i is owned and contains no references, thus it's 'static:
let i = 5;
print_it(i);

// oops, &i only has the lifetime defined by the scope of
// main(), so it's not 'static:
print_it(&i); // 编译器会报错。
}

编译器会报错:

1
2
3
4
5
6
7
8
9
10
error[E0597]: `i` does not live long enough
--> src/lib.rs:15:15
|
15 | print_it(&i);
| ---------^^--
| | |
| | borrowed value does not live long enough
| argument requires that `i` is borrowed for `'static`
16 | }
| - `i` dropped here while still borrowed

struct 与 trait 中的方法实现

在 Rust 中尽管为 struct 实现的方法与为其实现 trait 的方法是分开的,但是在实际定义和使用时,依然可以把它们看作是一个整体,在相互的作用域内可以互相调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
impl Sheep {
fn is_naked(&self) -> bool {
self.naked
}

fn shear(&mut self) {
// 调用 impl Animal for Sheep 中的 name()
println!("{} is already naked...", self.name());
}
}

// Implement the `Animal` trait for `Sheep`.
impl Animal for Sheep {
fn name(&self) -> &'static str {
self.name
}
fn noise(&self) -> &'static str {
// 调用 impl Sheep 中的 is_naked() 方法
if self.is_naked() {
"baaaaah?"
}
}
}

keyword,modifier

查看官方文档,了解哪些常用的保留字是关键词或修饰符。

问题,根据什么来判断定义一个词是keyword?还是modifier?

关键词

  • ref

修饰符

  • mut

todo! 宏与 unimplemented! 宏的区别

crate,mod

“src\result_rbe.rs” and “src\result_rbe\mod.rs” 是相等的

Derive trait

Rust 提供了几个通过使用#[derive] 属性自动实现的trait。

  • 也可以手动实现。

下面是可派生的 trait:

  • 用于比较的 trait :EqPartialEqOrdPartialOrd
    • 这四个的区别?
    • PartialEq 允许使用 ==!=对类型进行比较
    • Eq 只能用在实现了PartialEq 的类型上;
    • PartialOrd 用于排序比较即,>,<,>=,<=;也只能用在实现了PartialEq 的类型上;
    • Ord,还没看。
  • Clone
  • Copy,实现该 trait,可以通过 copy 所有权代替 move 所有权。(需要同时添加 Clone,因为CloneCopy 的supertrait);
  • Hash,计算一个引用的hash
  • Default,创建一个空的数据类型实例;
  • Debug,使用{:?} 的方式打印值;

trait 对象

因为 rust 编译器要求每个类型都要知道内存大小。所以类型要确定。

Rust 在堆上分配内存,无论何时都会尽可能明确。

这种情况下,单独使用 trait 是不行的,但是可以通过指针间接处理。

可以使用 Box 引用指向堆内存。

Box<dyn Trait>

例如:Box<dyn Animal>

所有权与方法调用

拥有所有权的实例,调用方法时,

  • 不可变实例(immutable):可以调用第一个参数为 selfmut self&self 的方法,
  • 可变实例(mutable):可以调用第一个参数为 self&selfmut self&mut self 形式的方法。

注意:尽管创建方法时第一个参数声明为 **mut** self,但是调用时,可能就变成 self 了,可以查看调用处的函数签名就能发现。所以这也是为什么不可变实例,也能调用 **mut** self 的原因。

Cargo 中的惯例

Cargo 中有一些约定俗成的惯例,遵守这些惯例有利于让 Rust 开发者更容易阅读你的代码。

文件位置上的惯例

1
2
3
4
5
6
7
8
9
10
11
12
13
package
|-- src // 功能源码所在目录
| |-- bin // 其他二进制文件所在目录
| | |-- <name>.rs
| |-- main.rs // 默认的main二进制文件
| |-- lib.rs // 默认的库文件
|-- tests // 集成测试所在目录
|-- <test_name>.rs
|-- benches // 基准测试所在目录
| |-- <benche_name>.rs
|-- examples // 样例所在目录
| |-- <example_name>.rs

导出包的使用技巧

技巧1,巧妙组合放入 prelude 中,在第三方 crate 中很常见的技巧。

1
2
3
4
5
6
7
8
9
// lib.rs 文件中
pub mod prelude {
pub use crate::assert::PathAssert;
pub use crate::fixture::FileTouch;
}


1. 先使用 pub use 将指定的内容重新导出,方便对外访问;
2. 在使用 pub mod prelude,将这些内容部分核心 trait 放到 prelude 中,这样在使用时,可直接通过 `use assert_fs::prelude::*` 的形式全部引入使用的代码中,这样基本在实际使用时大部分情况下这个 crate 中涉及的引用内容都在这里了。
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2023 ligongzhao
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信