Go学习第九章——面向“对象”编程(三大特性与接口和断言)

Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样,下面进行详细的讲解 Golang 的三大特性是如何实现的。

1. 封装

封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法).才能对字段进行操作

1.1 介绍

封装的理解和好处

  1. 隐藏实现细节
  2. 可以对数据进行验证,保证安全合理。

如何体现封装

  1. 对结构体中的属性进行封装
  2. 通过方法实现封装

封装的实现步骤

  1. 将结构体、字段(属性)的首字母小写(不能导出,其他包不能使用,类似private)。
  2. 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数。
  3. 提供一个首字母大写的Set方法(类似其他语言的public),用于对属性判断并赋值
func(var 结构体类型名) SetXxx(参数列表) (返回值列表) {
	// 加入数据验证的业务逻辑
	var.字段 = 参数
}
  1. 提供一个首字母大写的Get方法(类似其他语言的public),用于获取属性的值
func(var 结构体类型名) GetXxx(参数列表) (返回值列表) {
    return var.字段
}

**特别说明:**在Golang开发中并没有特别强调封装,这点并不像Java。所以提醒学过java的朋友,不用总是用java的语法特性来看待Golang,Golang本身对面向对象的特性做了简化的。

1.2 快速入门

案例入门:

设计一个person.go,不能随便查看人的年龄,工资等隐私,并对输入的年龄进行合理的验证。设计:model包(person.go),main包(main.go)。

person.go文件代码如下:

type person struct {
	Name string
	age  int //其它包不能直接访问..
	sal  float64
}

// 写一个工厂模式的函数,相当于构造函数
func NewPerson(name string) *person {
	return &person{
		Name: name,
	}
}

// 为了访问age 和 sal 我们编写一对SetXxx的方法和GetXxx的方法
func (p *person) SetAge(age int) {
	if age > 0 && age < 150 {
		p.age = age
	} else {
		fmt.Println("年龄范围不正确..")
		//给程序员给一个默认值
	}
}

func (p *person) GetAge() int {
	return p.age
}

func (p *person) SetSal(sal float64) {
	if sal >= 3000 && sal <= 30000 {
		p.sal = sal
	} else {
		fmt.Println("薪水范围不正确..")

	}
}

func (p *person) GetSal() float64 {
	return p.sal
}

main.go代码

import (
	"GoStudy_Day1/model"
	"fmt"
)

func main() {
	p := model.NewPerson("Tom")
	p.SetAge(18)
	p.SetSal(5000)
	fmt.Println(p)
	fmt.Println(p.Name, " age =", p.GetAge(), " sal = ", p.GetSal())
}

输出结果:

&{Tom 18 5000}
Tom  age = 18  sal =  5000

有一定编程基础的,就能迅速懂啦!!这就不详细说了,over~~

2.继承

2.1 介绍

继承可以解决代码复用,让我们的编程更加靠近人类思维。

当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体,在该结构体定义这些相同1属性和方法。

image-20231025221059766

其他的结构体不需要重新定义这些属性和方法,只需要嵌套一个Student匿名结构体即可。

**也就是说:**在Golang中,如果一个struct嵌套了另一匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。

基本语法

type Goods struct {
    Name string
    Price int
}

type Book struct {
    Goods // 这里就是嵌套匿名结构体Goods
    Writer string
}
2.2 快速入门

编写一个学生考试系统,简单化,有小学生和大学生

Student 学生

type Student struct {
    Name string
    Age int
    Score int
}

//将Pupil 和 Graduate 共有的方法也绑定到 *Student
func (stu *Student) ShowInfo() {
    fmt.Printf("学生名=%v 年龄=%v 成绩=%v\n", stu.Name, stu.Age, stu.Score)
}
func (stu *Student) SetScore(score int) {
    //业务判断
    stu.Score = score
}

//给 *Student 增加一个方法,那么 Pupil 和 Graduate都可以使用该方法
func (stu *Student) GetSum(n1 int, n2 int) int {
    return n1 + n2
}

