重构C语言版(二) 技能基础篇: 简化逻辑结构

上一篇:https://blog.csdn.net/weixin_42523774/article/details/105619681

· 为何 简化逻辑结构 单独作为一篇阐述?
· 如果代码逻辑复杂,如何才能理清代码中的浮云,显现出其最原本的逻辑,为后续修改逻辑来铺平道路。 这就需要一系列的手法,我称之为 技能基础。而这些技能遵循二八法则,学到这20%,使用会占到80%。因此学习这些手法是很有必要的。
· 由于重构用到了很多面向对象的思维,如果对C语言面向对象编程不熟悉,请查看这篇《学会C语言面向对象编程,弄清面向对象实质。》
· 首先介绍一下四个名词的含义:
(1)提炼——增加代码逻辑的层次,增加代码逻辑的中间层;
(2)内联——减少代码逻辑的层次,干掉代码逻辑的中间层;
(3)组合——将代码放到一起,增加相关性;
(4)拆分——将代码分开放置,减少耦合。
· 下面逐步介绍各种方法:

1. 提炼函数

· 将诸多的代码提炼成一个函数,那么何时需要提炼呢?
· 如果你要花一段时间来理解代码的意图,你就需要将它提炼到一个函数中,让人一看到这个函数就知道它在做什么。
· 函数不要太大,我的经验是超过6行的函数就感觉有点多了,你可能会担心短函数会导致大量的函数调用,而影响性能,
· 但是现在短函数常常能让编译器的优化功能运转更为良好,因此不用过多担心性能问题。
· 小函数需要有好名字,你可以从注释中得到提示。
· 范例:

(1)无局部变量:这部分最简单,就是把一段无影响的功能提出来

比如有一行异常打印:

printf("error happened in func!\n");

提炼成:

void print_error(void) {
	printf("error happened in func!\n");
}
(2)有局部变量:

比如我想把这个func提出来,作为一个参数输入,就可以这样;
提炼成:

void print_error(const char *func_name) {
	printf("error happened in %s!\n", func_name);
}
(3)对局部变量再赋值:

· 这种情况就是函数修改的变量,需要在函数外使用;我们可以把参数作为输入,然后用返回值返回;
· 比如上面的打印函数需要记录打印出错的次数:
提炼成:

int print_error(const char *func_name) {
	static int count = 0;
	printf("error %d happened in %s!\n", ++count, func_name);
	return count;
}

· 如果需要返回的函数参数有多个,我建议可以用多个函数来提炼,保证只返回一个,如果实在是需要多个,建议通过封装对象(struct)的方式范返回。

2.内联函数

· 内联函数就是提炼函数的反向重构。
· 当函数的内部代码和函数名称同样清晰易读,也可能是你重构了函数实现,使其内容和其名称同样清晰时,这样就建议你直接使用其中的代码。
· 间接性可能带来帮助,但是非必要的间接性总是让人不舒服。通过内联手法,可以找出有用的间接层,也可以将无用的间接层去除。
做法:
找出函数的所有调用点,逐个替换。
范例:

(1)简单情况
int rating(struct Driver_info aDriver) {
	return moreThanFiveLateDeliveries(aDriver) ? 2 : 1;
}
int moreThanFiveLateDeliveries() {
	return aDriver.numberOfLateDeliveries > 5;
}

内联为:

int rating(struct Driver_info aDriver) {
	return aDriver.numberOfLateDeliveries > 5? 2 : 1;
}
(2)复杂情况:

当需要移出的函数内容很复杂时,你就需要"剪切-粘贴-调整"来进行,当调用函数众多,则需要调整一次就测试一次;
如果你遇到了麻烦,就意味着需要使用更精细的重构手法:搬移语句到调用者(217)。

3. 提炼变量

· 有时表达式可能非常难以阅读,这时,局部变量可以帮助我们将表达式分解为比较容易管理的形式,让我们理解这部分的逻辑是干什么的。
· 命名:如果考虑使用提炼变量,就意味着我要给代码中的一个表达式命名。
· 如果这个名字只在当前函数中有意义,提炼变量是个不错的选择。
· 如果这个命名,在更宽的上下文中,我就会考虑将其暴露出来,通常以函数的形式。
范例:

