Rust 进阶

枚举(enum)和 match 控制流构造

枚举(enum)和 match 控制流构造是 Rust 语言中非常强大的特性,它们一起提供了一种安全、表达性强的方式来处理多种可能的值。

枚举(enum

枚举允许你定义一个类型,这个类型可以有固定数量的变体。每个变体可以有不同类型和数量的数据与之关联。这使得枚举非常适合于表示多种不同类型的值集合。

基本枚举

一个简单的例子是定义一个表示 IP 地址的枚举。IP 地址可以是 V4 格式的,也可以是 V6 格式的。

enum IpAddrKind {
    V4,
    V6,
}
带有数据的枚举

枚举的变体可以关联数据。例如,我们可以改进上面的例子,使得每个变体都可以持有实际的 IP 地址数据。

enum IpAddr {
    V4(String),
    V6(String),
}

甚至可以为每个变体关联不同类型的数据:

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}
枚举与结构体

枚举变体可以包含结构体:

struct Ipv4Addr {
    // ...
}

struct Ipv6Addr {
    // ...
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}

这展示了 Rust 类型系统的灵活性和强大能力。

match 控制流构造

match 控制流允许你根据枚举的变体来执行不同的代码路径。它是一种基于模式匹配的分支机制。

基本使用

使用 match 需要提供一个要匹配的值,然后列出所有可能的模式以及匹配到每个模式时要执行的代码。

let some_value = Some(3);

match some_value {
    Some(3) => println!("three"),
    _ => (),
}
解构枚举

match 可以解构枚举变体中的值:

match ip_addr {
    IpAddr::V4(a, b, c, d) => println!("{}.{}.{}.{}", a, b, c, d),
    IpAddr::V6(s) => println!("{}", s),
}
模式中的绑定

可以在模式中绑定变量,以在代码块中使用匹配到的值:

match some_value {
    Some(x) => println!("Matched: {}", x),
    None => (),
}

综合应用

枚举和 match 结合使用可以安全、清晰地处理多种可能的情况,尤其是在错误处理、状态机、解析任务等领域。

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn process_message(msg: Message) {
    match msg {
        Message::Quit => println!("Quit"),
        Message::Move { x, y } => println!("Move to ({}, {})", x, y),
        Message::Write(text) => println!("Text message: {}", text),
        Message::ChangeColor(r, g, b) => println!("Change color to RGB({}, {}, {})", r, g, b),
    }
}

这个例子演示了如何定义一个具有多种变体的枚举,每个变体可以关联不同类型的数据,以及如何使用 match 来根据枚举的不同变体执行不同的操作。

Rust 中的枚举和 match 表达式共同提供了一种强大的模式匹配功能,这是 Rust 提供安全和表达性的关键特性之一。

使用模式匹配来解构枚举、元组和结构体

模式匹配是 Rust 中一个非常强大的特性,它不仅允许你根据值的不同形式执行不同的代码,还可以用来解构(即分解)枚举、元组和结构体,以便你可以直接访问它们的内部数据。通过 match 表达式和其他一些结构(如 if let)来使用模式匹配进行解构是非常常见的。

解构枚举

当枚举的变体包含数据时,你可以通过模式匹配来解构并直接访问这些数据。

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
}

let msg = Message::Move { x: 30, y: 50 };

match msg {
    Message::Quit => {
        println!("Quit variant has no data to destructure.");
    },
    Message::Move { x, y } => {
        println!("Move in the x direction {} and in the y direction {}", x, y);
    },
    Message::Write(text) => {
        println!("Text message: {}", text);
    },
}

在这个例子中,Message::Move 的匹配分支展示了如何解构包含在枚举变体中的命名字段。这样你就可以直接使用这些字段的值了。

解构元组

元组是另一种可以通过模式匹配进行解构的复合数据类型。

let tuple = (3, "birds", 4.5);

match tuple {
    (x, _, z) => {
        println!("The first value is: {}, and the third value is: {}", x, z);
    },
}