Pupil 小学生

//小学生
type Pupil struct { 
    Student //嵌入了Student匿名结构体
}

//显示他的成绩

//这时Pupil结构体特有的方法,保留
func (p *Pupil) testing() {
    fmt.Println("小学生正在考试中.....")
}

Graduate 大学生

//大学生
type Graduate struct {
    Student //嵌入了Student匿名结构体
}

//显示他的成绩
//这时Graduate结构体特有的方法,保留
func (p *Graduate) testing() {
    fmt.Println("大学生正在考试中.....")
}

main方法

func main() {

    //当我们对结构体嵌入了匿名结构体使用方法会发生变化
    pupil := &Pupil{}
    pupil.Student.Name = "tom~"
    pupil.Student.Age = 8
    pupil.testing() 
    pupil.Student.SetScore(70)
    pupil.Student.ShowInfo()
    fmt.Println("res=", pupil.Student.GetSum(1, 2))


    graduate := &Graduate{}
    graduate.Student.Name = "mary~"
    graduate.Student.Age = 28
    graduate.testing() 
    graduate.Student.SetScore(90)
    graduate.Student.ShowInfo()
    fmt.Println("res=", graduate.Student.GetSum(10, 20))
}

输出结果:

小学生正在考试中.....
学生名=tom~ 年龄=8 成绩=70
res= 3
大学生正在考试中.....
学生名=mary~ 年龄=28 成绩=90
res= 30

继承给编程带来的便利性

  1. 代码的复用性提高了
  2. 代码的扩展性和维护性提高了
2.3 深入学习
  1. 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用。

  2. 匿名结构体字段访问可以简化:pupil.Student.Name = "tom~" ->pupil.Name = "tom~"

  3. 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如果希望访问匿名结构体的字段和方法,可以通过你们结构体名来区分。

​ 假设相同的Hello()方法:使用就近原则b.Hello(),使用匿名结构体b.A.Hello(),这样就OK~

  1. 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法**(同时结构体本身没有同名的字段和方法)**,在访问时,就必须明确指定匿名结构体名字,否则编译报错。
type A struct {
	Name string
	Age  int
}

type B struct {
	Name string
	Age  int
}

type C struct {
	A
	B
}

func main() {
	//c := C{"Tom", 18} // 这里会报错
	c := C{A{"Tom", 11}, B{"Kevin", 12}}
	name1 := c.A.Name
	name2 := c.B.Name
	fmt.Printf("name1: %v, name2: %v", name1, name2)
}
  1. 如果一个struct嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段和方法时,必须带上结构体的名字。
type A struct {
	Name string
	Age  int
}

type B struct {
	a A
}

func main() {
	var b B
	// b.name = "jack" // 报错
	b.a.Name = "jack"
}
  1. 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值。

​ 这个的说法就是我第五条那个代码,就是直接指定值。

3.接口

按顺序,我们应该讲解多态,但是在讲解多态前,我们需要讲解接口(interface),因为在Golang中,多态特性主要是通过接口来体现的。

3.1 接口特点和语法说明

基本介绍

interface类型可以定义一组方法,但是这些不需要实现。并且interface不能包含任何变量。到某个自定义类型(比如结构体Phone)要使用的时候,再根据具体情况把这些方法写出来。

基本语法

image-20231025230455914

小结说明:

  1. 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低耦合的思想。
  2. Golang中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个遍历就实现这个接口。因此,Golang中没有implement这样的关键字。
3.2 快速入门

案例入门:

声明一个usb接口,然后手机,相机,电脑结构体实现了这个usb接口

声明接口

//声明/定义一个接口
type Usb interface {
    //声明了两个没有实现的方法
    Start() 
    Stop()
}

手机Phone

type Phone struct {

}  

//让Phone 实现 Usb接口的方法
func (p Phone) Start() {
    fmt.Println("手机开始工作。。。")
}
func (p Phone) Stop() {
    fmt.Println("手机停止工作。。。")
}