(1)简单计算提取
int price(struct goods order) {
	//price is base price - quantity discount + shipping
	return order.quantity * order.itemPrice - 
	max(0, order.quantity - 500) * order.itemPrice * 0.05 +
	min(order.quantity * order.itemPrice * 0.1, 100);
}

提炼出basePrice为:

int price(struct goods order) {
	//price is base price - quantity discount + shipping
	const int basePrice = order.quantity * order.itemPrice;
	return basePrice - max(0, order.quantity - 500) * order.itemPrice * 0.05 +
	min(basePrice * 0.1, 100);
}

再提炼出quantity discount为:

int price(struct goods order) {
	//price is base price - quantity discount + shipping
	const int basePrice = order.quantity * order.itemPrice;
	const int quantityDiscount = max(0, order.quantity - 500) * order.itemPrice * 0.05;
	return basePrice - quantityDiscount + min(basePrice * 0.1, 100);
}

最后提炼出shipping为,修改之后,注释也就不需要了:

int price(struct Order order) {
	const int basePrice = order.quantity * order.itemPrice;
	const int quantityDiscount = max(0, order.quantity - 500) * order.itemPrice * 0.05;
	const int shipping = min(basePrice * 0.1, 100);
	return basePrice - quantityDiscount + shipping;
}
(2)类对象提取

· 同样的内容,比如在类对象中,可以如下操作:
· C语言面向对象操作,见另一篇文章《学会C语言面向对象编程,弄清面向对象实质。》
顺带介绍一下:

struct Order {
	int data;
	struct OrderOperations *orderOp;
};
struct OrderOperations {
	int (*price)(struct Order *order);
};
int price(struct Order *order) {
	//price is base price - quantity discount + shipping
	return order->quantity * order->itemPrice - 
	max(0, order->quantity - 500) * order->itemPrice * 0.05 +
	min(order->quantity * order->itemPrice * 0.1, 100);
}
struct Order* alloc_Order(void) {
#define ORDER_DEFAULT_DATA 0
	static struct OrderOperations orderOp = {
		.price = price,
	};
	struct Order* order = (struct Order* )malloc(sizeof(struct Order));
	order->orderOp = orderOp;
	order->data = ORDER_DEFAULT_DATA;
	return order;
}
int main(int argc, char *argv[]) {
	struct Order* order = alloc_Order();
	printf("order price is %d.\n", order->orderOp->price(order));
	return 0;
}

提炼为:

struct OrderOperations {
	int (*getPrice)(struct Order *order);
	int (*getBasePrice)(struct Order *order);
	int (*getQuantityDiscount)(struct Order *order);
	int (*getShipping)(struct Order *order);
};
int getBasePrice(struct Order *order) {
	return order->quantity * order->itemPrice;
}
int getQuantityDiscount(struct Order *order) {
	return max(0, order->quantity - 500) * order->itemPrice * 0.05;
}
int getShipping(struct Order *order) {
	return min(order->quantity * order->itemPrice * 0.1, 100);
}
int getPrice(struct Order *order) {
	return order->getBasePrice(order) - order->getQuantityDiscount(order) + order->getShipping(order);
}
struct Order* alloc_Order(void) {
#define ORDER_DEFAULT_DATA 0
	static struct OrderOperations orderOp = {
		.getPrice = getPrice,
		.getBasePrice = getBasePrice,
		.getQuantityDiscount = getQuantityDiscount,
		.getShipping = getShipping,
	};
	struct Order* order = (struct Order* )malloc(sizeof(struct Order));
	order->orderOp = orderOp;
	order->data = ORDER_DEFAULT_DATA;
	return order;
}
int main(int argc, char *argv[]) {
	struct Order* order = alloc_Order();
	printf("order price is %d.\n", order->orderOp->price(order));
}

· 在一个简单的对象中暂时看不出太明显的好处,但是当这个对象很大的时候,如果找出了可以共用的行为,赋予它独立的概念,起个好名字,对于使用对象的人来说会很有帮助。

4.内联变量

· 虽然有时候,变量可以给表达式提供更有意义的名字;但是有时候,这个名字并不比表达式本身更有表现力,甚至妨碍重构附近的代码,这时就该通过内联手法消除变量。
范例:

int basePrice = anOrder.basePrice;
return (basePrice > 1000);

内联为:

return anOrder.basePrice > 1000;

5.改变函数声明

· 函数是我们将程序拆成小块的主要方式,而这中方式其中最重要的当属函数的名字。一个好名字能让我一眼看出函数的用途,而不必查看其实现代码。
· 如果看到函数名字满足不了上面的要求,一旦发现更好的名字,就得尽快给函数改名。对于函数的参数列表也是一样的道理。对函数的参数也是同理。
这里说的改变函数声明,包括函数改名,函数参数改名,函数参数增减等项。

1)简单做法(可以一步到位)

直接将函数修改为新的函数声明,然后测试。
如果你既想修改函数名,又想添加参数,最好分2步做。

2)迁移式做法(不能做到一步到位)

.1.如果有必要的话,可以先对函数体内部进行重构,使得后面的提炼步骤易于开展;
.2.先提炼函数,将函数提炼成一个新的函数,如果想沿用旧函数名,建议先给新函数起一个便于查找的临时名字;
.3.如果需要添加参数,就用之前的方式添加;
.4.测试;
.5.对旧函数使用内联函数,释放函数中的内容;
.6.如果新函数使用了临时名字,在此使用改变函数声明将其改回来。

范例:

(1)函数改名(简单做法)
int circum(int radius) {
	return 2 * PI * radius;
}

修改成:

int circumference(int radius) {
	return 2 * PI * radius;
}
(2)函数改名(迁移式做法)
int circum(int radius) {
	return 2 * PI * radius;
}

修改成:

int circum(int radius) {
	return circumference(radius);
}
int circumference(int radius) {
	return 2 * PI * radius;
}

然后测试,通过后使用内联函数手法。

(3)添加参数

迁移式做法类似,不在重复介绍。

(4)把参数修改为属性

迁移式做法类似,不在重复介绍。

6.封装变量

· 数据的重构相对于函数要麻烦得多。
· 如果想要搬移一处被广泛使用的数据,最好的办法是先以函数的形式封装所有对数据的访问。
· 对于所有的可变数据,只要它的作用域超过单个函数,我就会将其封装起来,只允许通过函数访问。数据的作用域越大,封装就越重要。处理遗留代码时,一旦需要修改或增加使用可变数据的代码,我就会借机把这份数据封装起来,从而避免继续加重耦合。面向对象的方法如此强调对象的数据应该保持私有,背后也是同样的原理。
· 相比于封装数据,不可变的数据更重要,不可变让大家可以放心使用旧数据,不用做搬移,不用担心代码失效。

范例:
(1)全局变量

赋值:

struct Owner defaultOwner = {
	.firstName = "Martin",
	.lastName = "Fowler",
};

使用:

owner = defaultOwner;

更新:

defaultOwner.firstName = "Rebecca";
defaultOwner.lastName = "Parsons";

· 重构第一步:封装成函数
· 获取值的时候,建议不用指针,返回的内容就是副本,这样原来数值不会被修改。获取时建议不加get。

struct Owner defaultOwner(void) {
	return defaultOwner;
}
void setDefaultOwner(struct Owner newOwner) {
	defaultOwner = newOwner;
	return;
}

· 重构第二步:修改变量限制
然后可以将原来的变量增加限制,比如加上 static 限制在此文件中使用,如果做不到,建议将变量取一个有意义有难看的名字。
例如 __privateOnly_defaultOwner 。本次将其改为

static struct Owner defaultOwnerData = {
	.firstName = "Martin",
	.lastName = "Fowler"
};

· 重构第三步:将其封装为一个对象,但是C语言不支持,因此封装成结构体是唯一选择。前面已经做好。

7.变量改名

· 好的命名是整洁编程的核心。变量名可以很好的解释这段程序在干啥——如果名字起的好的话。
· 使用范围越广,名字的好坏就越重要。
· 我习惯将变量的类型信息也放进名字里面,我的类型对于的名字前缀表:

char - c,
unsigned char - uc, 
short - s,
unsigned short - us
int - i,
unsigned int - ui,
struct - t或对应的类型名字,
union - u,
指针 - p,
enum - e,
数组 - a,
float - f,
double - d,

