Rust程序设计第二版-把书读薄之难点使用(一)

77 次浏览
2024年07月24日创建

Rust的问题还是很多,主要是一些关键的使用方法太多了,需要知道什么时候用,为什么用是非常关键的,下面罗列的是基本上贯穿《Rust程序设计 - 第二版》的所有难点理解内容。为了将这些内容融会贯通,需要非常多的例子总结才能理解。下面内容就开始对其中的内容一个一个来梳理。

主要概念

BoxAsRef(as_ref)、AsMut(as_mut)、borrow、borrow_mut、from、into、try_from、try_into、toOwned

函数入参数与返回值约束

动态大小类型(DST,Dynamically Sized Types)不能直接作为函数参数,也不能作为函数的返回值。所以需要有手段来解决这个问题

  • 如果一个结构体的所有字段都是固定大小的类型,那么这个结构体本身也是固定大小的,可以直接作为函数的返回值和参数。
  • 引用或者Rc包装
  • Box类型

Box

当一个Vec或者某个对象需要在栈中分配,但无法知道其大小,编译器其实很难搞,所以需要用Box将对象分配到堆上面。几种场景,第一个是作为结构体的成员,第二个是作为入参数,第三个是作为返回值。

AsRef与AsMut

它们分别用于将一个类型转换为引用和可变引用。

AsRef

AsRef 特性允许将一个类型转换为另一个类型的引用。具体来说,AsRef<Path> 表示可以将一个类型转换为 Path 的引用。标准库中已经为许多常见类型实现了这些特性,其中包括 &str 转换为 &Path,是因为&str 实现了 AsRef<Path>。AsRef 特性允许你将一个类型转换为另一个类型的引用的特点,这在编写泛型代码时特别有用,因为它使得你的代码能够接受更广泛的输入类型。

let path = "example.txt";
let content = as_ref_test(path);
print!("{}", content.unwrap());

pub fn as_ref_test<P:AsRef<Path>>(s:P) -> Result<String>{
    Ok("good".to_string())
}

以及其他例子:

fn takes_asref_u8_slice<S: AsRef<[u8]>>(s: S) {
    let slice: &[u8] = s.as_ref();
    println!("Slice: {:?}", slice);
}

fn main() {
    let vec = vec![1, 2, 3];
    let array = [1, 2, 3];
    let string = String::from("hello");
    let str_slice = "hello";

    takes_asref_u8_slice(vec);
    takes_asref_u8_slice(&array);
    takes_asref_u8_slice(string);
    takes_asref_u8_slice(str_slice);
}

只要一个类型实现了 AsRef 特性,你就可以直接调用 as_ref 方法:

use std::path::Path;

fn main() {
    let s = "example.txt";
    let path: &Path = s.as_ref();  // 这里进行隐式转换

    println!("Path: {:?}", path);
}

以及其他类型:

fn main() {
    let vec = vec![1, 2, 3];
    let slice: &[u8] = vec.as_ref();  // 这里进行隐式转换

    println!("Slice: {:?}", slice);
}

可以将 as_ref 看作是一种简化类型转换的工具,使代码更加灵活和通用。 也可以自己实现as_ref

struct MyStructS {
    value: i32,
}

impl AsRef<i32> for MyStructS {
    fn as_ref(&self) -> &i32 {
        &self.value
    }
}

let ms = MyStructS{value:32};
let val_ms = ms.as_ref();
println!("{}", val_ms)

AsMut

AsMut 特性与 AsRef 类似,但它允许将一个类型转换为其可变引用类型。AsMut 特性提供了 as_mut 方法,用于获取可变引用。

fn main() {
    let mut vec = vec![1, 2, 3];
    let slice: &mut [u8] = vec.as_mut();  // 这里进行隐式转换

    slice[0] = 10;

    println!("Modified vec: {:?}", vec);
}

以及

fn main() {
    let mut boxed = Box::new(42);
    let reference: &mut i32 = boxed.as_mut();  // 这里进行隐式转换

    *reference += 1;

    println!("Modified value: {}", boxed);
}