相机Camera

type Camera struct {

}
//让Camera 实现   Usb接口的方法
func (c Camera) Start() {
    fmt.Println("相机开始工作~~~。。。")
}
func (c Camera) Stop() {
    fmt.Println("相机停止工作。。。")
}

计算机Computer

//计算机
type Computer struct {

}

//编写一个方法Working 方法,接收一个Usb接口类型变量
//只要是实现了 Usb接口 (所谓实现Usb接口,就是指实现了 Usb接口声明所有方法)
func (c Computer) Working(usb Usb) {

    //通过usb接口变量来调用Start和Stop方法
    usb.Start()
    usb.Stop()
}

测试

func main() {

	//测试
	//先创建结构体变量
	computer := Computer{}
	phone := Phone{}
	camera := Camera{}

	//关键点
	computer.Working(phone)
	computer.Working(camera) //
}

这个案例有点点抽象,但是使用方式就是这么一回事,多看几遍

3.3 注意事项和细节说明
  1. 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)。
type AInterface interface {
	Say()
}

type Stu struct {
	Name string
}

func (stu Stu) Say() {
	fmt.Println("Stu Say()")
}

func main() {
	var stu Stu
	var a AInterface = stu
	a.Say()
}
// 输出:Stu Say()
  1. 接口中所有的方法都没有方法体,即都是没有实现的方法。

  2. 在 Golang 中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口。

  3. 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型。

  4. 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。

  5. 一个自定义类型可以实现多个接口

    type AInterface interface {
    	Say()
    	
    }
    
    type BInterface interface {
    	Hello()
    }
    
    type Monster struct {
    	
    }
    
    func (m Monster) Hello() {
    	fmt.Println("Monster Hello()~~~")
    }
    
    func (m Monster) Say() {
    	fmt.Println("Monster Say()~~~")
    }
    
    func main() {
    	// Monster实现了AInterface和BInterface
    	var monster Monster
    	var a1 AInterface = monster
    	var b1 BInterface = monster
    	a1.Say()
    	b1.Hello()
    	
    }
    
    1. Golang接口中不能有任何变量

    2. 一个接口(比如A接口)可以继承多个别的接口(比如B,C接口),这时如果要实现A接口,也必须将B,C接口的方法也全部实现。

    3. interface 类型默认是一个指针(引用类型),如果没有对interface初始化就使用,那么会输出 nil。

    4. 空接口interace{}没有任何方法,所以所有类型都实现了空接口,即我们可以把任何一个变量
      赋给空接口。

3.4 接口和继承关系

接口和继承解决的解决的问题不同

  1. 继承的价值主要在于:解决代码的复用性和可维护性。
  2. 接口的价值主要在于:设计,设计好各种规范(方法),让其它自定义类型去实现这些方法.
  3. 接口比继承更加灵活:Person Student BirdAble LittleMonkey
    接口比继承更加灵活,继承是满足 is-a 的关系,而接口只需满足 like- a 的关系。
  4. 接口在一定程度上实现代码解耦

在Go语言中,接口和继承是两个非常重要的特性。不过,Go语言并不是面向对象的语言,因此其实现方式和其他语言有一些不同。在 Go 中,并没有传统意义上的继承机制,但是可以通过嵌入结构体来实现类似继承的效果。具体来说,可以通过在一个结构体中嵌入另一个结构体的方式,让前者继承后者的属性和方法。

补:Go语言中有一个非常重要的概念,叫做“组合”。组合是指在一个结构体或类型中嵌入另一个类型。当一个类型组合了另一个类型时,就可以直接使用被嵌入类型的属性和方法,就像使用自己的一样。

从使用场景上看待这两种方式:

接口可以把一个方法的实现和其它代码(包括参数和返回值)进行分离,实现代码的解耦。一个实现了某个接口的类型,能够被包含其接口类型的任何代码所使用;因此,接口也促进了代码的复用和模块化。比如,Go语言的标准库就广泛使用了接口。