· 如果变量被广泛使用,建议使用封装变量的方法,将其封装起来。
· 常量改名,通常先复制这个常量,用新常量复制给旧的常量,这样删除就常量时会稍微快一点。
范例:此处前面变量封装例子类似,就不做此范例。

8.引入参数对象

· 当同一组数据项经常同时出现在多个函数的参数列表中时,我喜欢代之以一个数据结构,将其组合起来。
· 这件事情的价值,在于将数据项之间关系变得明晰,而进一步围绕该结构来捕捉共用行为,这个结构将提升为新的抽象概念。这个力量是强大的。
做法:
· 在该函数增加一个创建的数据结构的参数,然后一个一个的将参数项移到这个结构中,记得每一步都要保证测试通过。
范例:

int  readingsOutsideRange(int station, int min, int max) {
	return (station < min)? min : ((station > max)? max: station);
}

首先逐步重构为:

struct NumberRange {
	int station;
	int min;
	int max;
};
int  readingsOutsideRange(struct NumberRange range) {
	return (range.station < range.min)? range.min : ((range.station > range.max)? range.max: range.station);
}

对此结构中的数据提取最好封装成函数:

struct NumberRange {
	int station;
	int min;
	int max;
};
int min(struct NumberRange *range) {
	return range->min;
}
int max(struct NumberRange *range) {
	return range->max;
}
int  readingsOutsideRange(struct NumberRange range) {
	return (range.station < min(&range))? min(&range) : ((range.station > max(&range) )? max(&range): range.station);
}

9.函数组合成类

将数据组合起来,将数组的操作通过函数也放进来。

· 函数组合成类是将函数重新组织的一种方式,当一组函数形影不离地操作同一块数据,这是就该组件一个类了。一般来说,类可以提供一套环境,可以让我们的函数少传许多参数,而一个对象也可以更方便的传递给系统的其他部分。只是在C语言中,没有面向对象的支持,但是仍然可以将其组合成结构体使用。
做法:
(1)运用封装记录 对多个函数共用的数据记录加以封装;
(2)对于使用该记录结构的每个函数,运用搬移函数 将其移入新类;
(3)用以处理数据记录的逻辑,可以用提炼函数 的方法提炼出,并移入新类。
范例:
· 延续运用第8节中的例子,上一节中使用了如下代码

struct NumberRange {
	int station;
	int min;
	int max;
	int (*getMin)(struct NumberRange *range);
	int (*getMax)(struct NumberRange *range);
};
int min(struct NumberRange *range) {
	return range->min;
}
int max(struct NumberRange *range) {
	return range->max;
}
struct NumberRange* allocNumberRange(void) {
    static struct OrderOperations orderOp = {
        .getMin = min,
		.getMax = max,
    };
    struct NumberRange* range = (struct NumberRange* )malloc(sizeof(struct NumberRange));
    range->min = ORDER_DEFAULT_DATA;
    range->max = ORDER_DEFAULT_DATA;
    range->getMin = min;
	range->getMax = max;
    return range;
}

int readingsOutsideRange(struct NumberRange *range) {
	return (range->station < min(range))? min(range) : ((range->station > max(range) )? max(range): range->station);
}
int main(void **argc,void *argv[]) {
	struct NumberRange* range = allocNumberRange();
	printf("OutsideRange is %d.\n", readingsOutsideRange(range));
}

· 我想把 readingsOutsideRange函数搬移到 NumberRange 的内部;

struct NumberRange {
    int station;
    int min;
    int max;
    int (*getMin)(struct NumberRange *range);
    int (*getMax)(struct NumberRange *range);
    int (*readingsOutsideRange)(struct NumberRange *range);/*添加*/
};
static int min(struct NumberRange *range) {
    return range->min;
}
static int max(struct NumberRange *range) {
    return range->max;
}
static int _readingsOutsideRange(struct NumberRange *range) {
    return (range->station < min(range))? min(range) : ((range->station > max(range) )? max(range): range->station);
}
struct NumberRange* allocNumberRange(void) {
#define ORDER_DEFAULT_DATA 1
    struct NumberRange* range = (struct NumberRange* )malloc(sizeof(struct NumberRange));
    range->min = ORDER_DEFAULT_DATA;
    range->max = ORDER_DEFAULT_DATA;
    range->getMin = min;
    range->getMax = max;
    range->readingsOutsideRange = _readingsOutsideRange;/*添加*/
    return range;
}


