Rust Programming :The Complete Developor‘s Guide--03 Trait

本文介绍了Rust编程语言中的模块系统,包括外部模块的结构、作用以及如何通过Cargo.toml配置。此外,讨论了trait的概念,用于实现多态性,以及泛型在编写通用函数和结构体时的应用。还涉及内存管理,如堆栈和堆的区别,以及trait对象如何实现动态类型和行为。
摘要由CSDN通过智能技术生成

External Modules

  • Allows code to be compartmentalized
    • Organized source code management
    • Better collaboration
  • More intuitive coding
    • Quickly identify where imported code is used

Module Details

  • Can have any name
  • Hierarchical organization
  • Private by default
    • Use pub keyword to make a module public
  • External modules can be a:
    • Directory
      • Must contain mod.rs
      • Can contain additional modules
    • File

File Structure
Cargo.toml

[lib]
name = "demo"
path = "src/lib/mod.rs"
.
├── bin
|   └── app.rs
└── lib
    ├── codec
    │   ├── audio
    |   |   ├── flac.rs
    |   |   ├── mod.rs
    |   |   └── mp3.rs
    │   ├── video
    |   |   ├── h264.rs
    |   |   ├── mod.rs
    |   |   └── vp9.rs
    │   └── mod.rs
    ├── mod.rs
    └── transcode.rs

Module Declaration

.
├── bin
|   └── app.rs
└── lib
    ├── codec
    │   ├── audio
    |   |   ├── flac.rs
    |   |   ├── mod.rs   -----------> pub mod flac; 
    |   |   |                         pub mod mp3; 
    |   |   └── mp3.rs
    │   ├── video
    |   |   ├── h264.rs
    |   |   ├── mod.rs   -----------> pub mod h264; 
    |   |   |                         pub mod vp9; 
    |   |   └── vp9.rs
    │   └── mod.rs       -----------> pub mod audio; 
    |                                 pub mod video; 
    |
    ├── mod.rs           -----------> pub mod codec; 
    |                                 pub mod transcode;
    └── transcode.rs

the transcode module exists as a file
the codec module exist as a directory

Recap

  • Modules are organized hierarchically
    • Use super to go up one level
    • Use crate to start from the top
  • The as keyword can be used to create an alias for a module
  • The mod keyword is used to declare a module
    • No curly braces for external modules
  • Modules can be re-exported with the use keyword
  • pub indicates the module may be accessed from anywhere
    • Ommitted pub restricts access to only the containing module and sub-modules

Activity External Modules

  • code structure
    .
    ├── Cargo.lock
    ├── Cargo.toml
    ├── README.md
    ├── src
    │   ├── activitylib.rs
    │   ├── main.rs
    │   ├── math.rs
    │   └── msg.rs
    └── target
    
  • Cargo.toml
    [lib]
    name = "activity"
    path = "src/activitylib.rs"
    
  • source code
    // src/activitylib.rs
    pub mod msg;
    pub mod math;
    
    // src/msg.rs
    pub fn trim(msg: &str) -> &str {
        msg.trim()
    }
    
    pub fn capitalize(msg: &str) -> std::borrow::Cow<'_, str> {
        if let Some(letter) = msg.get(0..1) {
            format!("{}{}", letter.to_uppercase(), &msg[1..msg.len()]).into()
        } else {
            msg.into()
        }
    }
    
    pub fn exciting(msg: &str) -> String {
        format!("{}", msg)
    }
    
    // src/math.rs
    pub fn add(lhs: isize, rhs: isize) -> isize {
        lhs + rhs
    }
    
    pub fn sub(lhs: isize, rhs: isize) -> isize {
        lhs - rhs
    }
    
    pub fn mul(lhs: isize, rhs: isize) -> isize {
        lhs * rhs
    }
    
    
    // main.rs
    fn main() {
        use activity::math;
        let result = {
            let two_plus_two = math::add(2, 2);
            let three = math::sub(two_plus_two, 1);
            math::mul(three, three)
        };
    
        assert_eq!(result, 9);
        println!("(2 + 2 - 1) * 3 = {}", result);
    
        {
            use activity::msg::{capitalize, exciting, trim};
            let hello = {
                let msg = "hello";
                let msg = trim(msg);
                capitalize(msg)
            };
    
            let world = {
                let msg = "world";
                exciting(msg)
            };
    
            let msg = format!("{}, {}", hello, world);
            assert_eq!(&msg, "Hello, world");
            println!("{}", msg);
        }
    }
    