我们可以直接调用 boxed.as_mut() 将其转换为 &mut i32,然后通过这个可变引用修改原始值。

也可以自己实现:

struct MyStruct {
    value: i32,
}

impl AsMut<i32> for MyStruct {
    fn as_mut(&mut self) -> &mut i32 {
        &mut self.value
    }
}

fn main() {
    let mut my_struct = MyStruct { value: 42 };
    let value_ref: &mut i32 = my_struct.as_mut();  // 这里进行隐式转换

    *value_ref += 1;

    println!("Modified value: {}", my_struct.value);
}

&str的as_ref

因为 s 本身就是一个 &str 类型,所以 s.as_ref() 返回的也是一个 &str 类型的引用,并不是引用的引用。

fn main() {
    let s: &str = "hello";
    let s_ref: &str = s.as_ref(); // 这里 s_ref 也是 &str
    println!("s_ref: {}", s_ref);
}

AsRef和AsMut与dyn实现多态入参数之间的区别

AsRef 是一个特性(trait),它允许你将一个类型转换为某个引用类型。使用 AsRef 可以使函数接受多种输入类型,只要这些类型实现了 AsRef 特性。这种方式通常用于泛型编程,提供了编译时的多态性。

use std::convert::AsRef;

fn print_as_str<T: AsRef<str>>(input: T) {
    let s: &str = input.as_ref();
    println!("{}", s);
}

fn main() {
    let s = String::from("Hello, world!");
    let s_ref: &str = "Hello, world!";

    print_as_str(s);       // String 实现了 AsRef<str>
    print_as_str(s_ref);   // &str 实现了 AsRef<str>
}

dyn 用于定义动态分发的特性对象。它允许你在运行时决定调用哪个实现,而不是在编译时。这种方式提供了运行时的多态性,但通常会带来一些性能开销,因为它需要通过虚表(vtable)进行动态分发。

trait Printable {
    fn print(&self);
}

impl Printable for String {
    fn print(&self) {
        println!("{}", self);
    }
}

impl Printable for &str {
    fn print(&self) {
        println!("{}", self);
    }
}

fn print_dyn(input: &dyn Printable) {
    input.print();
}

fn main() {
    let s = String::from("Hello, world!");
    let s_ref: &str = "Hello, world!";

    print_dyn(&s);       // String 实现了 Printable
    print_dyn(&s_ref);   // &str 实现了 Printable
}

在这个示例中,print_dyn 函数接受一个特性对象 &dyn Printable,它可以在运行时调用适当的 print 方法。String 和 &str 都实现了 Printable 特性,因此可以传递给这个函数。

选择使用 AsRef 还是 dyn 取决于你的具体需求和场景。如果你需要编译时的多态性和更高的性能,可以考虑使用 AsRef。如果你需要运行时的灵活性,可以考虑使用 dyn。

Borrow与Borrow_mut

std::borrow::Borrow 和 std::borrow::BorrowMut 是两个不同的特性(traits),它们分别用于不可变借用和可变借用。

Borrow

如果一个类型实现了 Borrow<T>,那么它的 borrow 方法就能高效地从自身借入一个 &T。但是 Borrow 施加了更多限制:只有当 &T 能通过与它借来的 值相同的方式进行哈希和比较时,此类型才应实现 Borrow<T>。 (Rust 并不强制执行此限制,它只是记述了此特型的意图。)这使得 Borrow 在处理哈希表和树中的键或者处理因为某些原因要进行哈希 或比较的值时非常有用。

Borrow提供了一种将类型转换为某个引用类型的机制。这个特性主要用于泛型编程,允许你编写能够接受多种类型的代码,只要这些类型实现了 Borrow 特性。

use std::collections::HashMap;
use std::borrow::Borrow;