组合则可以帮助我们实现代码的复用和扩展。通过嵌入一个类型到另一个类型中,就可以轻松地对其进行扩展,并继承其属性和方法。这比使用继承更加灵活和易于扩展。同时,组合也避免了类型继承导致的循环依赖和类型层次混乱的问题,使得代码更加清晰易懂。

4. 多态

多态是指同一种类型的实例,能够通过不同的方式处理数据或行为的能力。在面向对象编程中,多态是一个非常重要的概念。它既提高了代码的可扩展性和复用性,也促进了代码之间的解耦和模块化。

在Go语言中,实现多态的主要方式是通过接口实现。接口定义了一组方法的约束,实现了某个接口的类型,即使这些类型具有不同的内部实现,但却能够以相同的方式被其他代码调用,这就是多态的体现。

4.1 基本概念

在Go语言中,一个接口是一个命名的方法集合。任何类型,只要它实现了接口中的所有方法,都可以看做是该接口类型的实例。这也就是接口实现多态的方式。

考虑下面这个代码示例:

package main

import "fmt"

type Animal interface {
    Eat()
    Sleep()
}

type Cat struct {
    name string
}

func (c *Cat) Eat() {
    fmt.Printf("%s is eating\n", c.name)
}

func (c *Cat) Sleep() {
    fmt.Printf("%s is sleeping\n", c.name)
}

type Dog struct {
    name string
}

func (d *Dog) Eat() {
    fmt.Printf("%s is eating\n", d.name)
}

func (d *Dog) Sleep() {
    fmt.Printf("%s is sleeping\n", d.name)
}

func main() {
    var a Animal

    a = &Cat{"Tom"}
    a.Eat()
    a.Sleep()

    a = &Dog{"Spike"}
    a.Eat()
    a.Sleep()
}

在这个示例中,我们定义了一个Animal接口,里面包含了两个方法EatSleep。接着,我们定义了两个类型CatDog,它们都实现了Animal接口中的方法。最后,我们在main函数中,使用接口类型变量a引用了CatDog两个类型的实例,并调用了它们的EatSleep方法。

这里,我们使用了接口类型变量来引用不同的类型的实例,实现了多态。虽然a变量的静态类型是Animal,但它的具体类型却可以是CatDog,并被以相同的方式调用其中定义的方法。

4.2 快速入门

在Go语言中,实现多态的过程就是实现接口的过程。具体步骤如下:

  1. 定义一个接口:定义一个接口,并在里面定义一些方法。
  2. 实现接口:定义一个类型,并实现接口中的方法。
  3. 实现多态:使用接口类型的变量,引用已经实现了该接口的类型的实例,并调用这个实例中定义的方法。

下面我们将在一个例子中进行快速入门:

package main

import "fmt"

type Shape interface {
    Area() float64
}

type Rect struct {
    width  float64
    height float64
}

func (r *Rect) Area() float64 {
    return r.width * r.height
}

type Circle struct {
    radius float64
}

func (c *Circle) Area() float64 {
    return 3.14 * c.radius * c.radius
}

func main() {
    var s Shape

    s = &Rect{4.0, 5.0}
    fmt.Printf("Rect area: %.2f\n", s.Area())

    s = &Circle{3.0}
    fmt.Printf("Circle area: %.2f\n", s.Area())
}

在这个示例中,我们定义了一个Shape接口,里面只有一个方法Area。然后,我们定义了两个类型RectCircle,它们都实现了Shape接口中的方法。

最后,在main函数中,我们用接口类型变量s分别引用了RectCircle两个类型的实例,并调用它们的Area方法。虽然s的静态类型是Shape,但在不同的实例中,Area方法的实现是不同的,这就是多态的体现。

4.3 使用场景