Demo User Input

use std::io;

fn get_input() -> io::Result<String> {
    let mut buffer = String::new();
    io::stdin().read_line(&mut buffer)?;
    Ok(buffer.trim().to_owned())
}

fn main() {
    let mut all_input = vec![];
    let mut time_input = 0;

    while time_input < 2 {
        match get_input() {
            Ok(words) =>{
                all_input.push(words);
                time_input += 1;
            }
            Err(e) =>{
                println!("error: {:?}", e);
            }
        }
    }

    for input in all_input {
        println!("{:?}", input);
    }
}

Activity User Input

use std::io;

enum PowerState {
    Off,
    Sleep,
    Reboot,
    Shutdown,
    Hibernate
}

impl PowerState {
    fn new(state: &str)->Option<PowerState> {
        let state = state.trim().to_lowercase();
        // String -> &str
        match state.as_str() {
            "off" => Some(PowerState::Off),
            "sleep" => Some(PowerState::Sleep),
            "reboot" => Some(PowerState::Reboot),
            "shutdown" => Some(PowerState::Shutdown),
            "hibernate" => Some(PowerState::Hibernate),
            _ => None,
        }
    }
} 

fn print_power_action(state:PowerState) {
    use PowerState::*;
    match state {
        Off=> println!("turning off"),
        Sleep => println!("sleeping"),
        Reboot => println!("rebooting"),
        Shutdown => println!("shutting down"),
        Hibernate => println!("hibernating"),
    }
}

fn main() {
    println!("Enter new power state");
    let mut buffer = String::new();
    let user_input_status = io::stdin().read_line(&mut buffer);
    if user_input_status.is_ok() {
        match PowerState::new(&buffer) {
            Some(state)=> print_power_action(state),
            None=> println!("invalid power state"),
        }
    } else {
        println!("error reading input");
    }
}

Project Interactive billing application

About

  • Command line application to track bills / expenditures
    • Add, edit, view, remove
  • Purposefully simple
    • Focus on working with Result:
      • enums, Option, Result, match, iterators, etc
      • Ownership / Borrowing issues
      • Mutability
  • Objectives
    • Implement previously covered materials in a larger context
      • Solidify concepts before moving to more advanced material
        Sample
    == Manage Bills ==
    1. Add Bill
    2. View Bills
    3. Remove Bill
    4. Update Bill
    
    Enter selection:
    

Project

use std::collections::HashMap;

#[derive(Clone, Debug)]
pub struct Bill {
    name: String,
    amount: f64,
}

pub struct Bills {
    inner: HashMap<String, Bill>,
}

impl Bills {
    fn new() -> Self {
        Self {
            inner: HashMap::new(),
        }
    }

    fn add(&mut self, bill: Bill) {
        self.inner.insert(bill.name.to_string(), bill);
    }

    fn get_all(&self) -> Vec<&Bill> {
        self.inner.values().collect()
    }

    fn remove(&mut self, name: &str) -> bool {
        self.inner.remove(name).is_some()
    }

    fn update(&mut self, name: &str, amount: f64) -> bool {
        match self.inner.get_mut(name) {
            Some(bill) => {
                bill.amount = amount;
                true
            }
            None => false,
        }
    }
}

fn get_input() -> Option<String> {
    let mut buffer = String::new();
    while std::io::stdin().read_line(&mut buffer).is_err() {
        println!("Please enter your data again");
    }

    let input = buffer.trim().to_owned();
    if &input == "" {
        None
    } else {
        Some(input)
    }
}

fn get_bill_amount() -> Option<f64> {
    println!("Amount");
    loop {
        let input = match get_input() {
            Some(input) => input,
            None => return None,
        };

        if &input == "" {
            return None;
        }

        let parsed_input: Result<f64, _> = input.parse();
        match parsed_input {
            Ok(amount) => return Some(amount),
            Err(_) => println!("Please enter a number"),
        }
    }
}

mod menu {
    use crate::{get_bill_amount, get_input, Bill, Bills};