fn main() {
    let mut map: HashMap<String, i32> = HashMap::new();
    map.insert("one".to_string(), 1);
    map.insert("two".to_string(), 2);

    // 使用 &str 查找 HashMap<String, i32>
    let key: &str = "one";
    if let Some(value) = map.get(key.borrow()) {
        println!("The value for '{}' is {}", key, value);
    } else {
        println!("Key '{}' not found", key);
    }
}

Borrow与mut之间的区别

例如:这里的代码中:y和z有什么区别?

let key: &str = "one";
let mut y = &key;
let z = key.borrow(); 

y 是一个对 key 的引用的引用,因此可以重新绑定以指向不同的字符串切片引用。而 z 是一个直接的字符串切片引用,因此它与 key 本身相同。

use std::borrow::Borrow;

fn main() {
    let key: &str = "one";
    let mut y = &key;
    let z = key.borrow();

    println!("key: {}", key); // 输出: key: one
    println!("y: {}", y);     // 输出: y: one
    println!("z: {}", z);     // 输出: z: one

    // 修改 y 以指向一个新的 &str 引用
    let new_key: &str = "two";
    y = &new_key;

    println!("y after modification: {}", y); // 输出: y after modification: two
}

你会发现下面的代码报错:

let key: &str = "one";
let z = key.borrow();
*z = "sdf".to_string();

为了解决上面的报错,接下来介绍下Borrow_mut。

Borrow_mut

实现了 BorrowMut 特性的类型可以通过 borrow_mut 方法获取一个对某个类型的可变引用。这个特性允许你在需要修改数据时获取可变引用。

let mut key: String = "one".to_string();
let z: &mut String = key.borrow_mut();
*z = "sdf".to_string();

使用 String 类型而不是 &str,因为 String 是可变的。使用 borrow_mut 方法获取可变引用,然后通过该引用修改数据。

在 Rust 中,&str 是一个不可变的引用,表示对字符串切片的不可变借用。因此,不能对 &str 进行可变借用(即不能调用 borrow_mut 方法)。

From和Into

表示 类型转换,这种转换会接受一种类型的值并返回另一种类型的值。AsRef 特型和 AsMut 特型用于从一种类型借入另一种类型的引用, 而 From 和 Into 会获取其参数的所有权,对其进行转换,然后将 转换结果的所有权返回给调用者。

From

From 特性用于定义一种类型如何从另一种类型转换过来。通常,你会为目标类型实现 From 特性。

use std::convert::From;

struct MyStruct {
    value: i32,
}

impl From<i32> for MyStruct {
    fn from(item: i32) -> Self {
        MyStruct { value: item }
    }
}

fn main() {
    let num = 42;
    let my_struct = MyStruct::from(num);
    println!("MyStruct value: {}", my_struct.value);
}

在这个示例中,我们没有显式地为 MyStruct 实现 Into<i32>,但由于我们已经实现了 From<i32>,所以我们可以使用 num.into() 将 i32 类型的值转换为 MyStruct。

Into

Into 特性是 From 特性的对偶。如果你为一个类型实现了 From 特性,那么 Into 特性会自动为你实现。

use std::convert::Into;

struct MyStruct {
    value: i32,
}

impl From<i32> for MyStruct {
    fn from(item: i32) -> Self {
        MyStruct { value: item }
    }
}

fn main() {
    let num = 42;
    let my_struct: MyStruct = num.into();
    println!("MyStruct value: {}", my_struct.value);
}

如果你为类型 T 实现了 From<U>,那么你可以自动将 T 转换为 U,因为 Into<U> 会自动为 T 实现。

  • From:当你希望提供一种明确的从一种类型到另一种类型的转换方式时,可以实现 From 特性。
  • Into:当你希望利用已有的 From 实现进行类型转换时,可以使用 Into 特性。通常在泛型代码中使用 Into 更为方便,因为它可以自动推导类型转换。

try_from、try_into

