rust program英文和汉语笔记(5)

面向对象
封装:结构体自身被标记为 pub,这样其他代码就可以使用这个结构体,但是在结构体内部的字段仍然是私有的。
继承:
有时为 trait 中的某些或全部方法提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。
pub trait Summary {//相当于interface,virtual function
fn summarize(&self) -> String {
String::from("(Read more…)")//默认实现
}
}

pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}

impl Summary for NewsArticle {}

pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}

impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)//覆盖重载,否则就会继承Summary的默认实现
}
}
如果想要对 NewsArticle 实例使用这个默认实现,而不是定义一个自己的实现,则可以通过 impl Summary for NewsArticle {} 指定一个空的 impl 块。
如果不想覆盖重载,可以pub trait Summary {
fn summarize_author(&self) -> String;

fn summarize(&self) -> String {
    format!("(Read more from {}...)", self.summarize_author())
}

}

pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}

impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}//覆盖重载summarize_author
多态(Polymorphism)

很多人将多态描述为继承的同义词。不过它是一个有关可以用于多种类型的代码的更广泛的概念。对于继承来说,这些类型通常是子类。 Rust 则通过泛型来对不同的可能类型进行抽象,并通过 trait bounds 对这些类型所必须提供的内容施加约束。这有时被称为 bounded parametric polymorphism。

近来继承作为一种语言设计的解决方案在很多语言中失宠了,因为其时常带有共享多于所需的代码的风险。子类不应总是共享其父类的所有特征,但是继承却始终如此。如此会使程序设计更为不灵活,并引入无意义的子类方法调用,或由于方法实际并不适用于子类而造成错误的可能性。某些语言(java?)还只允许子类继承一个父类,进一步限制了程序设计的灵活性。
Vec[枚举]来储存整型,浮点型和文本成员的替代方案(这就是指针数组)
我们通过指定某种指针来创建 trait 对象,例如 & 引用或 Box 智能指针,还有 dyn keyword, 以及指定相关的 trait(第十九章 ““动态大小类型和 Sized trait” 部分会介绍 trait 对象必须使用指针的原因)。我们可以使用 trait 对象代替泛型或具体类型。任何使用 trait 对象的位置,Rust 的类型系统会在编译时确保任何在此上下文中使用的值会实现其 trait 对象的 trait。如此便无需在编译时就知晓所有可能的类型。
不过 trait 对象不同于传统的对象,因为不能向 trait 对象增加数据。trait 对象并不像其他语言中的对象那么通用:其(trait 对象)具体的作用是允许对通用行为进行抽象。这是函数编程的基因,重方法,轻数据。数据不能继承,只能impl trait名 for 数据名方式联系全部新数据。

Trait对象比泛型更抽象通用,例如:
src/lib.rs

pub struct Screen<T: Draw> {//Draw改成Box>是否可以draw()不同类型?
pub components: Vec,
}

impl Screen
where
T: Draw,
{
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}

示例 17-6: 一种 Screen 结构体的替代实现,其 run 方法使用泛型和 trait bound

这限制了 Screen 实例必须拥有一个全是 Button 类型或者全是 TextField 类型的组件列表。如果只需要同质(相同类型)集合或知道具体是什么类型就可以type/trait bound,则倾向于使用泛型和 trait bound,因为其定义会在编译时采用具体类型进行单态化。不知道具体类型,就Box::new(dyn trait))

另一方面,通过使用 trait 对象的方法,一个 Screen 实例可以存放一个既能包含 Box,也能包含 Box 的 Vec。让我们看看它是如何工作的,接着会讲到其运行时性能影响。
pub trait Draw {
fn draw(&self);
}

pub struct Screen {
pub components: Vec<Box>,
}

impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}

pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}

impl Draw for Button {
fn draw(&self) {
// code to actually draw a button
}
}

struct SelectBox {
width: u32,
height: u32,
options: Vec,
}

impl Draw for SelectBox {
fn draw(&self) {
// code to actually draw a select box
}
}

use gui::{Button, Screen,SelectBox};

fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from(“Yes”),
String::from(“Maybe”),
String::from(“No”),
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from(“OK”),
}),
],
};

screen.run();

}

使用 trait 对象和 Rust 类型系统来进行类似鸭子类型操作的优势是无需在运行时检查一个值是否实现了特定方法或者担心在调用时因为值没有实现方法而产生错误。如果值没有实现 trait 对象所需的 trait 则 Rust 不会编译这些代码。动态分发也阻止编译器有选择的内联方法代码,这会相应的禁用一些优化。
如果一个 trait 中定义的所有方法都符合以下规则,则该 trait 是对象安全的:

返回值不是 Self
没有泛型类型的参数

就是说,必须知道传递的是什么具体类型。

pub struct Post {
state: Option<Box>,
content: String,
}

impl Post {
// --snip–
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}

pub fn add_text(&mut self, text: &str) {
    self.content.push_str(text);
}

pub fn content(&self) -> &str {
    ""
}

pub fn request_review(&mut self) {
    if let Some(s) = self.state.take() {
        self.state = Some(s.request_review())
    }
}

}

trait State {
fn request_review(self: Box) -> Box;
}

struct Draft {}

impl State for Draft {
fn request_review(self: Box) -> Box {
Box::new(PendingReview {})
}
}

struct PendingReview {}

impl State for PendingReview {
fn request_review(self: Box) -> Box {
self
}
}

面向对象设计模式的实现

状态模式(state pattern)是一个面向对象设计模式。该模式的关键在于一个值有某些内部状态,体现为一系列的 状态对象,同时值的行为随着其内部状态而改变。状态对象共享功能:当然,在 Rust 中使用结构体和 trait 而不是对象和继承。每一个状态对象负责其自身的行为,以及该状态何时应当转移至另一个状态。持有一个状态对象的值对于不同状态的行为以及何时状态转移毫不知情。

使用状态模式意味着当程序的业务需求改变时,无需改变值持有状态或者使用值的代码。我们只需更新某个状态对象中的代码来改变其规则,或者是增加更多的状态对象。

将状态编码进不同的类型,实现状态转移为不同类型的转换,这就是面向对象的精髓?使得这个实现不再完全遵守面向对象的状态模式:状态间的转换不再完全封装在 Post 实现中。然而,得益于类型系统和编译时类型检查,我们得到了的是无效状态是不可能的!这确保了某些特定的 bug,比如显示未发布博文的内容,将在部署到生产环境之前被发现。即便 Rust 能够实现面向对象设计模式,也有其他像将状态编码进类型这样的模式存在。这些模式有着不同的权衡取舍。虽然你可能非常熟悉面向对象模式,重新思考这些问题来利用 Rust 提供的像在编译时避免一些 bug 这样有益功能。在 Rust 中面向对象模式并不总是最好的解决方案,因为 Rust 拥有像所有权这样的面向对象语言所没有的功能。
pub struct Post {//已或可发表的
content: String,
}

pub struct DraftPost {//草稿
content: String,
}

impl Post {
pub fn new() -> DraftPost {
DraftPost {
content: String::new(),
}
}

pub fn content(&self) -> &str {//post 还要用,就&
    &self.content
}

}

impl DraftPost {
// --snip–
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}

pub fn request_review(self) ->
         PendingReviewPost {//销了
        content: self.content,
    }
}

pub struct PendingReviewPost {
content: String,//空子串
}

impl PendingReviewPost {
pub fn approve(self) -> Post {//销了
Post {
content: self.content,
}
}
}
use blog::Post;

fn main() {

let mut post = Post::new();//DraftPost

post.add_text("I ate a salad for lunch today");//post=DraftPost

let post = post.request_review();//post=PendingReviewPost

let post = post.approve();//post=Post

assert_eq!("I ate a salad for lunch today", post.content());

}//这个状态转换,一气呵成,过一个流水线环节就用move self,这样前后都无法越界查看审批。纵向的流程就用这个move self,横向的就用Arc(Mutex)并发,并发就是借用clone。
函数的返回符号->可以理解为指向:一个返回类型结构:
模式匹配:
if let 表达式的缺点在于其穷尽性没有为编译器所检查,而 match 表达式则检查了。如果去掉最后的 else 块而遗漏处理一些情况,编译器也不会警告这类可能的逻辑错误。
while let 条件循环,它允许只要模式匹配就一直进行 while 循环。
for 也是
fn main() {
let v = vec![‘a’, ‘b’, ‘c’];

for (index, value) in v.iter().enumerate() {
    println!("{} is at index {}", value, index);
}

}

let PATTERN = EXPRESSION;//仿射
如:fn main() {
let (x, y, z) = (1, 2, 3);
}
函参也是:fn print_coordinates(&(x, y): &(i32, i32)) {
println!(“Current location: ({}, {})”, x, y);
}