    pub fn add_bill(bills: &mut Bills) {
        println!("Bill name:");
        let name = match get_input() {
            Some(input) => input,
            None => return,
        };

        let amount = match get_bill_amount() {
            Some(amount) => amount,
            None => return,
        };

        let bill = Bill { name, amount };

        bills.add(bill);
        println!("Bill added");
    }

    pub fn remove_bill(bills: &mut Bills) {
        for bill in bills.get_all() {
            println!("{:?}", bill);
        }

        println!("Enter remove bill name: ");
        let name = match get_input() {
            Some(name) => name,
            None => return,
        };

        if bills.remove(&name) {
            println!("Bill removed");
        } else {
            println!("bill not found");
        }
    }

    pub fn update_bill(bills: &mut Bills) {
        for bill in bills.get_all() {
            println!("{:?}", bill);
        }

        println!("Enter update bill name: ");
        let name = match get_input() {
            Some(name) => name,
            None => return,
        };

        println!("Enter update bill amount: ");
        let amount = match get_bill_amount() {
            Some(amount) => amount,
            None => return,
        };

        if bills.update(&name, amount) {
            println!("Update bill done");
        } else {
            println!("bill not found");
        }
    }

    pub fn view_bills(bills: &Bills) {
        for bill in bills.get_all() {
            println!("{:?}", bill);
        }
    }
}
enum MainMenu {
    AddBill,
    ViewBill,
    RemoveBill,
    UpdateBill,
}

impl MainMenu {
    fn from_str(input: &str) -> Option<MainMenu> {
        match input {
            "1" => Some(Self::AddBill),
            "2" => Some(Self::ViewBill),
            "3" => Some(Self::RemoveBill),
            "4" => Some(Self::UpdateBill),
            _ => None,
        }
    }

    fn show() {
        println!("");
        println!("== Manage Bills ==");
        println!("1. Add Bill");
        println!("2. View Bills");
        println!("3. Remove Bill");
        println!("4. Update Bill");
        println!("");
        println!("Enter selection: ");
    }
}

fn run_program() -> Option<()> {
    // Create bill structure
    let mut bills = Bills::new();
    loop {
        // Display the menu
        MainMenu::show();
        let input = get_input()?;
        match MainMenu::from_str(input.as_str()) {
            Some(MainMenu::AddBill) => menu::add_bill(&mut bills),
            Some(MainMenu::ViewBill) => menu::view_bills(&bills),
            Some(MainMenu::RemoveBill) => menu::remove_bill(&mut bills),
            Some(MainMenu::UpdateBill) => menu::update_bill(&mut bills),
            None => break,
        }
        // Make a choice, based on input
    }

    None
}

fn main() {
    run_program();
}

Traits

  • A way to specify that some functionality exists
  • Used to standardize functionality acrosss multiple different types
    • Standardization permits functions to operate on multiple different types
    • Code deduplication
      Example
trait Noise {
    fn make_noise(&self);
}

struct Cat;
impl Noise for Cat {
    fn make_noise(&self) {
        println!("miao miao");
    }
}

struct Dog;
impl Noise for Dog {
    fn make_noise(&self) {
        println!("wang wang");
    }
}

fn hello(noisy: impl Noise) {
    noisy.make_noise();
}

fn main() {
    hello(Cat{});
    hello(Dog{});
}

Recap

  • Traits define similar functionality for different types
  • Trait functions are just regular functions
    • Can accept arguments and return values
  • Use impl Trait as a function argument to pass data via trait

Activity Trait

trait Perimeter {
    fn calculate_perimeter(&self) -> i32;
}

struct Square {
    side: i32,
}

impl Perimeter for Square {
    fn calculate_perimeter(&self) -> i32 {
        self.side * 4
    }
}

struct Triangle {
    side_a: i32,
    side_b: i32,
    side_c: i32,
}

impl Perimeter for Triangle {
    fn calculate_perimeter(&self) -> i32 {
        self.side_a + self.side_b + self.side_c
    }
}

fn print_perimeter(shape: impl Perimeter) {
    let perimeter = shape.calculate_perimeter();
    println!("perimeter = {:?}", perimeter);
}

fn main() {
    let square = Square { side: 5 };
    let triangle = Triangle {
        side_a: 2,
        side_b: 3,
        side_c: 4,
    };

    print_perimeter(square);
    print_perimeter(triangle);
}