int main(void **argc,void *argv[]) {
    struct NumberRange* range = allocNumberRange();
    range->station = 3;
    printf("OutsideRange is %d.\n", range->readingsOutsideRange(range));/*修改*/
}

10.函数组合成变换

将数据组合起来,将各种函数的操作集合成一个函数。

· 我们经常会将数据"喂"给一个程序,让它产生很多派生信息。这些派生数据可能在多个不同的地方用到,而计算的逻辑也就会在多个地方重复。
我更愿意将所有计算派生数据的逻辑收拢到一处,这样可以在固定的地方找到和更新这些逻辑,避免重复。
· 本方案的替代方案是 函数组合成类,如何判断使用哪一个呢?如果代码中会对源数据进行更新,那么使用类要好得多。使用变换的话,源数据更新之后会导致与派生数据不一致。
使用方式:
(1)创建一个变化函数,入参是需要变换的记录,并直接返回记录的值。
(2)选择一块逻辑,将主体移入该函数中,并将结果添加到输出记录中。
(3)测试并重复上述步骤。

范例:
延续使用第8节的例子,用函数组合成变换的方式进行重构。

struct NumberRange {
	int station;
	int min;
	int max;
};
int min(struct NumberRange *range) {
	return range->min;
}
int max(struct NumberRange *range) {
	return range->max;
}
int  readingsOutsideRange(struct NumberRange range) {
	return (range.station < min(&range))? min(&range) : ((range.station > max(&range) )? max(&range): range.station);
}
/*新增一个获取范围宽度的函数*/
int  readingsRangeWidth(struct NumberRange range) {
	return max(&range) - min(&range);
}

· 首先,readingsOutsideRange 和 readingsRangeWidth 的两个函数是获取的扩展信息,首先将扩展信息放入struct中,然后创建一个计算扩展信息的函数,并逐步把函数移到这里。

struct NumberRange {
	int station;
	int min;
	int max;
	/*扩展信息*/
	int outsideRange;
	int rangeWidth;
};

struct NumberRange enrichReadings(struct NumberRange range) {
	range.outsideRange = readingsOutsideRange(range);
	range.rangeWidth = readingsRangeWidth(range);
	return range;
}

· 这里要记住,不能一步到位,没改一步都要测试,调用的位置要非常仔细。

11.拆分阶段

学会了组合,也要学会拆分。每当同一块代码在同时处理两件不同的事情,我就想将其拆分成独自的模块,这是运用解耦的思想。因为到了要修改的时候,我就可以单独处理每个主题而不用考虑两个不同的主题。
最简洁的方法:将一大块行为分成顺序执行的两个阶段。如果数据不符合要求,你可能需要先对输入数据做调整。
做法:
(1)将第二阶段的代码提炼成独立的函数,并测试。(假设只有2段需要拆分的逻辑)
(2)引入一个中转数据结构,将其作为参数添加到提炼的新函数参数列表中,并测试。
(3)判断入参是否被第一阶段代码使用,是就将入参逐步搬移到 中转数据结构中,注意每次搬移都要测试一次;
(4)对第一阶段的代码运用提炼函数,将提炼出的函数返回中转数据结构。
范例:
这里有一个计算价格的函数:

struct Product {
	int basePrice;
	int discountThreshold;
	int discountRate;
};
struct ShippingMethod {
	int discountedFee;
	int feePerCase;
	int discountThreshold;
};
int priceOrder(struct Product product, int quantity, struct ShippingMethod shippingMethod) {
	const int basePrice = product.basePrice * quantity;
	const int discount = max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
	const int shippingPerCase = (basePrice > shippingMethod.discountThreshold)? shippingMethod.discountedFee : shippingMethod.feePerCase;
	const int shippingCost = quantity * shippingPerCase;
	int price = basePrice - discount + shippingCost;
	return price;
}

· 该函数中有点混乱,需要逐步拆分,首先拆出shipping配送相关的部分;