由于转换的行为方式不够清晰,因此 Rust 没有为 i32 实现 From<i64>,也没有实现任何其他可能丢失信息的数值类型之间的转 换,而是为 i32 实现了 TryFrom<i64>。TryFrom 和 TryInto 是 From 和 Into 的容错版“表亲”,这种转换同样是双向的,实 现了 TryFrom 也就意味着实现了 TryInto。

try_from

TryFrom 特性用于定义一种类型如何尝试从另一种类型转换过来。与 From 不同,TryFrom 的转换可能会失败,因此返回一个 Result 类型。

use std::convert::TryFrom;

struct EvenNumber(i32);

impl TryFrom<i32> for EvenNumber {
    type Error = &'static str;

    fn try_from(value: i32) -> Result<Self, Self::Error> {
        if value % 2 == 0 {
            Ok(EvenNumber(value))
        } else {
            Err("Value is not an even number")
        }
    }
}

fn main() {
    let even = EvenNumber::try_from(8);
    let odd = EvenNumber::try_from(3);

    match even {
        Ok(num) => println!("Even number: {}", num.0),
        Err(e) => println!("Error: {}", e),
    }

    match odd {
        Ok(num) => println!("Even number: {}", num.0),
        Err(e) => println!("Error: {}", e),
    }
}

try_into

TryInto 特性是 TryFrom 特性的对偶。如果你为一个类型实现了 TryFrom 特性,那么 TryInto 特性会自动为你实现。

// 溢出时饱和,而非回绕
let smaller: i32 = huge.try_into().unwrap_or(i32::MAX);

try_into() 方法给了我们一个 Result,因此我们可以选择在异 常情况下该怎么做。

use std::convert::TryInto;

struct EvenNumber(i32);

impl TryFrom<i32> for EvenNumber {
    type Error = &'static str;

    fn try_from(value: i32) -> Result<Self, Self::Error> {
        if value % 2 == 0 {
            Ok(EvenNumber(value))
        } else {
            Err("Value is not an even number")
        }
    }
}

fn main() {
    let even: Result<EvenNumber, _> = 8i32.try_into();
    let odd: Result<EvenNumber, _> = 3i32.try_into();

    match even {
        Ok(num) => println!("Even number: {}", num.0),
        Err(e) => println!("Error: {}", e),
    }

    match odd {
        Ok(num) => println!("Even number: {}", num.0),
        Err(e) => println!("Error: {}", e),
    }
}
  • TryFrom:当你希望提供一种可能失败的从一种类型到另一种类型的转换方式时,可以实现 TryFrom 特性。
  • TryInto:当你希望利用已有的 TryFrom 实现进行类型转换时,可以使用 TryInto 特性。通常在泛型代码中使用 TryInto 更为方便,因为它可以自动推导类型转换并处理可能的错误。

toOwned

在 Rust 中,函数不能直接返回 str 和 [u] 这样的类型,因为它们是动态大小类型(DSTs)。动态大小类型在编译时无法确定其大小,因此无法直接在栈上分配空间。为了更好地理解这个问题,我们需要深入了解 Rust 的类型系统和内存管理模型。

ToOwned 特性用于将借用类型转换为拥有类型。它通常与 Borrow 特性一起使用,以便在需要时从借用转换为拥有。

use std::borrow::ToOwned;

impl ToOwned for str {
    type Owned = String;

    fn to_owned(&self) -> String {
        String::from(self)
    }
}

impl<T: Clone> ToOwned for [T] {
    type Owned = Vec<T>;

    fn to_owned(&self) -> Vec<T> {
        self.to_vec()
    }
}

在这个示例中,str 实现了 ToOwned,其拥有类型是 String。这意味着你可以将一个 &str 转换为一个 String。同样,[T] 实现了 ToOwned,其拥有类型是 Vec<T>,前提是 T 实现了 Clone。

use std::path::{Path, PathBuf};