** Default Trait**
The default trait is used to create new structures and enumerations with a default value

#[derive(Debug)]
struct Package {
    weight: f64,
}

impl Package {
    fn new(weight: f64) -> Self {
        Self { weight }
    }
}

// default trait
impl Default for Package {
    fn default() -> Self {
        Self { weight: 3.0 }
    }
}

fn main() {
    let p = Package::default();
    println!("{:?}", p);
}

Shared Functionality Generic Functions

What Are Generic Functions?

  • A way to write a function that can have a single parameter with multiple data types
  • Trait is used as function parameter instead of data type
    • Function depends on existence of functions declared by trait
  • Less code to write
    • Automatically works when new data types are intoduced

Quick Review: Traits

trait Move {
    fn move_to(&self, x: i32, y: i32);
}

struct Snake;
impl Move for Snake {
    fn move_to(&self, x: i32, y: i32) {
        println!("slither to ({}, {})", x, y);
    }
}

struct Grasshopper;
impl Move for Grasshopper {
    fn move_to(&self, x: i32, y: i32) {
        println!("hop to ({}, {})", x, y);
    }
}

// thing has to implement the move trait
fn make_move(thing: impl Move, x: i32, y: i32) {
    thing.move_to(x, y);
}

fn main() {
    let python = Snake {};
    make_move(python, 1, 1);
}

Generic Syntax

fn function(param1: impl Trait1, param2: impl Trait2) {
    /* body */
}

fn function<T: Traits, U: Trait2>(param1: T, param2: U) {
    /* body */
}

fn function<T, U>(param1: T, param2: U) 
where 
    T: Trait1 + Trait2,
    U: Trait1 + Trait2 + Trait3,
{
    /* body */
}

Generic Example

fn make_move(thing: impl Move, x: i32, y: i32) {
    thing.move_to(x, y);
}

fn make_move<T: Move>(thing: T, x: i32, y: i32) {
    thing.move_to(x, y);
}

fn make_move<T>(thing: T, x: i32, y: i32) 
where 
    T: Move,
{
    thing.move_to(x, y);
}

Recap

  • Generics let you write one function to work with multiple types of data
  • Generic functions are “bound” or “constrained” by the traits
    • Only able to work with data that implements the trait
  • Three syntaxes available:
    • fn func(param: impl Trait) {}
    • fn func<T: Trait>(param: T) {}
    • fn func(param: T) where T: Trait {}

Demo Generic Functions

trait CheckIn {
    fn check_in(&self);
    fn process(&self);
}

struct Pilot;
impl CheckIn for Pilot {
    fn check_in(&self) {
        println!("checked in as pilot");
    }

    fn process(&self) {
        println!("pilot enters the cockpit");
    }
}
struct Passenger;
impl CheckIn for Passenger {
    fn check_in(&self) {
        println!("checked in as passenger");
    }

    fn process(&self) {
        println!("passenger takes a seat");
    }
}
struct Cargo;
impl CheckIn for Cargo {
    fn check_in(&self) {
        println!("cargo checked in");
    }

    fn process(&self) {
        println!("cargo moved to storage");
    }
}

fn process_item<T: CheckIn>(item: T) {
    item.check_in();
    item.process();
}
fn main() {
    let paul = Passenger;
    let kathy = Pilot;
    let cargo1 = Cargo;
    let cargo2 = Cargo;
    process_item(paul);
    process_item(kathy);
    process_item(cargo1);
    process_item(cargo2);
}

Activity Generic Functions

#[derive(Debug)]
enum ServicePriority {
    High,
    Standard,
}

trait Priority {
    fn get_priority(&self) -> ServicePriority;
}

#[derive(Debug)]
struct ImportantGuest;
impl Priority for ImportantGuest {
    fn get_priority(&self) -> ServicePriority {
        ServicePriority::High
    }
}

#[derive(Debug)]
struct Guest;
impl Priority for Guest {
    fn get_priority(&self) -> ServicePriority {
        ServicePriority::Standard
    }
}

fn print_guest_priority<T: Priority + std::fmt::Debug>(guest: T) {
    println!("{:?} is {:?} priority", guest, guest.get_priority());
}

fn main() {
    let guest = Guest;
    let vip = ImportantGuest;
    print_guest_priority(guest);
    print_guest_priority(vip);
}