多态在Go语言中非常常用,特别是在面向接口编程的情况下。下面介绍几个多态的使用场景:

  1. 接口实现多态:如果一个类型实现了某个接口的所有方法,则可以把它看做是该接口的实例,这就可以通过接口类型变量对这个类型的实例进行操作。
  2. 组合实现多态:通过在一个类型中嵌入另一个类型,并重写嵌入类型的一些方法,就可以实现继承和多态。
  3. 接口组合实现多态:通过将多个接口组合到一起,实现一个大的接口,其他类型只要实现了这个大的接口,就可以看做是该大接口的实例,从而实现多态。
  4. 方法集实现多态:通过方法集的限制,即一个类型只能实现了某个接口的部分方法,仍然可以被看做是该接口的实例。

总之,多态是Go语言中非常重要的概念,它帮助我们实现代码的解耦、复用和扩展,大大提升了代码的可维护性和可扩展性。开发者在编写代码时应该充分考虑多态的使用场景,以实现更加健壮和可维护的代码。

5. 断言

在Go语言中,断言是一种判断接口是否能够接收到指定类型的值并进行相应处理的方式。

在编写代码时,我们通常是通过接口来实现多态性,但多态性也会带来一些麻烦,比如我们需要对接口中的具体值进行类型判断,这时就需要用到断言。

在Go语言中,有两种形式的断言:类型断言和表达式断言。

需求:如何将一个接口变量,赋给自定义类型的变量 => 引出类型断言

image-20231026000352476

5.1 类型断言

类型断言用于判断接口中的具体值是否为指定类型,并进行相应的类型转换。

类型断言可以分为无需判断需判断两种形式。

  1. 无需判断的类型断言

无需判断的类型断言需要明确知道断言的类型,常见的形式如下:

value.(Type)

其中,value 为接口类型的值,Type 为要转换的类型,并返回转换后的值。

下面是一个无需判断的类型断言的示例:

// value 是一个接口类型值
if v, ok := value.(int); ok {
    fmt.Printf("value 是一个整型,值为 %d\n", v)
} else {
    fmt.Println("value 不是一个整型")
}

在上面的代码中,我们想知道 value 是否为 int 类型,如果是,就将其转换成 int 类型的值,并按照指定格式打印出来。如果不是,就提示用户它不是一个整型。

  1. 需判断的类型断言

需判断的类型断言可能需要在判断之前执行某些操作,如下所示:

switch v := value.(type) {
    case Type1:
        /* 做 Type1 类型的操作,v 是 Type1 类型的值 */
    case Type2:
        /* 做 Type2 类型的操作,v 是 Type2 类型的值 */
    ...
    default:
        /* 做默认处理 */
} 

在这种情况下,value.(type) 是一个表达式,返回的不再是一个指定类型的值,而是一个interface{}类型的变量v,需要在case语句中再进行断言。

下面是一个需判断的类型断言的示例:

// value 是一个接口类型值
switch v := value.(type) {
case int:
    fmt.Printf("value 是一个整型,值为 %d\n", v)
case float64:
    fmt.Printf("value 是一个浮点数,值为 %f\n", v)
case string:
    fmt.Printf("value 是一个字符串,值为 %s\n", v)
default:
    fmt.Printf("value 是一个未知类型,值为 %v\n", v)
}

在上面的示例中,我们使用了 switch 语句来进行判断,根据不同的类型进行相应的处理。

5.2 表达式断言

表达式断言用于判断接口中的具体值是否为 nil,形式如下:

value.(type) == nil

其中,value 为接口类型的值,当具体的值为nil时返回true,否则返回false

下面是一个表达式断言的示例:

// value 是一个接口类型值
if value == nil {
    fmt.Println("value 是 nil")
} else {
    fmt.Printf("value 不是 nil,值为 %v\n", value)
}

在上面的示例中,我们使用了 if 语句来进行判断,当 value 为 nil 时,执行相应的处理。

5.3 最佳实践-案例1

下面会给出一段代码,是前面接口的案例,注意一些细节:

  1. phone有一个特有的方法call()
  2. computer的working方法,如果使用phone.Call()会报错,这时候就需要使用断言