这里,通过匹配 tuple,我们忽略了中间的值(使用 _),并直接获得了第一和第三个值。

解构结构体

结构体也可以通过模式匹配来解构,这让你能够直接访问其字段。

struct Point {
    x: i32,
    y: i32,
}

let point = Point { x: 0, y: 7 };

match point {
    Point { x, y } => {
        println!("The point is at ({}, {})", x, y);
    },
}

在这个例子中,通过 match 表达式解构 Point 结构体,我们可以直接访问它的 xy 字段。

使用 if let

对于只关心一种模式的情况,if let 语法提供了一种更简洁的方式来处理值和解构。它允许你同时测试某个值是否匹配某个模式,并在匹配的情况下解构它。

let some_option_value = Some(3);

if let Some(x) = some_option_value {
    println!("The value is: {}", x);
}

这里,if let 用于解构 Option<i32> 类型的值,如果值是 Some,则将内部的 i32 值绑定到变量 x 并使用它。

模式匹配和解构是 Rust 提供的强大工具,它们增加了代码的清晰性和安全性,尤其在处理复杂的数据结构时。这些特性使得 Rust 在编译时就能检查错误,减少运行时的错误。

模块、包和路径:Rust 的模块系统,理解如何组织代码和模块

Rust 的模块系统提供了一种强大的方式来组织和封装代码。它通过模块(modules)、包(crates)、路径(paths)以及使用声明(use statements)来管理作用域和隐私,使得大型项目的代码更易于管理、理解和重用。

包和Crate

  • Crate:一个 Rust 的 crate 是一个二进制项或库。所有的 Rust 程序都是以 crate 的形式开始编译的。Crate 根(crate root)是源文件,Rust 编译器以它为起点,并构成 crate 的根模块。
  • 包(Package):一个包包含提供一系列功能的一个或多个 crate。一个包有一个 Cargo.toml 文件,其中描述了如何构建这些 crate。

包的目标是允许你构建、测试和共享 crate。一个包可以包含多个二进制 crate 和可选的一个库 crate。

模块和路径

  • 模块(Module):模块是 Rust 中代码组织和封装的基本单位。模块用来控制作用域和路径的私有性。

  • 路径(Path):路径用于引用模块中的项(比如函数、结构体、模块等)。Rust 中的路径可以是相对的也可以是绝对的。

使用模块组织代码

你可以通过定义模块来组织相关功能的代码,这样做有助于提高代码的可读性和重用性。模块也可以嵌套。

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

在这个例子中,front_of_house 是一个模块,它又包含了另一个模块 hostinghosting 模块包含了一个函数 add_to_waitlist

访问模块中的项

默认情况下,模块中的项(如函数、方法、结构体、枚举等)是私有的,只能在其父模块中访问。你可以使用 pub 关键字使项变为公开的,从而允许外部访问。

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

在这个例子中,通过将 hosting 模块和 add_to_waitlist 函数标记为 pub,我们允许外部代码访问它们。

使用路径来引用项

当需要使用模块中的项时,你可以通过路径来引用它。Rust 中的路径可以是相对的,也可以是绝对的。

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // 绝对路径
    crate::front_of_house::hosting::add_to_waitlist();
    
    // 相对路径
    front_of_house::hosting::add_to_waitlist();
}

使用 use 关键字引入路径

为了方便,Rust 允许你使用 use 关键字在作用域中引入路径,这样就可以直接使用路径的最后一部分,而不需要写出完整的路径。

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}

Rust 的模块系统通过以上机制提供了一种强大的方式来组织你的代码,使其更加模块化和可重用,同时也让你能够精细控制哪些部分是公开的,哪些部分是私有的,以及如何在不同的模块和包之间共享代码。

泛型编程:Rust 中的泛型概念及其语法

泛型编程是 Rust 语言中的一项核心功能,允许你编写灵活且可重用的代码。通过泛型,你可以编写在多种数据类型上工作的函数和数据结构,而不必为每种类型重写代码。

泛型的基本概念