Generic Structures

  • Store data of any type within a structure
    • Trait bounds restrict the type of data the structure can utilize
      • Also known as “generic constraints”
  • Useful when making your own data collections
  • Reduces technical debt as program expands
    • New data types can utilize generic structures and be easily integrated into the program

Syntax

struct Name<T: Trait1 + Trait2, U: Trait3> {
    field1: T,
    field2: U,
}

struct Name<T, U>
where
    T: Trait1 + Trait2,
    U: Trait3,
{
    field1: T,
    field2: U,
}

Generic Structures impl blocks

Implementing Functionality

  • Generic implementation
    • Implements functionality for any type that can be used with the structure
  • Concrete implementation
    • Implements functionality for only the type specified

Concrete Implementation - Setup

trait Game {
    fn name(&self) -> String;
}

enum BoardGame {
    Chess,
    Monopoly,
}

enum VideoGame {
    PlayStation,
    Xbox,
}

impl Game for BoardGame {
    // ...
}

impl Game for VideoGame {
    // ...
}

struct PlayRoom<T: Game> {
    game: T,
}

impl PlayRoom<BoardGame> {
    pub fn cleanup(&mut self) {
        // ...
    }
}

fn main() {
    let video_room = PlayRoom {
        game: VideoGame::Xbox,
    };

    let board_room = PlayRoom {
        game: BoardGame::Monopoly,
    };

    board_room.cleanup();
    video_room.cleanup();// method not found in PlayRoom<VideoGame>
}

Generic Implementation - Syntax

struct Name<T: Trait1 + Trait2, U: Trait3> {
    field1: T,
    field2: U,
}

impl<T: Trait1 + Trait2, U: Trait3> Name<T, U> {
    fn func(&self, arg1: T, arg2: U) {}
}

struct Name<T, U>
where
    T: Trait1 + Trait2,
    U: Trait3,
{
    field1: T,
    field2: U,
}

impl<T, U> Name<T, U>
where
    T: Trait1 + Trait2,
    U: Trait3,
{
    fn func(&self, arg1: T, arg2: U) {}
}

Demo Generic Structures

struct Dimensions {
    width: f64,
    height: f64,
    depth: f64,
}
struct ConveyBelt<T: Convey> {
    pub items: Vec<T>,
}

impl<T: Convey> ConveyBelt<T> {
    pub fn add(&mut self, item: T) {
        self.items.push(item);
    }
}

struct CarPart {
    width: f64,
    height: f64,
    depth: f64,
    weight: f64,
    part_number: String,
}

impl Default for CarPart {
    fn default() -> Self {
        Self {
            width: 5.0,
            height: 1.0,
            depth: 2.0,
            weight: 3.0,
            part_number: "abc".to_owned(),
        }
    }
}
trait Convey {
    fn weight(&self) -> f64;
    fn dimensions(&self) -> Dimensions;
}

impl Convey for CarPart {
    fn weight(&self) -> f64 {
        self.weight
    }
    fn dimensions(&self) -> Dimensions {
        Dimensions {
            width: self.width,
            height: self.height,
            depth: self.depth,
        }
    }
}

fn main() {
    let mut belt: ConveyBelt<CarPart> = ConveyBelt { items: vec![] };

    belt.add(CarPart::default());

    let mut belt = ConveyBelt { items: vec![] };
}

Activity Generic Structures

trait Body {}
trait Color {}

#[derive(Debug)]
struct Vehicle<B, C>
where
    B: Body,
    C: Color,
{
    body: B,
    color: C,
}

impl<B, C> Vehicle<B, C>
where
    B: Body,
    C: Color,
{
    fn new(body: B, color: C) -> Self {
        Self { body, color }
    }
}
#[derive(Debug)]
struct Car;
impl Body for Car {}

#[derive(Debug)]
struct Truck;
impl Body for Truck {}

#[derive(Debug)]
struct Red;
impl Color for Red {}

#[derive(Debug)]
struct Blue;
impl Color for Blue {}

fn main() {
    let red_truck = Vehicle::new(Truck, Red);
    let blue_car = Vehicle::new(Car, Blue);
    println!("{:?}", red_truck);
    println!("{:?}", blue_car);
}

Fundamentals Advanced Memory

