From 01e91aa6ef79beee7aaf461f25db63a70a560e7e Mon Sep 17 00:00:00 2001 From: sunface Date: Fri, 7 Jan 2022 10:27:27 +0800 Subject: [PATCH] add int-to-enum --- book/contents/SUMMARY.md | 3 + book/contents/advance/self-referential.md | 233 +++++++++++++++++++--- book/contents/converse/enum-int.md | 198 ++++++++++++++++++ book/contents/converse/intro.md | 2 + book/writing-material/books.md | 7 +- 5 files changed, 413 insertions(+), 30 deletions(-) create mode 100644 book/contents/converse/enum-int.md create mode 100644 book/contents/converse/intro.md diff --git a/book/contents/SUMMARY.md b/book/contents/SUMMARY.md index e9d893cf..af4c3998 100644 --- a/book/contents/SUMMARY.md +++ b/book/contents/SUMMARY.md @@ -112,6 +112,9 @@ - [智能指针引起的重复借用错误](fight-with-compiler/borrowing/borrow-distinct-fields-of-struct.md) - [类型未限制(todo)](fight-with-compiler/unconstrained.md) +- [进阶类型转换](converse/intro.md) + - [枚举和整数](converse/enum-int.md) + - [错误处理 todo](errors/intro.md) - [简化错误处理](errors/simplify.md) - [自定义错误](errors/user-define.md) diff --git a/book/contents/advance/self-referential.md b/book/contents/advance/self-referential.md index 2ef01931..78730b4c 100644 --- a/book/contents/advance/self-referential.md +++ b/book/contents/advance/self-referential.md @@ -35,36 +35,179 @@ fn main(){ 因为我们试图同时使用值和值的引用,最终所有权转移和借用一起发生了。所以,这个问题貌似并没有那么好解决,不信你可以回想下自己具有的知识,是否可以解决? #### 使用ouroboros +对于自引用结构体,三方库也有支持的,其中一个就是`ouroboros`,当然它也有自己的限制,我们后面会提到,先来看看该如何使用: +```rust +use ouroboros::self_referencing; -## 玉树临风的自引用 +#[self_referencing] +struct SelfRef { + value: String, + + #[borrows(value)] + pointer_to_value: &'this str, +} + +fn main(){ + let v = SelfRefBuilder { + value: "aaa".to_string(), + pointer_to_value_builder: |value: &String| value, + }.build(); + + // 借用value值 + let s = v.borrow_value(); + // 借用指针 + let p = v.borrow_pointer_to_value(); + // value值和指针指向的值相等 + assert_eq!(s, *p); +} +``` + +可以看到,`ouroboros`使用起来并不复杂,就是需要你去按照它的方式创建结构体和引用类型:`SelfRef`变成`SelfRefBuilder`,引用字段从`pointer_to_value`变成`pointer_to_value_builder`,并且连类型都变了。 + +在使用时,通过`borrow_value`来借用`value`的值,通过`borrow_pointer_to_value`来借用`pointer_to_value`这个指针。 + +看上去很美好对吧?但是你可以尝试着去修改`String`字符串的值试试,`ouroboros`限制还是较多的,但是对于基本类型依然是支持的不错: ```rust -use std::str; +use ouroboros::self_referencing; -struct MyStruct<'a>{ - Buf: Vec, - repr: Parsed<'a> +#[self_referencing] +struct MyStruct { + int_data: i32, + float_data: f32, + #[borrows(int_data)] + int_reference: &'this i32, + #[borrows(mut float_data)] + float_reference: &'this mut f32, } -struct Parsed<'a>{ - name:&'a str +fn main() { + let mut my_value = MyStructBuilder { + int_data: 42, + float_data: 3.14, + int_reference_builder: |int_data: &i32| int_data, + float_reference_builder: |float_data: &mut f32| float_data, + }.build(); + + // Prints 42 + println!("{:?}", my_value.borrow_int_data()); + // Prints 3.14 + println!("{:?}", my_value.borrow_float_reference()); + // Sets the value of float_data to 84.0 + my_value.with_mut(|fields| { + **fields.float_reference = (**fields.int_reference as f32) * 2.0; + }); + + // We can hold on to this reference... + let int_ref = *my_value.borrow_int_reference(); + println!("{:?}", *int_ref); + // As long as the struct is still alive. + drop(my_value); + // This will cause an error! + // println!("{:?}", *int_ref); } +``` -fn main(){ +总之,使用这个库前,强烈建议看一些官方的例子中支持什么样的类型和API,如果能满足的你的需求,就果断使用它,如果不能满足,就继续往下看。 - let v = vec!(0065,0066,0067,0068,0069); - let s = str::from_utf8(&v).unwrap(); - println!("{}",s); - let p = &v[1..=3]; - let s1 = str::from_utf8(p).unwrap(); - println!("{}",s1); - let par = Parsed{name:s1}; + - let new1 = MyStruct{Buf:v,repr:par}; +#### unsafe实现 +```rust +#[derive(Debug)] +struct SelfRef { + value: String, + pointer_to_value: *const String, +} + +impl SelfRef { + fn new(txt: &str) -> Self { + SelfRef { + value: String::from(txt), + pointer_to_value: std::ptr::null(), + } + } + + fn init(&mut self) { + let self_ref: *const String = &self.value; + self.pointer_to_value = self_ref; + } + + fn value(&self) -> &str { + &self.value + } + + fn pointer_to_value(&self) -> &String { + assert!(!self.pointer_to_value.is_null(), "Test::b called without Test::init being called first"); + unsafe { &*(self.pointer_to_value) } + } +} + +fn main() { + let mut t = SelfRef::new("hello"); + t.init(); + // 打印值和指针地址 + println!("{}, {:p}",t.value(), t.pointer_to_value()); +} +``` + +在这里,我们在`pointer_to_value`中直接存储原生指针,而不是Rust的引用,因此不再受到Rust借用规则和生命周期的限制,而且实现起来非常清晰、简洁。但是缺点就是,通过指针获取值时需要使用`unsafe`代码, + +当然,上面的代码你还能通过原生指针来修改`String`,但是需要将`*const`修改为`*mut`: +```rust +#[derive(Debug)] +struct SelfRef { + value: String, + pointer_to_value: *mut String, +} + +impl SelfRef { + fn new(txt: &str) -> Self { + SelfRef { + value: String::from(txt), + pointer_to_value: std::ptr::null_mut(), + } + } + + fn init(&mut self) { + let self_ref: *mut String = &mut self.value; + self.pointer_to_value = self_ref; + } + + fn value(&self) -> &str { + &self.value + } + + fn pointer_to_value(&self) -> &String { + assert!(!self.pointer_to_value.is_null(), "Test::b called without Test::init being called first"); + unsafe { &*(self.pointer_to_value) } + } +} + +fn main() { + let mut t = SelfRef::new("hello"); + t.init(); + println!("{}, {:p}",t.value(), t.pointer_to_value()); + + t.value.push_str(", world"); + unsafe { + (&mut *t.pointer_to_value).push_str("!"); + } + + println!("{}, {:p}",t.value(), t.pointer_to_value()); } ``` +运行后输出: +```console +hello, 0x16f3aec70 +hello, world!, 0x16f3#aec70 +``` + +上面的`unsafe`虽然简单好用,但是它不太安全,是否还有其他选择?还真的有,那就是`Pin`。 + +#### 无法被移动的Pin +Pin在后续章节会深入讲解,目前你只需要知道它可以固定住一个值,防止该值的所有权被转移。 -## 使用Pin来解决自引用 -Pin在后续章节会深入讲解,目前你只需要知道它可以固定住一个值,防止该值的所有权被转移。通过Pin也可以实现自引用的数据结构: +通过开头我们知道,自引用最麻烦的就是创建引用的同时,值的所有权会被转移,而通过Pin就可以很好的防止这一点: ```rust use std::marker::PhantomPinned; use std::pin::Pin; @@ -80,21 +223,18 @@ struct Unmovable { } impl Unmovable { - // To ensure the data doesn't move when the function returns, - // we place it in the heap where it will stay for the lifetime of the object, - // and the only way to access it would be through a pointer to it. + // 为了确保函数返回时数据的所有权不会被转移, 我们将它放在堆上, 唯一的访问方式就是通过指针 fn new(data: String) -> Pin> { let res = Unmovable { data, - // we only create the pointer once the data is in place - // otherwise it will have already moved before we even started + // 只有在数据到位时,才创建指针,否则数据会在开始之前就被转移所有权 slice: NonNull::dangling(), _pin: PhantomPinned, }; let mut boxed = Box::pin(res); let slice = NonNull::from(&boxed.data); - // we know this is safe because modifying a field doesn't move the whole struct + // 这里其实安全的,因为修改一个字段不会转移整个结构体的所有权 unsafe { let mut_ref: Pin<&mut Self> = Pin::as_mut(&mut boxed); Pin::get_unchecked_mut(mut_ref).slice = slice; @@ -105,18 +245,48 @@ impl Unmovable { fn main() { let unmoved = Unmovable::new("hello".to_string()); - // The pointer should point to the correct location, - // so long as the struct hasn't moved. - // Meanwhile, we are free to move the pointer around. + // 只要结构体没有被转移,那指针就应该指向正确的位置,而且我们可以随意移动指针 let mut still_unmoved = unmoved; assert_eq!(still_unmoved.slice, NonNull::from(&still_unmoved.data)); - // Since our type doesn't implement Unpin, this will fail to compile: + // 因为我们的类型没有实现`Unpin`特征,下面这段代码将无法编译 // let mut new_unmoved = Unmovable::new("world".to_string()); // std::mem::swap(&mut *still_unmoved, &mut *new_unmoved); } ``` +上面的代码也非常清晰,虽然使用了`unsafe`,其实更多的是无奈之举,跟之前的`unsafe`实现完全不可同日而语。 + +总之通过`Pin`来实现,绝对值得优先考虑,代码清晰的同时逼格还挺高。 + +## 玉树临风的自引用 +```rust +use std::str; + +struct MyStruct<'a>{ + Buf: Vec, + repr: Parsed<'a> +} + +struct Parsed<'a>{ + name:&'a str +} + +fn main(){ + + let v = vec!(0065,0066,0067,0068,0069); + let s = str::from_utf8(&v).unwrap(); + println!("{}",s); + let p = &v[1..=3]; + let s1 = str::from_utf8(p).unwrap(); + println!("{}",s1); + let par = Parsed{name:s1}; + + let new1 = MyStruct{Buf:v,repr:par}; +} +``` + + ## 三方库解决引用循环 一些三方库也可以用来解决引用循环的问题,例如: @@ -126,6 +296,11 @@ fn main() { 不过需要注意的是,这些库需要目标值的内存地址不会改变,因此`Vec`动态数组就不适合,因为当内存空间不够时,Rust会重新分配一块空间来存放该数组,这会导致内存地址的改变。 +## 学习一本书:如何实现链表 ## 总结 -本文深入讲解了何为引用循环以及如何使用Weak来解决,同时还结合`Rc`、`RefCell`、`Weak`等实现了两个有实战价值的例子,让大家对智能指针的使用更加融会贯通。 \ No newline at end of file +上面讲了这么多方法,但是我们依然无法正确的告诉你在某个场景应该使用哪个方法,这个需要你自己的判断,因为自引用实在是过于复杂。 + +我们能做的就是告诉你,有这些办法可以解决自引用问题,而这些办法每个都有自己适用的范围,需要你未来去深入的挖掘和发现。 + +偷偷说一句,就算是我,遇到自引用一样挺头疼,好在这种情况真的不常见,往往是实现特定的算法和数据结构时才需要,应用代码中几乎用不到。 \ No newline at end of file diff --git a/book/contents/converse/enum-int.md b/book/contents/converse/enum-int.md new file mode 100644 index 00000000..24cb4056 --- /dev/null +++ b/book/contents/converse/enum-int.md @@ -0,0 +1,198 @@ +# 整数转换为枚举 +在Rust中,从枚举到整数的转换很容易,但是反过来,就没那么容易,甚至部分实现还挺邪恶, 例如使用`transmute`。 + +## C语言的实现 +对于C语言来说,万物皆邪恶,因此我们不讨论安全,只看实现,不得不说很简洁: +```C +#include + +enum atomic_number { + HYDROGEN = 1, + HELIUM = 2, + // ... + IRON = 26, +}; + +int main(void) +{ + enum atomic_number element = 26; + + if (element == IRON) { + printf("Beware of Rust!\n"); + } + + return 0; +} +``` + +但是在Rust中,以下代码: +```rust +enum MyEnum { + A = 1, + B, + C, +} + +fn main() { + // 将枚举转换成整数,顺利通过 + let x = MyEnum::C as i32; + + // 将整数转换为枚举,失败 + match x { + MyEnum::A => {} + MyEnum::B => {} + MyEnum::C => {} + _ => {} + } +} +``` + +就会报错: `MyEnum::A => {} mismatched types, expected i32, found enum MyEnum`。 + +## 一个真实场景的需求 +在实际场景中,从枚举到整数的转换有时还是非常需要的,例如你有一个枚举类型,然后需要从外面穿入一个整数,用于控制后续的流程走向,此时就需要用整数去匹配相应的枚举(你也可以用整数匹配整数-, -,看看会不会被喷)。 + +既然有了需求,剩下的就是看看该如何实现,这篇文章的水远比你想象的要深,且看八仙过海各显神通。 + +## 使用三方库 +首先可以想到的肯定是三方库,毕竟Rust的生态目前已经发展的很不错,类似的需求总是有的,这里我们先使用`num-traits`和`num-derive`来试试。 + +在`Cargo.toml`中引入: +```toml +[dependencies] +num-traits = "0.2.14" +num-derive = "0.3.3" +``` + +代码如下: +```rust +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; + +#[derive(FromPrimitive)] +enum MyEnum { + A = 1, + B, + C, +} + +fn main() { + let x = 2; + + match FromPrimitive::from_i32(x) { + Some(MyEnum::A) => println!("Got A"), + Some(MyEnum::B) => println!("Got B"), + Some(MyEnum::C) => println!("Got C"), + None => println!("Couldn't convert {}", x), + } +} +``` + +除了上面的库,还可以使用一个较新的库: [`num_enums`](https://github.com/illicitonion/num_enum)。 + +## TryFrom + 宏 +在Rust1.34后,可以实现`TryFrom`特征来做转换: +```rust +use std::convert::TryFrom; + +impl TryFrom for MyEnum { + type Error = (); + + fn try_from(v: i32) -> Result { + match v { + x if x == MyEnum::A as i32 => Ok(MyEnum::A), + x if x == MyEnum::B as i32 => Ok(MyEnum::B), + x if x == MyEnum::C as i32 => Ok(MyEnum::C), + _ => Err(()), + } + } +} +``` + +以上代码定义了从`i32`到`MyEnum`的转换,接着就可以使用`TryInto`来实现转换: +```rust +use std::convert::TryInto; + +fn main() { + let x = MyEnum::C as i32; + + match x.try_into() { + Ok(MyEnum::A) => println!("a"), + Ok(MyEnum::B) => println!("b"), + Ok(MyEnum::C) => println!("c"), + Err(_) => eprintln!("unknown number"), + } +} +``` + +但是上面的代码有个问题,你需要为每个枚举成员都实现一个转换分支,非常麻烦。好在可以使用宏来简化,自动根据枚举的定义来实现`TryFrom`特征: +```rust +macro_rules! back_to_enum { + ($(#[$meta:meta])* $vis:vis enum $name:ident { + $($(#[$vmeta:meta])* $vname:ident $(= $val:expr)?,)* + }) => { + $(#[$meta])* + $vis enum $name { + $($(#[$vmeta])* $vname $(= $val)?,)* + } + + impl std::convert::TryFrom for $name { + type Error = (); + + fn try_from(v: i32) -> Result { + match v { + $(x if x == $name::$vname as i32 => Ok($name::$vname),)* + _ => Err(()), + } + } + } + } +} + +back_to_enum! { + enum MyEnum { + A = 1, + B, + C, + } +} +``` + + + + + +## 邪恶之王std::mem::transmute +**这个方法原则上并不推荐,但是有其存在的意义,如果要使用,你需要清晰的知道自己为什么使用**。 + +在之前的类型转换章节,我们提到过非常邪恶的[`transmute`转换](../basic/converse.md#变形记(Transmutes)),其实,当你知道数值一定不会超过枚举的范围时(例如枚举成员对应1,2,3,传入的整数也在这个范围内),就可以使用这个方法完成变形。 + +> 最好使用#[repr(..)]来控制底层类型的大小,免得本来需要i32,结果传入i64,最终内存无法对齐,产生奇怪的结果 + +```rust +#[repr(i32)] +enum MyEnum { + A = 1, B, C +} + +fn main() { + let x = MyEnum::C; + let y = x as i32; + let z: MyEnum = unsafe { ::std::mem::transmute(y) }; + + // match the enum that came from an int + match z { + MyEnum::A => { println!("Found A"); } + MyEnum::B => { println!("Found B"); } + MyEnum::C => { println!("Found C"); } + } +} +``` + +既然是邪恶之王,当然得有真本事,无需标准库、也无需unstable的Rust版本,我们就完成了转换!awesome!?? + + +## 总结 +本文列举了常用(其实差不多也是全部了,还有一个unstable特性没提到)的从整数转换为枚举的方式,推荐度按照出现的先后顺序递减。 + +但是推荐度最低,不代表它就没有出场的机会,只要使用边界清晰,一样可以大方光彩,例如最后的`transmute`函数. \ No newline at end of file diff --git a/book/contents/converse/intro.md b/book/contents/converse/intro.md new file mode 100644 index 00000000..cf5fa148 --- /dev/null +++ b/book/contents/converse/intro.md @@ -0,0 +1,2 @@ +# 进阶类型转换 +Rust是强类型语言,同时也是强安全语言,因此这些决定了Rust的类型转换注定比一般语言要更困难,再加上Rust的繁多的类型和类型转换特征,用于很难对这块内容了若指掌,因此我们专门整了一个专题来讨论Rust中那些不太容易的类型转换, 容易的请看[这一章](../basic/converse.md). \ No newline at end of file diff --git a/book/writing-material/books.md b/book/writing-material/books.md index 677bf519..451d8d84 100644 --- a/book/writing-material/books.md +++ b/book/writing-material/books.md @@ -25,4 +25,9 @@ 13. [Learning Rust](https://learning-rust.github.io/docs/a1.why_rust.html) -14. [Rust doc](https://doc.rust-lang.org/rustdoc/the-doc-attribute.html) \ No newline at end of file +14. [Rust doc](https://doc.rust-lang.org/rustdoc/the-doc-attribute.html) + +15. [Unstable Rust](https://doc.rust-lang.org/stable/unstable-book/) + +## 可参考的教程 +1. https://github.com/ferrous-systems/teaching-material#core-topics \ No newline at end of file