泛型让你在定义函数、结构体、枚举或方法时使用类型参数。在实际使用时,这些类型参数会被具体的类型替换,这样同一段代码就可以安全地工作在不同的类型上。

泛型函数

一个简单的泛型函数示例是,交换两个值的位置:

fn swap<T>(x: &mut T, y: &mut T) {
    let temp = std::mem::replace(x, std::mem::replace(y, std::mem::take(x)));
}

let mut a = 5;
let mut b = 10;
swap(&mut a, &mut b);
println!("a = {}, b = {}", a, b);

这里,<T> 表示 swap 函数是泛型的,它可以接受任何类型的 xy,只要它们是相同的类型。

泛型结构体

泛型也可以用在结构体的定义中,使得结构体能够持有任何类型的数据:

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

let integer_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.0, y: 4.0 };

这个例子中的 Point<T> 结构体可以用来存储任何类型的 xy,比如整数或浮点数。

泛型枚举

Rust 的标准库中的 Option<T>Result<T, E> 枚举就是泛型的好例子:

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

enum Result<T, E> {
    Ok(T),
    Err(E),
}

这里,Option<T> 可以包含任何类型的值或者没有值,Result<T, E> 可以包含任何类型的成功值或者错误类型的错误值。

泛型和trait约束

你可以使用 trait 约束来限制泛型类型必须实现某些行为。这是通过 where 从句实现的:

use std::fmt::Display;

fn print<T: Display>(item: T) {
    println!("{}", item);
}

// 或使用 where 从句
fn print<T>(item: T)
where
    T: Display,
{
    println!("{}", item);
}

在这个例子中,T: Display 表示类型 T 必须实现了 Display trait,这使得 print 函数可以打印任何实现了 Display 的类型。

泛型代码的性能

Rust 泛型是在编译时进行类型具体化的。这意味着编译器会为泛型函数使用的每种具体类型生成专门的代码,所以使用泛型并不会导致运行时性能损失。

通过使用泛型,Rust 使得代码更加灵活和可重用,同时保持了类型安全和高性能。这是 Rust 语言设计中的一个重要特性,对于构建高效且可维护的软件系统至关重要。

特质(Traits):理解特质如何定义共享的行为

特质(Traits)是 Rust 中一种用于定义和共享接口的功能。通过特质,你可以定义一组方法,这组方法可以被不同的类型实现,从而允许不同类型共享相同的行为。这是实现多态的一种方式,也是 Rust 用来实现抽象和封装的机制之一。

定义特质

你可以通过 trait 关键字来定义一个特质,然后在其中声明一系列的方法。这些方法可以有默认实现,也可以没有实现(抽象方法),留给实现该特质的类型去具体实现。

trait Drawable {
    fn draw(&self);
}

在这个例子中,Drawable 是一个简单的特质,它有一个 draw 方法。任何实现了 Drawable 特质的类型都必须提供 draw 方法的具体实现。

实现特质

任何类型都可以实现一个或多个特质。实现特质意味着提供特质中所有抽象方法的具体实现。

struct Circle {
    radius: f64,
}

impl Drawable for Circle {
    fn draw(&self) {
        println!("Drawing a circle with radius {}", self.radius);
    }
}

在这个例子中,Circle 结构体实现了 Drawable 特质,提供了 draw 方法的具体实现。现在,你可以对 Circle 类型的实例调用 draw 方法。

使用特质作为参数

特质可以用来定义函数或方法的参数类型,使得你可以传入任何实现了该特质的类型作为参数。

fn render(item: &impl Drawable) {
    item.draw();
}

这里,render 函数接受一个实现了 Drawable 特质的类型的引用作为参数,并在其上调用 draw 方法。

特质约束

当你需要在泛型函数中对类型参数施加特质约束时,可以使用特质界定(Trait Bounds)。

fn render<T: Drawable>(item: &T) {
    item.draw();
}

这与上一个例子相似,但这次 render 是一个泛型函数,它只接受实现了 Drawable 特质的类型 T 的引用作为参数。