fn main() {
    // 创建一个 PathBuf
    let path_buf = PathBuf::from("/some/path");

    // 从 PathBuf 借用一个 Path
    let path: &Path = path_buf.borrow();
    println!("Borrowed Path: {:?}", path);

    // 将 Path 转换为 PathBuf
    let owned_path_buf: PathBuf = path.to_owned();
    println!("Owned PathBuf: {:?}", owned_path_buf);
}

Vec<T>与[T]、str与String、Path与PathBuf。

  • Vec<T> 是拥有类型,而 [T] 是借用类型。Vec<T> 实现了 Borrow<[T]>,这意味着你可以从 Vec<T> 借用一个 &[T]。同时,只要 T 实现了 Clone,[T] 就能实现 ToOwned<Owned=Vec<T>>,这意味着你可以将一个 &[T] 转换为一个 Vec<T>。
  • String 和 str:String 是拥有类型,而 str 是借用类型。String 实现了 Borrow<str>,这意味着你可以从 String 借用一个 &str。同时,str 实现了 ToOwned<Owned=String>,这意味着你可以将一个 &str 转换为一个 String。
  • Path 和 PathBuf:类似于 str 和 String,Path 是借用类型,而 PathBuf 是拥有类型。PathBuf 实现了 Borrow<Path>,而 Path 实现了 ToOwned<Owned=PathBuf>。

拥有转借用,借用转拥有

  1. Borrow 特性:用于从拥有类型借用到借用类型。
  2. ToOwned 特性:用于将借用类型转换为拥有类型。

实现关系:拥有类型必须实现 Borrow<Self>,这样可以从拥有类型借用到自身的借用类型。同时,借用类型可以实现 ToOwned,以便在需要时转换为拥有类型。

Cow

要想用好 Rust,就必然涉及对所有权问题的透彻思考,比如函数应该 通过引用还是值接受参数。通常你可以任选一种方式,让参数的类型 反映你的决定。但在某些情况下,在程序开始运行之前你无法决定是 该借用还是该拥有,std::borrow::Cow 类型(用于“写入时克 隆”,clone on write 的缩写)提供了一种兼顾两者的方式。

enum Cow<'a, B: ?Sized>
    where B: ToOwned
{
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}

怎么理解:

泛型参数和生命周期:

  • 'a:生命周期参数,表示借用数据的生命周期。
  • B: ?Sized:泛型参数 B,表示可以是一个不定大小的类型(?Sized 表示类型可以是动态大小的,比如 [T] 或 str)。
  • where B: ToOwned:约束条件,要求 B 实现 ToOwned 特性。

枚举变体:

  • Borrowed(&'a B):表示借用的数据,类型是 &'a B,即一个生命周期为 'a 的引用。
  • Owned(<B as ToOwned>::Owned):表示拥有的数据,类型是 B 的 ToOwned 特性的关联类型 Owned。
use std::borrow::Cow;

fn main() {
    let borrowed: Cow<str> = Cow::Borrowed("Hello, world!");
    let owned: Cow<str> = Cow::Owned(String::from("Hello, world!"));

    match borrowed {
        Cow::Borrowed(s) => println!("Borrowed: {}", s),
        Cow::Owned(s) => println!("Owned: {}", s),
    }

    match owned {
        Cow::Borrowed(s) => println!("Borrowed: {}", s),
        Cow::Owned(s) => println!("Owned: {}", s),
    }
}

怎么使用?

struct Config<'a> {
    data: Cow<'a, str>,
}

impl<'a> Config<'a> {
    fn new(data: Cow<'a, str>) -> Self {
        Config { data }
    }

    fn process(&self) {
        // 处理配置数据
        println!("Processing config: {}", self.data);
    }
}

接下来,展示如何使用 Config 结构体处理借用的和拥有的配置数据:

fn main() {
    // 借用字符串切片作为配置数据
    let config_data1 = "max_connections=100";
    let config1 = Config::new(Cow::Borrowed(config_data1));
    config1.process();

    // 拥有字符串数据作为配置数据
    let config_data2 = String::from("max_connections=200");
    let config2 = Config::new(Cow::Owned(config_data2));
    config2.process();
}