int priceOrder(struct Product product, int quantity, struct ShippingMethod shippingMethod) {
	int basePrice = product.basePrice * quantity;
	int discount = max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
	int price = applyShipping(basePrice, shippingMethod, quantity, discount);
	return price;
}
int applyShipping(int basePrice, struct ShippingMethod shippingMethod, int quantity, int discount) {
	int shippingPerCase = (basePrice > shippingMethod.discountThreshold)? shippingMethod.discountedFee : shippingMethod.feePerCase;
	int shippingCost = quantity * shippingPerCase;
	int price = basePrice - discount + shippingCost;
	return price;
}

· 增加一个结构,审视各个参数,将函数参数逐步放入。shippingMethod第一阶段没用到,可以不放,quantity这个参数可以继续选择放不放,我还是想尽可能放进去,得到如下代码。

struct PriceData {
	int basePrice;
	int quantity;
	int discount;
};
int priceOrder(struct Product product, int quantity, struct ShippingMethod shippingMethod) {
	int basePrice = product.basePrice * quantity;
	int discount = max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
	struct PriceData priceData = {basePrice, quantity, discount};/*新建结构体*/
	int price = applyShipping(priceData, shippingMethod/*, basePrice, quantity, discount*/);
	return price;
}
int applyShipping(struct PriceData priceData, struct ShippingMethod shippingMethod/*, int basePrice, int quantity, int discount*/) {
	int shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)? shippingMethod.discountedFee : shippingMethod.feePerCase;
	int shippingCost = priceData.quantity * shippingPerCase;
	int price = priceData.basePrice - priceData.discount + priceData.shippingCost;
	return price;
}

· 最后将第一阶段也组合成一个函数:

struct PriceData {
	int basePrice;
	int quantity;
	int discount;
};
/*新的函数,计算出priceData*/
struct priceData caculatePriceData(struct Product product, int quantity) {
	struct PriceData priceData;
	priceData.basePrice = product.basePrice * quantity;
	priceData.discount = max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
	priceData.quantity = quantity;
	return priceData;
}
int priceOrder(struct Product product, int quantity, struct ShippingMethod shippingMethod) {
	/*int basePrice = product.basePrice * quantity;
	int discount = max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;*/
	struct PriceData priceData = caculatePriceData(product, quantity);{basePrice, quantity, discount};/*新建结构体*/
	int price = applyShipping(priceData, shippingMethod/*, basePrice, quantity, discount*/);
	return price;
}
int applyShipping(struct PriceData priceData, struct ShippingMethod shippingMethod/*, int basePrice, int quantity, int discount*/) {
	int shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)? shippingMethod.discountedFee : shippingMethod.feePerCase;
	int shippingCost = priceData.quantity * shippingPerCase;
	int price = priceData.basePrice - priceData.discount + priceData.shippingCost;
	return price;
}

最后去掉注释,整理下,得最终结果。


struct PriceData {
	int basePrice;
	int quantity;
	int discount;
};
int priceOrder(struct Product product, int quantity, struct ShippingMethod shippingMethod) {
	struct PriceData priceData = caculatePriceData(product, quantity)return applyShipping(priceData, shippingMethod/*, basePrice, quantity, discount*/);
}
/*第一阶段计算出priceData*/
struct priceData caculatePriceData(struct Product product, int quantity) {
	struct PriceData priceData;
	priceData.basePrice = product.basePrice * quantity;
	priceData.discount = max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
	priceData.quantity = quantity;
	return priceData;
}
/*第二阶段计算出price*/
int applyShipping(struct PriceData priceData, struct ShippingMethod shippingMethod) {
	int shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)? shippingMethod.discountedFee : shippingMethod.feePerCase;
	int shippingCost = priceData.quantity * shippingPerCase;
	return priceData.basePrice - priceData.discount + priceData.shippingCost;
}

12. 总结

· 本文介绍了简化逻辑结构的各种方法,主要思想就是用 2 对武器 {提炼,内联},{组合,拆分}。就像2把快刀,将程序修剪得整整齐齐。这2对武器大部分情况下都是够用的了。
· 学到东西,就要去实践,实践中出现的问题,也请大家给我留言反馈,谢谢!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值