fn main() {
let point = (3, 5);
print_coordinates(&point);
}
函数参数、 let 语句和 for 循环只能接受不可反驳的模式,因为通过不匹配的值程序无法进行有意义的工作。if let 和 while let 表达式被限制为只能接受可反驳的模式,因为根据定义他们意在处理可能的失败:条件表达式的功能就是根据成功或失败执行不同的操作。
为了修复在需要不可反驳模式的地方使用可反驳模式的情况,可以修改使用模式的代码:不同于使用 let,可以使用 if let。
fn main() {
let x = 1;

match x {
    1 | 2 => println!("one or two"),
    3 => println!("three"),
    _ => println!("anything"),
}

}
fn main() {
let x = 5;

match x {
    1..=5 => println!("one through five"),//范围只允许用于数字或 char 值
    _ => println!("something else"),
}

}
fn main() {
let x = ‘c’;

match x {
    'a'..='j' => println!("early ASCII letter"),
    'k'..='z' => println!("late ASCII letter"),
    _ => println!("something else"),
}

}
解构结构体:
struct Point {
x: i32,
y: i32,
}

fn main() {
let p = Point { x: 0, y: 7 };

let Point { x, y } = p;
assert_eq!(0, x);
assert_eq!(7, y);

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

fn main() {
let msg = Message::ChangeColor(0, 160, 255);

match msg {
    Message::Quit => {
        println!("The 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::ChangeColor(r, g, b) => println!(
        "Change the color to red {}, green {}, and blue {}",
        r, g, b
    ),
}

}
嵌套:
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}

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

fn main() {
let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));

match msg {
    Message::ChangeColor(Color::Rgb(r, g, b)) => println!(
        "Change the color to red {}, green {}, and blue {}",
        r, g, b
    ),
    Message::ChangeColor(Color::Hsv(h, s, v)) => println!(
        "Change the color to hue {}, saturation {}, and value {}",
        h, s, v
    ),
    _ => (),
}

}
解构:
fn main() {
struct Point {
x: i32,
y: i32,
}

let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });

}

这里得到了警告说未使用变量 y,不过没有警告说未使用下划线开头的变量。

注意, 只使用 _ 和使用以下划线开头的名称有些微妙的不同:比如 _x 仍会将值绑定到变量,而 _ 则完全不会绑定。

可以使用 … 语法来只使用部分并忽略其它值,
fn main() {
struct Point {
x: i32,
y: i32,
z: i32,
}

let origin = Point { x: 0, y: 0, z: 0 };

match origin {
    Point { x, .. } => println!("x is {}", x),
}

}
匹配守卫:这种额外表现力的缺点在于当涉及匹配守卫表达式时编译器不会尝试检查穷尽性。
fn main() {
let num = Some(4);

match num {
    Some(x) if x < 5 => println!("less than five: {}", x),
    Some(x) => println!("{}", x),
    None => (),
}

}
也可以在匹配守卫中使用 或 运算符 | 来指定多个模式,同时匹配守卫的条件会作用于所有的模式。
@:
fn main() {
enum Message {
Hello { id: i32 },
}

let msg = Message::Hello { id: 5 };

match msg {
    Message::Hello {
        id: id_variable @ 3..=7,
    } => println!("Found an id in range: {}", id_variable),
    Message::Hello { id: 10..=12 } => {
        println!("Found an id in another range")
    }
    Message::Hello { id } => println!("Found some other id: {}", id),
}

}//上例会打印出 Found an id in range: 5

Unsafe:
Rust 存在不安全一面的原因是:底层计算机硬件固有的不安全性。如果 Rust 不允许进行不安全操作,那么有些任务则根本完成不了。Rust 需要能够进行像直接与操作系统交互,甚至于编写你自己的操作系统这样的底层系统编程!
五个不检查:1、解引用裸指针:程序员确保不溢出
2、调用不安全的函数或方法:extern “C” {
fn abs(input: i32) -> i32;
}

fn main() {
unsafe {
println!(“Absolute value of -3 according to C: {}”, abs(-3));
}
}也可以被调用:
#![allow(unused)]
fn main() {
#[no_mangle]
pub extern “C” fn call_from_c() {
println!(“Just called a Rust function from C!”);
}
}
3、三种变通方法访问或修改可变静态变量:
Const…常量:没问题
多线程的通道的接收端rx代替全局mut
单线程:Rc::new(RefCell::new(x)),编译器会检查提前drop,而且还能修改,所以会在错误中建议你使用,多线程:Arc::new(Mutex::new(全局mut))
4、实现不安全 trait(trait中用泛型从而不能确定类型)
5、访问 union 的字段是不安全的,因为 Rust 无法保证当前存储在联合体实例中数据的类型,union本来就是类型的隐式转换
高级trait
struct Counter {
count: u32,
}

impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}

impl Iterator for Counter {
type Item = u32;

fn next(&mut self) -> Option<Self::Item> {//覆盖继承函数next
    // --snip--
    if self.count < 5 {
        self.count += 1;
        Some(self.count)
    } else {
        None
    }
}

}//直接impl trait_name for struct_name更简洁地覆盖继承方法
不用type bound,不用泛型,是因为只针对特定struct使用一次,实际上不断地for也是类型注释
pub trait Iterator {
fn next(&mut self) -> Option;
}使用时需注释类型,书写麻烦,因重复使用。而且有类型一致限制,不像Box::new(dyn…)

#![allow(unused)]
fn main() {
trait Add<Rhs=Self> {//默认泛型参数
type Output;//重载占位

fn add(self, rhs: Rhs) -> Self::Output;

}
}
Rust 并不允许创建自定义运算符或重载任意运算符,不过 std::ops 中所列出的运算符和相应的 trait 可以通过实现运算符相关 trait 来重载
use std::ops::Add;

struct Millimeters(u32);
struct Meters(u32);

impl Add for Millimeters {//重载
type Output = Millimeters;//输出类型重载

fn add(self, other: Meters) -> Millimeters {
    Millimeters(self.0 + (other.0 * 1000))
}

}
完全限定语法(fully qualified syntax):
trait Animal {
fn baby_name() -> String;
}

struct Dog;

impl Dog {
fn baby_name() -> String {
String::from(“Spot”)
}
}

impl Animal for Dog {
fn baby_name() -> String {
String::from(“puppy”)
}
}

fn main() {
println!(“A baby dog is called a {}”, ::baby_name());
}

Newtype:
use std::fmt;

struct Wrapper(Vec);//元组结构体

impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {//重载
write!(f, “[{}]”, self.0.join(", "))//元组的第一个元素
}
}

fn main() {
let w = Wrapper(vec![String::from(“hello”), String::from(“world”)]);
println!(“w = {}”, w);
}
类型别名:
fn main() {
type Thunk = Box<dyn Fn() + Send + 'static>;

let f: Thunk = Box::new(|| println!("hi"));

fn takes_long_type(f: Thunk) {
    // --snip--
}

fn returns_long_type() -> Thunk {
    // --snip--
    Box::new(|| ())//Box::new包装后可以返回闭包
}

}

Rust 有一个叫做 ! 的特殊类型。在类型理论术语中,它被称为 empty type,因为它没有值。我们更倾向于称之为 never type。这个名字描述了它的作用:在函数从不返回的时候充当返回值。

为了将 trait 用于 trait 对象,必须将他们放入指针之后,比如 &dyn Trait 或 Box(Rc 也可以)。

泛型函数默认只能用于在编译时已知大小的类型。然而可以使用如下特殊语法来放宽这个限制:

fn generic<T: ?Sized>(t: &T) {
// --snip–
}

?Sized 上的 trait bound 意味着 “T 可能是也可能不是 Sized” 同时这个注解会覆盖泛型类型必须在编译时拥有固定大小的默认规则。这种意义的 ?Trait 语法只能用于 Sized ,而不能用于任何其他 trait。

函数指针实现了所有三个闭包 trait(Fn、FnMut 和 FnOnce),所以总是可以在调用期望闭包的函数时传递函数指针作为参数。倾向于编写使用泛型和闭包 trait 的函数,这样它就能接受函数或闭包作为参数。

一个只期望接受 fn 而不接受闭包的情况的例子是与不存在闭包的外部代码交互时:C 语言的函数可以接受函数作为参数,但 C 语言没有闭包。
fn add_one(x: i32) -> i32 {
x + 1
}

fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}

fn main() {
let answer = do_twice(add_one, 5);

println!("The answer is: {}", answer);

}
这里创建了 Status::Value 实例,它通过 map 用范围的每一个 u32 值调用 Status::Value 的初始化函数。一些人倾向于函数风格,一些人喜欢闭包。这两种形式最终都会产生同样的代码,所以请使用对你来说更明白的形式吧。
fn main() {
enum Status {
Value(u32),
Stop,
}

let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();

}

返回闭包
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}


一个函数标签必须声明函数参数个数和类型。相比之下,宏能够接受不同数量的参数:用一个参数调用 println!(“hello”) 或用两个参数调用 println!(“hello {}”, name) 。而且,宏可以在编译器翻译代码前展开,例如,宏可以在一个给定类型上实现 trait 。而函数则不行,因为函数是在运行时被调用,同时 trait 需要在编译时实现。