//声明/定义一个接口
type Usb interface {
    //声明了两个没有实现的方法
    Start()
    Stop()
}

type Phone struct {
    name string
}  

//让Phone 实现 Usb接口的方法
func (p Phone) Start() {
    fmt.Println("手机开始工作。。。")
}
func (p Phone) Stop() {
    fmt.Println("手机停止工作。。。")
}

func (p Phone) Call() {
    fmt.Println("手机 在打电话..")
}


type Camera struct {
    name string
}
//让Camera 实现   Usb接口的方法
func (c Camera) Start() {
    fmt.Println("相机开始工作。。。")
}
func (c Camera) Stop() {
    fmt.Println("相机停止工作。。。")
}

type Computer struct {

}

func (computer Computer) Working(usb Usb) {
    usb.Start()
    //如果usb是指向Phone结构体变量,则还需要调用Call方法
    //类型断言..[注意体会!!!]
    if phone, ok := usb.(Phone); ok {
       phone.Call()
    }
    usb.Stop()
}

func main() {
    //定义一个Usb接口数组,可以存放Phone和Camera的结构体变量
    //这里就体现出多态数组
    var usbArr [3]Usb
    usbArr[0] = Phone{"vivo"}
    usbArr[1] = Phone{"小米"}
    usbArr[2] = Camera{"尼康"}

    //遍历usbArr
    //Phone还有一个特有的方法call(),请遍历Usb数组,如果是Phone变量,
    //除了调用Usb 接口声明的方法外,还需要调用Phone 特有方法 call. =》类型断言
    var computer Computer
    for _, v := range usbArr{
       computer.Working(v)
       fmt.Println()
    }
    //fmt.Println(usbArr)
}

输出结果:

手机开始工作。。。
手机 在打电话..
手机停止工作。。。

手机开始工作。。。
手机 在打电话..
手机停止工作。。。

相机开始工作。。。
相机停止工作。。。

这里会发现,call()正好只有是手机才会有这个功能,牛掰!!

5.4 最佳实践-案例2

写一个函数,循环判断传入参数的类型

//定义Student类型
type Student struct {

}

//编写一个函数,可以判断输入的参数是什么类型
func TypeJudge(items... interface{}) {
	for index, x := range items {
		switch x.(type) {
		case bool :
			fmt.Printf("第%v个参数是 bool 类型,值是%v\n", index, x)
		case float32 :
			fmt.Printf("第%v个参数是 float32 类型,值是%v\n", index, x)
		case float64 :
			fmt.Printf("第%v个参数是 float64 类型,值是%v\n", index, x)
		case int, int32, int64 :
			fmt.Printf("第%v个参数是 整数 类型,值是%v\n", index, x)
		case string :
			fmt.Printf("第%v个参数是 string 类型,值是%v\n", index, x)
		case Student :
			fmt.Printf("第%v个参数是 Student 类型,值是%v\n", index, x)
		case *Student :
			fmt.Printf("第%v个参数是 *Student 类型,值是%v\n", index, x)
		default :
			fmt.Printf("第%v个参数是  类型 不确定,值是%v\n", index, x)
		}
	}
}

func main() {
	var n1 float32 = 1.1
	var n2 float64 = 2.3
	var n3 int32 = 30
	var name string = "tom"
	address := "北京"
	n4 := 300

	stu1 := Student{}
	stu2 := &Student{}

	TypeJudge(n1, n2, n3, name, address, n4, stu1, stu2)
}

输出结果:

0个参数是 float32 类型,值是1.11个参数是 float64 类型,值是2.32个参数是 整数 类型,值是303个参数是 string 类型,值是tom  
第4个参数是 string 类型,值是北京 
第5个参数是 整数 类型,值是3006个参数是 Student 类型,值是{}7个参数是 *Student 类型,值是&{}

Over~~!!!!终于干完了,想无,呜呜呜!!!冲冲冲冲!!!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值