默认方法和覆盖

特质中的方法可以有默认实现,实现特质的类型可以选择使用默认实现或提供自己的实现。

trait Drawable {
    fn draw(&self) {
        println!("Drawing an unknown shape.");
    }
}

在这里,Drawable 特质有一个默认实现的 draw 方法。如果一个类型选择不提供 draw 方法的具体实现,它将自动获得这个默认实现。

总结

特质是 Rust 中实现代码复用和抽象的强大工具。通过特质,Rust 允许不同类型共享接口,实现多态,并提供灵活性和强大的抽象能力。特质还支持默认实现,允许类型重用或覆盖这些实现。这些特性使得 Rust 在构建大型且复杂的系统时,代码更加模块化和可维护。

单元测试和集成测试:学习如何在 Rust 中编写和运行测试,包括测试私有函数

在 Rust 中,测试是一等公民。Rust 提供了强大的内建测试工具,使得单元测试和集成测试都能轻松地编写和运行。这些工具帮助你验证代码的正确性,确保新增的特性不会破坏现有的功能。

单元测试

单元测试通常位于与它们测试的代码相同的文件中,并使用 #[cfg(test)]#[test] 属性进行标注。#[cfg(test)] 属性告诉 Rust 只在执行 cargo test 时才编译和运行测试代码。#[test] 属性标注一个函数为测试函数。

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}

这个例子展示了一个基本的单元测试。如果测试通过,cargo test 会输出测试成功的信息;如果测试失败(比如,如果你将 4 改为 5),它会输出失败的信息,帮助你定位问题。

测试私有函数

Rust 允许你测试私有函数。由于单元测试位于相同的文件中,它们可以访问私有模块、函数和字段。这使得你可以对内部实现细节进行测试,而无需将它们公开为 pub

fn private_function() -> i32 {
    42
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_private_function() {
        assert_eq!(private_function(), 42);
    }
}

这个例子中,即使 private_function 是私有的,单元测试仍然可以对它进行测试。

集成测试

集成测试位于项目根目录下的 tests 目录中,它们测试库的外部接口。每个文件被视为单独的 crate。

要创建集成测试,首先需要在你的项目根目录下创建一个名为 tests 的目录。然后,在这个目录中创建测试文件。

// tests/integration_test.rs

extern crate my_crate;

#[test]
fn test_external() {
    assert_eq!(my_crate::function_to_test(), 42);
}

这个例子展示了如何在集成测试中引入并测试库的外部接口。

运行测试

  • 运行所有测试:cargo test
  • 运行特定测试:cargo test it_works
  • 运行所有集成测试:cargo test --test integration_test

测试输出

默认情况下,如果测试通过,Rust 测试框架不会显示 println! 宏的输出。如果你希望看到成功测试的输出,可以使用:

cargo test -- --nocapture

总结

Rust 中的测试是一个强大的工具,它支持单元测试和集成测试,允许你测试私有函数,并提供了丰富的命令行选项来运行和控制测试输出。通过编写和维护测试,你可以提高代码质量,减少 bug,并确保代码的长期健康。

测试组织:理解如何组织测试代码和使用测试配置

组织测试代码是维护大型 Rust 项目的关键部分。正确的测试组织和配置方法不仅可以让测试更加易于编写和维护,还可以提高测试的效率。以下是一些关于如何组织测试代码和使用测试配置的最佳实践。

单元测试的组织

单元测试通常放在与源代码相同的文件中,但被封装在一个专用的 mod tests 模块中,并使用 #[cfg(test)] 属性注解。这种方式使得测试代码与生产代码分离,同时又能够访问私有函数和模块。

// src/lib.rs 或 src/main.rs

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_example() {
        assert_eq!(2 + 2, 4);
    }
}

这样组织单元测试的主要优点是,测试位于它们正在测试的代码旁边,这使得查看实现和测试代码变得方便。

集成测试的组织