实现一个宏而不是一个函数的缺点是宏定义要比函数定义更复杂,因为你正在编写生成 Rust 代码的 Rust 代码。由于这样的间接性,宏定义通常要比函数定义更难阅读、理解以及维护。

宏和函数的最后一个重要的区别是:在一个文件里调用宏 之前 必须定义它,或将其引入作用域,而函数则可以在任何地方定义和调用。

附件:
raw identiferb原始标识符
fn r#match(needle: &str, haystack: &str) -> bool {
haystack.contains(needle)
}

fn main() {
assert!(r#match(“foo”, “foobar”));
}

Rust库
https://doc.rust-lang.org/std/sync/struct.Mutex.html
单线程:用std::rc:Rc去count多所有者,用use std::sync::Weak;去count多借用者
多线程:只读并发,就不用锁,use std::sync::mpsc::channel;如多写,use std::sync::{Arc, Mutex};,而且在线程中不再需要就drop,最后还要let =match {去测试有没有中途panic!情况

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

const N: usize = 10;

// Spawn a few threads to increment a shared variable (non-atomically), and
// let the main thread know once all increments are done.
//
// Here we’re using an Arc to share memory among threads, and the data inside
// the Arc is protected with a mutex.
let data = Arc::new(Mutex::new(0));

let (tx, rx) = channel();
for _ in 0…N {
let (data, tx) = (Arc::clone(&data), tx.clone());
thread::spawn(move || {
// The shared state can only be accessed once the lock is held.
// Our non-atomic increment is safe because we’re the only thread
// which can access the shared state when the lock is held.
//
// We unwrap() the return value to assert that we are not expecting
// threads to ever fail while holding the lock.
let mut data = data.lock().unwrap();
*data += 1;
if *data == N {
tx.send(()).unwrap();
}
// the lock is unlocked here when data goes out of scope.
});
}
rx.recv.unwrap();

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

let lock = Arc::new(Mutex::new(0_u32));
let lock2 = Arc::clone(&lock);

let _ = thread::spawn(move || -> () {
// This thread will acquire the mutex first, unwrapping the result of
// lock because the lock has not been poisoned.
let _guard = lock2.lock().unwrap();
//不及时drop _guard?马上就join()了
// This panic while holding the lock (_guard is in scope) will poison
// the mutex.
panic!();
}).join();

// The lock is poisoned by this point, but the returned result can be
// pattern matched on to return the underlying guard on both branches.
let mut guard = match lock.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),//can access inner data
};
*guard += 1;

Drop:
use std::sync::{Arc, Mutex};
use std::thread;
const N: usize = 3;

let data_mutex = Arc::new(Mutex::new(vec![1, 2, 3, 4]));
let res_mutex = Arc::new(Mutex::new(0));

let mut threads = Vec::with_capacity(N);
(0…N).for_each(|_| {
let data_mutex_clone = Arc::clone(&data_mutex);
let res_mutex_clone = Arc::clone(&res_mutex);

threads.push(thread::spawn(move || {
    let mut data = data_mutex_clone.lock().unwrap();
    // This is the result of some important and long-ish work.
    let result = data.iter().fold(0, |acc, x| acc + x * 2);
    data.push(result);
    drop(data);//unnecessary,but efficient
    *res_mutex_clone.lock().unwrap() += result;
}));

});

let mut data = data_mutex.lock().unwrap();
// This is the result of some important and long-ish work.
let result = data.iter().fold(0, |acc, x| acc + x * 2);
data.push(result);
// We drop the data explicitly because it’s not necessary anymore and the
// thread still has work to do. This allow other threads to start working on
// the data immediately, without waiting for the rest of the unrelated work
// to be done here.
//
// It’s even more important here than in the threads because we .join the
// threads after that. If we had not dropped the mutex guard, a thread could
// be waiting forever for it, causing a deadlock.
drop(data);//necessary
// Here the mutex guard is not assigned to a variable and so, even if the
// scope does not end after this line, the mutex is still released: there is
// no deadlock.mutex一定要及时drop?
*res_mutex.lock().unwrap() += result;

threads.into_iter().for_each(|thread| {
thread
.join()//if don’t drop,still mutex,cause deadlock
.expect(“The thread creating or execution failed !”)
});

assert_eq!(*res_mutex.lock().unwrap(), 800);

字符:单字符:char,4字节,汉字占用3字节。
有限字符用array
不定用Vec::new ()或vec![ ],Vec
宽字符用String,可以.chars(),其它的索引都是字节,需要汉字索引在3的倍数。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值