Intermediate memory refresh

  • All data has a memory address
    • Addresses determine the location of data in memory
  • Offsets can be used to access adjacent addresses
    • Also called indexes/indices

Stack

  • Data placed sequentially
  • Limited space
  • All variables stored on the stack
    • Not all data
  • Very fast to work with
    • Offsets to access

Heap

  • Data placed algorithmically
    • Slower than stack
  • Unlimited space(RAM/disk limits apply)
  • Uses pointers
    • Pointers are fixed size
    • usize data type
  • Vectors & HashMaps stored on the heap
    • All dynamically sized collections

Example

struct Entry {
    id: i32,
}

fn main() {
    let data = Entry { id: 5 };
    let data_ptr: Box<Entry> = Box::new(data);
    let data_stack = *data_ptr;
}

Recap

  • Stack
    • Sequential memory addresses
    • Used for variables
    • Limited sizde
    • Must know data size ahead of time
  • Heap
    • Algorithmically calculated memory address
    • Used for large amounts of data
    • Unlimited size
    • Dynamically size data/unknown sized data

Shared Functionality Trait Objects

Trait Object Basics

  • Dynamically allocated object
    • Runtime generics
      • More flexible than generics
      • Dynamic Dispatch VS Static Dispatch
  • Allows mixed types in a collection
    • Easier to work with similar data types
    • Polymorphic program behavior
      • Dynamically change program behavior at runtime
      • Easily add new behaviors just by creating a new struct
  • Small performance penalty

Creating a Trait Object

trait Clicky {
    fn click(&self);
}

struct Keyboard;
impl Clicky for Keyboard {
    fn click(&self) {
        println!("click clack");
    }
}

fn main() {
    let keeb = Keyboard;
    let keeb_obj: &dyn Clicky = &keeb;

    let keeb: &dyn Clicky = &Keyboard;
    let keeb: Box<dyn Clicky> = Box::new(Keyboard);
}

Trait Object Parameter - Borrow

fn borrow_clicky(obj:&dyn Clicky) {
    obj.click();
}

let keeb = Keyboard;
borrow_clicky(&keeb);

Trait Object Parameter - Move

fn move_clicky(obj: Box<dyn Clicky>) {
    obj.click();
}

let keeb = Box::new(Keyboard);
move_clicky(keeb);

Heterogenous Vector

struct Mouse;
impl Clicky for Mouse {
    fn click(&self) {
        println!("click");
    }
}

fn make_clicks(clickers:Vec<Box<dyn Clicky>>) {
    for clicker in clickers{
        clicker.click();
    }
}

let keeb: Box<dyn Clicky> = Box::new(Keyboard);
let mouse: Box<dyn Clicky> = Box::new(Mouse);
let clickers = vec![keeb, mouse];
make_clicks(clickers);

let keeb  = Box::new(Keyboard);
let mouse = Box::new(Mouse);
let clickers: Vec<Box<dyn Clicky>> = vec![keeb, mouse];
make_clicks(clickers);

Recap

  • Trait objects allow for composite collections
  • Slightly less performant than using generics
  • Use the dyn keyword when workong with trait objects
  • Trait objects can be borrowed using a reference or moved using a box
    • Usually want to use a box when storing trait objects in a Vector

Demo Trait Objects

trait Sale {
    fn amount(&self) -> f64;
}

struct FullSale(f64);
impl Sale for FullSale {
    fn amount(&self) -> f64 {
        self.0
    }
}

struct OneDollarOffCoupon(f64);
impl Sale for OneDollarOffCoupon {
    fn amount(&self) -> f64 {
        self.0 - 1.0
    }
}
struct TenPercentOffPromo(f64);
impl Sale for TenPercentOffPromo {
    fn amount(&self) -> f64 {
        self.0 * 0.9
    }
}

fn calculate_revenue(sales: &Vec<Box<dyn Sale>>) -> f64 {
    sales.iter().map(|sale| sale.amount()).sum()
}
fn main() {
    let price = 20.0;
    let regular = Box::new(FullSale(price));
    let coupon = Box::new(OneDollarOffCoupon(price));
    let promo = Box::new(TenPercentOffPromo(price));

    // add a type annotations
    let sales: Vec<Box<dyn Sale>> = vec![regular, coupon, promo];
    println!("total revenue {:?}", calculate_revenue(&sales));
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

血_影

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值