与单元测试不同,集成测试应放在项目根目录下的 tests 文件夹中。Rust 会将每个文件当作单独的包来编译。这样做的目的是确保你的库的外部接口能够正常工作,而不是测试库的内部实现细节。

project_root/
├── Cargo.toml
├── src/
│   └── lib.rs
└── tests/
    ├── integration_test1.rs
    └── integration_test2.rs

集成测试通常会使用你的库的完整外部API,就像其他代码使用你的库那样。这有助于保证你的库对外部用户是可用的。

使用条件编译进行测试配置

#[cfg(test)] 属性允许你指定某些代码块仅在运行测试时才编译。除了用于 mod tests,它还可以用来条件编译测试时需要但正常构建不需要的代码。

#[cfg(test)]
use std::collections::HashMap;

#[cfg(test)]
mod tests {
    // 测试代码
}

控制测试行为

通过向 cargo test 命令添加参数,你可以控制测试的行为。例如,--test-threads 参数允许你设置测试运行时的线程数,而 --nocapture 参数允许你查看测试的输出,即使测试通过。

  • 运行特定测试:cargo test test_name
  • 在多线程环境下运行测试:cargo test -- --test-threads=4
  • 查看通过的测试的输出:cargo test -- --nocapture
  • 只运行忽略的测试:cargo test -- --ignored

文档测试

文档测试是 Rust 的另一个强大特性,允许你在文档注释中编写可测试的示例代码。这确保了你的文档是最新的,并且示例代码是可工作的。

/// 对两个数求和。
///
/// # 示例
///
/// ```
/// use my_crate::add;
///
/// assert_eq!(add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

这种方式既提升了文档质量,又增强了测试的全面性。

总之,合理组织和配置测试代码对于维护高质量的 Rust 项目至关重要。利用 Rust 的测试功能可以帮助你编写更健壯、易于维护的代码。

智能指针:熟悉 Box<T>、Rc<T>、RefCell<T> 和 Arc<T> 等智能指针的使用

智能指针是一种数据结构,它们不仅可以充当指针(比如提供对数据的引用),还可以附加额外的元数据和功能,比如引用计数。Rust 标准库提供了几种不同的智能指针,用于不同的场景。以下是最常用的几种智能指针的介绍和用法。

Box<T>

Box<T> 是最简单的智能指针,用于在堆上分配值。它可以用来存储大型数据或者当你希望确保数据不被复制时使用。它也常用于递归类型的构建。

let b = Box::new(5);
println!("b = {}", b);

Box<T> 的解引用是透明的——你可以像使用普通引用一样使用 Box<T>

Rc<T>

Rc<T> 是一个引用计数类型,允许多个所有者拥有同一个数据。Rc<T> 用于单线程场景。当最后一个 Rc<T> 被销毁时,其内部数据也会被销毁。

use std::rc::Rc;

let five = Rc::new(5);
let five_clone = Rc::clone(&five);

Rc::clone(&five) 并不会进行深拷贝,它只是增加了引用计数。这使得多个指针可以安全地共享相同的数据。

RefCell<T>

RefCell<T> 提供了内部可变性,即允许你在数据结构外部不可变的情况下修改内部数据。它在运行时执行借用检查,而非在编译时。这意味着你可以在只有不可变引用时,通过 RefCell<T> 修改数据。

use std::cell::RefCell;

let x = RefCell::new(42);

{
    let mut y = x.borrow_mut();
    *y += 1;
}

println!("{}", x.borrow());

这里使用 borrow_mut 方法获取一个可变引用,并修改了 RefCell 内部的值。使用 borrow 方法可以获取一个不可变引用。

Arc<T>

Arc<T> 是一个线程安全的引用计数智能指针。它类似于 Rc<T>,但可以安全地用于多线程环境。当最后一个 Arc<T> 被销毁时,其内部数据也会被销毁。

use std::sync::Arc;
use std::thread;

let value = Arc::new(5);
let value_clone = Arc::clone(&value);

thread::spawn(move || {
    println!("value: {}", value_clone);
}).join().unwrap();

在这个例子中,value_clone 被传入了一个新线程中。因为 Arc<T> 是线程安全的,所以这是安全的操作。

总结

智能指针提供了 Rust 中内存管理的高级功能。选择合适的智能指针可以帮助你实现更复杂的内存管理模式,比如共享数据、循环引用和内部可变性等。理解每种智能指针的用途和限制是进行高效和安全 Rust 编程的关键。

并发编程:学习 Rust 中的线程模型,理解如何创建线程以及线程间的数据共享和同步

Rust 提供了强大的并发编程能力,其设计目的是帮助开发者编写高效且安全的并发代码。Rust 的并发模型建立在线程之上,提供了几种机制来处理线程间的数据共享和同步。

创建线程

在 Rust 中,可以通过 std::thread 模块来创建新的线程。使用 thread::spawn 函数,你可以启动一个新的线程并执行一个闭包(匿名函数)。

use std::thread;

let handler = thread::spawn(|| {
    // 这里编写线程要执行的代码
    println!("Hello from a new thread!");
});

handler.join().unwrap(); // 等待线程结束

spawn 函数返回一个 JoinHandle,你可以用它来等待线程结束(通过调用 join 方法)。

线程间的数据共享

Rust 遵循所有权原则,这意味着你不能简单地从一个线程传递一个变量到另一个线程(因为这可能导致数据竞争)。为了在线程间共享数据,Rust 提供了几种同步操作的机制,如互斥锁(Mutex)、原子类型(Atomic 类型)和引用计数(Arc)。

使用 Mutex<T> 实现线程间的互斥访问

Mutex(互斥锁)允许多个线程访问某些数据,但任意时刻只允许一个线程访问。

use std::sync::{Arc, Mutex};
use std::thread;

let counter = Arc::new(Mutex::new(0));
let mut handlers = vec![];

for _ in 0..10 {
    let counter = Arc::clone(&counter);
    let handler = thread::spawn(move || {
        let mut num = counter.lock().unwrap();
        *num += 1;
    });
    handlers.push(handler);
}

for handler in handlers {
    handler.join().unwrap();
}

println!("Result: {}", *counter.lock().unwrap());

这个例子中,使用 Arc 来共享 Mutex(因为 Mutex 本身并不是线程安全的,需要包装在 Arc 中),Arc 允许多个线程拥有对同一个 Mutex 的所有权。

使用原子类型实现线程间的无锁同步

Rust 的 std::sync::atomic 模块提供了原子类型,如 AtomicBoolAtomicIsize 等,用于无锁的线程间同步。

use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;

let count = Arc::new(AtomicUsize::new(0));
let mut handlers = vec![];

for _ in 0..10 {
    let count = Arc::clone(&count);
    let handler = thread::spawn(move || {
        count.fetch_add(1, Ordering::SeqCst);
    });
    handlers.push(handler);
}

for handler in handlers {
    handler.join().unwrap();
}

println!("Count: {}", count.load(Ordering::SeqCst));

原子类型使用特殊的 CPU 指令来保证跨线程的操作是原子性的,这意味着操作要么完全执行,要么完全不执行,且不会被其他线程打断。

使用通道进行线程间通信

Rust 通过 std::sync::mpsc(多生产者,单消费者)提供了通道(channels)来实现线程间的通信。

use std::sync::mpsc;
use std::thread;

let (tx, rx) = mpsc::channel();

thread::spawn(move || {
    let val = String::from("hello");
    tx.send(val).unwrap();
});

let received = rx.recv().unwrap();
println!("Got: {}", received);

这个例子展示了如何创建一个通道,并在一个线程中发送消息,然后在另一个线程中接收消息。

总结

Rust 的并发编程模型旨在提供安全且有效的方式来利用现代多核处理器的能力。通过精心设计的 API 和所有权系统,Rust 帮助开发者避免了并发编程中常见的一些陷阱,如数据竞争和死锁。通过使用线程、互斥锁、原子类型和通道,你可以编写高效且安全的并发 Rust 应用。

  • 17
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值