Kotlin - 面向对象(下)

概览:

  • 扩展:Kotlin提供了扩展机制,通过扩展机制可以弥补Java作为静态语言灵活性不足的问题;

  • Koltin默认为所有类、方法、属性都提供了final关键字来修饰,这意味着在默认情况下,Kotlin的类不可派生子类、方法、属性不可被重写,为了取消默认的final修饰符,Kotlin提供了final的反义词:open。

  • 与Java内部类相似,Kotlin提供了嵌套类和内部类。嵌套类相当于Java的静态内部类;而内部类则相当于Java的非静态内部类。

  • 对象表达式 = 》 Java的匿名内部类;对象表达式是增强版的匿名内部类,匿名内部类只能指定一个父类型,而对象表达式可以指定多个父类型;

  • 对象声明 = 》单例模式;

  • 伴生对象 = 》 静态成员;

  • Kotlin的类委托和属性委托机制,通过属性委托,Kotlin可以实现延迟属性,属性监听等现代化编程语言普遍支持的功能。

1、扩展

Kotlin支持扩展方法和扩展属性。

1.1、扩展方法

fun 类名.方法名(..)[:...]{

}

可以看出,与定义普通的方法没太大的区别,只不过是在方法名前,加上“类名.”而已。

示例:

class Raw{

fun test(){

println("test 方法")

}}

fun Raw.info(){

println("info方法")

}

程序为Raw类扩展了info方法之后,就像为Raw类增加了info()方法一样,所有的Raw对象都可以调用info方法。不仅如此,Raw类的子类的实例也可调用info方法。

扩展不仅可以用在我们自定义的类上面,也可以用在系统为我们提供的类上面进行方法的扩展。

1.2、扩展的实现机制

Java是一门静态语言。一个类被定义完成之后,程序无法动态的为该类增加、删除成员(field、方法等),除非开发者重新编辑该类的源代码,并重新编译该类。

但Kotlin却可以做到,难道Kotlin可以突破JVm的限制?

实际上,Kotlin的扩展并没有真正地修改所扩展的类,被扩展的类还是原来的类,没有任何的改变。Kotlin扩展的本质就是定义了一个函数,当程序用对象调用扩展方法时,Kotlin在编译时会执行静态解析 - 就是根据调用对象、方法名找到扩展函数,转换为函数调用。

比如:

strList.shuffle()

Kotlin在编译时这行代码的执行步骤如下:

  • 检查strList,发现其是List<String>类型;

  • 检查List<String>本身是否定义了shuffle()方法,如果类本身包含了该方法,则Kotlin无需进行处理,直接编译即可;

  • 如果List<String>类本身不包含shuffle(0方法,则Kotlin会查找程序是否为List<String>扩展了该方法 - 即查找系统中是否包含了名为List<String>.shuffle()的函数(或泛型函数)定义。如果找到了该函数,则Kotlin编译器就会执行静态解析,它会将上面的代码替换成执行List<String>.shuffle()函数;

  • 如果找不到,则编译器报错;

由此可见,Kotlin并没有真正的去扩展类,当Kotlin程序去调用扩展方法的时候,Kotlin编译器会将这行代码静态解析为调用函数,这样JVM就可以接受了。

==》这意味着调用扩展方法是由其所在表达式的编译时类型决定,而不是由它所在表达式的运行时类型决定的。

open class Base

class Sub:Base()

fun Base.foo(){

println("调用了Base的foo")}

fun Sub:foo(){

println("调用了Sub的foo")}

fun invoke(b:Base){

b.foo()

}

invoke(Sub())

==>执行结果:调用了Base的foo,Why?因为b的编译时类型是Base.

总结:成员方法执行动态解析(由运行时类型决定);扩展方法执行静态解析(由编译时类型决定)。

由前面介绍的Kotlin编译时解析一个成员方法和扩展方法的步骤,可知成员方法的优先级高于扩展方法。即如果一个类包含了相同签名的成员方法和扩展方法,当程序调用这个方法的时候,系统总会先执行成员方法,而不是扩展方法。

扩展与类、接口一样,如果扩展定义在其他包中,一样需要使用import进行导包。

Java调用Kotlin的扩展方法:

Raw t = new Raw();

Raw.foo(t);//调用Raw对象的扩展方法,需要自己解析成调用扩展函数

区别:Kotlin编译器本身支持静态解析,因此程序可以直接的调用对象的扩展方法;而Java编译器本身不支持静态解析,因此,需要开发者写代码的时候,做好静态解析。

1.3、可空类型扩展方法、扩展属性

a)为可空类型扩展方法,由于可空类型允许接受null值,这样使得null值也可调用该扩展方法,因此需要程序在扩展方法中处理null值的情形。

fun Any?.equals(other:Any?):Boolean{

if(this==null){

if(other==null) return true else false

return other.equals(this)

}

b)扩展属性

扩展属性的本质是通过增加get/set方法来实现的,没有幕后字段。简单说,扩展属性只能是计算属性。

由于Kotlin的扩展属性只能是计算属性,因此对扩展属性有如下两个限制:

I)扩展属性不能有初始值;

II)不能有field字段显示访问扩展属性;

III)扩展只读属性必须提供getter方法,扩展读写属性,必须提供getter/setter方法;

var User.fullName:String

get() = "${first}.${last}"

set(value){

if(".".!in value || value.indexOf(".")!=value.lastIndexOf(".")){

println(""输入的名字不合法)

}else{

var tokens = value.split(".")

first = tokens[0]

last = tokens[1]

}}

1.4、以成员的方式定义扩展

前面都是以顶层函数的形式进行定义扩展的,因此这些扩展可以直接的使用。Kotlin还支持以类成员的方式定义扩展 - 就像为类定义方法、属性那样定义扩展。

对于以类的成员方式定义的扩展:一方面它属于被扩展的类,因此可以在扩展方法(属性)中直接调用被扩展的类的成员;另一方面它又位于类体中,因此在扩展方法(属性)中又可直接调用它所在类的成员。

class A{

fun bar() = println("A 的 bar方法")

}

class B{

fun baz() = println("B  的baz方法")

fun A.foo(){

//在该方法中,既可以调用类A的成员,也可以调用类B的成员

bar()

baz()

}}

这样就产生了一个问题:如果被扩展类和扩展定义所在的类包含同名的方法,系统是怎么处理的?

==》系统总是优先调用被扩展类的方法,为了让系统调用扩展类所在类的方法,必须使用带标签的this进行限定。this@类名。

1.5、带接收者的匿名函数(为类扩展匿名函数)

在这种情况下,该扩展函数所属的类也是该函数接收者。

与普通扩展方法不同的是:去掉被扩展类的类名和点之后的函数名,其他部分没有太大的区别。

与普通扩展方法相似的是,带接收者的匿名函数(相当于扩展匿名函数)也允许在函数内访问接收者对象的成员。

 var fac = fun Int.():Int{

......

}

fun main(args:Array<String>){

println(6.fac())

}

与普通函数相同,带接收者的匿名函数也有自身的类型,比如上面的例子,则该函数的类型是:

Int.()->Int

如果接收者类型可通过上下文推断出来,那么Kotlin允许使用Lambda表达式作为带接收者的匿名函数。

class HTML{

fun body(){

}

fun head(){

}}

fun html(init:HTML.()->Unit){

val html = HTML()

html.init()

}

==>调用:

html{//如果调用函数只有一个Lambda表达式参数,则可以省略调用函数的圆括号,将Lambda表达式放在函数之外即可

head()//带接收者的匿名函数可以访问所属类的成员

body()

}

1.6、何时使用扩展呢?

扩展的作用:

a)扩展可动态地为已有的类添加方法或属性。尤其是系统的类。【如果使用Java就只能通过派生子类来实现,但一方面派生子类是有限制的,另一方面性能开销也是比较大的】

b)扩展能以更好的形式组织一些工具方法。

扩展是一种非常灵活的动态机制,它既不需要使用继承,也不需要使用类似装饰者的任何设计模式,即可为现有的类增加功能,因此使用非常方便。

2、final和open修饰符

  • Kotlin的final不能用于修饰局部变量;

  • Kotlin中,类、属性、方法默认自动添加final修饰符,表示不可改变。如果开发者希望取消,则可以使用open修饰符。

对于一个private方法、属性,因为它仅在当前类中可见,其子类无法访问它,所以子类无法重写它。如果在子类中定义了一个与父类private有相同方法名、相同形参列表,相同返回值类型的方法,则不是方法重写,而是重新定义了一个新的方法。

final修饰符仅仅是不能被重写,并不是不能被重载。

2.1、宏替换(const)

Kotlin不允许使用final修饰局部变量,也不允许直接在类中定义成员变量(Kotlin定义的是属性),因此在Kotlin中不可能用final定义宏变量。

Kotlin提供了const用来修饰可执行“宏替换”的常量,这种常量也被称为“编译时”常量,因为它在编译阶段就会被替换掉。

“宏替换”的常量除了使用const修饰之外,必须满足如下条件:

a)位于顶层或者是对象表达式中的成员;

b)初始值为基本类型值或字符串字面值;

c)没有自定义的getter方法;

const val MAX_AGE = 100

println(MAX_AGE)//对于程序来说,常量MAX_AGE是根本不存在,程序执行println(MAX_AGE)代码时,实际替换为执行println(100)

3、不可变类

创建不可变类,需要满足如下规则:

  • 提供带参数的构造器,用于根据传入的参数来初始化类中的属性;

  • 定义使用final修饰的只读属性,避免程序通过setter方法改变属性;

如果有必要,重写hashCode和equals方法。

class Address(val detail:String,val postCode:Int){

//重写equals和hashcode

override operator fun equals(other:Any?):Boolean{

if(other==null){

return false

}

if(other==this){

return true

}

if(other.javaclass == Address::class){

var ad = other as Address

return (ad.detail.equals(this.detail) && ad.postCode==this.postCode)

}

return false

}

override fun hashCode():Int{

return detail.hashCode() + postCode

}}

与不可变类相对应的是可变类,只要我们定义了任何读写属性,这个类就是可变类。

与可变类相比,不可变类的实例在整个生命周期中永远出于初始化状态,因此对不可变类的对象的控制是很简单的。

注意:如果不可变类包含的成员属性是可变的,那么这个不可变类是失败的。

class Name(var fullName:String=""){

}

class Person(val name:Name){

}

==>

val n = Name("tom")

 var p =Person(n)

p.name = “heery”

为了保持Person的不可变性,必须保护好Person对象的引用类型的属性:name。让程序无法访问到Person对象的name属性的幕后字段,也就无法利用name属性的可变性改变Person对象了。

class Person{

val name:Name

get(){

return Name(name.fullName)

}}

如果需要一个不可变的类,尤其要注意其引用类型的属性,如果属性的类型本身是可变的,就必须采取必要的措施来保护该属性所引用的对象不会被修改,这样才能真正的创建不可变类。

4、抽象类

abstract修饰的类,表明这个类是需要被继承的,因此无需再使用open修饰了。

abstract修饰的方法和属性,必须由子类提供实现(即重写)。

抽象类中的具体方法、属性依然由final修饰,如果程序需要重写抽象类中的具体的方法、属性,则依然需要使用显式的为这些方法和属性使用open修饰。

abstract不能用于修饰局部变量,不能用于修饰构造器。

5、密封类

密封类是一种特殊的抽象类,专门用于派生子类。

与普通的抽象类相比,密封类有如下特点:

  • 子类是固定的,密封类的子类必须与密封类本身在一个文件中,在其他文件中则不能为密封类派生子类。

sealed class Apple{

abstract fun taste()

}

open class RedFuJi:Apple(){

override fun taste(){

}}

data class Gala(var weight:Double):Apple(){

override fun taste(){

}}

==> 密封类经过Kotlin编译器编译之后得到的是一个抽象类的class文件,只不过该抽象类的构造器被private修饰。

6、接口

Kotlin的接口是以Java8为蓝本设计的,因此Kotlin的接口与Java8的接口非常相似。

[修饰符] interface 接口名:父接口1,父接口2,......{

零到多个属性定义。。。。。

零到多个方法定义。。。。。

零到多个嵌套类、嵌套接口、嵌套枚举类。。。。。

}

修饰符

  • public|private|internal,默认修饰符是public

  • 支持继承多个父接口,不支持继承类

  • 与Java8类似,Kotlin接口既包含了抽象方法,也包含了非抽象方法,非常自由。

  • 接口中的属性没有幕后字段的,因此无法保存状态,所以接口中的属性,要么声明为抽象属性,要么为之提供getter/setter方法

  • 接口中不能包含构造器和初始化块

如下情况,接口会自动添加abstract修饰符:

  • 方法无方法体;

  • 只读属性,没有定义getter方法;

  • 读写属性,没有定义getter和setter方法;

Kotlin接口中的成员可支持private、public两种访问权限。这一点与Java有较大的区别,Java接口中只能指定public修饰符。

  • 抽象的成员只能用public修饰,如果不加,默认也是public;

  • 对于不需要实现类重写的成员,可以使用private或public,如果不加修饰符,默认也是public;

6.1、接口的用途

  • 定义变量,也可用于进行强制类型转换。

  • 被其他类实现。

7、嵌套类和内部类

Java的内部类可分为两种:静态内部类和非静态内部类。Kotlin则完全取消了static修饰符,但实际上Kotlin也需要有静态内部类和非静态内部类之分,所以Kotlin只是为它们换了一个马甲:

  • 嵌套类

只要将一个类放在另一个类中定义,这个类就变成了嵌套类。相当于Java中的静态内部类。

  • 内部类

使用inner修饰的内部类,相当于Java中的非静态内部类。

嵌套类和内部类可以使用protected修饰,表示可在其外部类的子类中被访问。

由于嵌套类是属于其外部类的成员,所以可以使用任意访问控制符如private、internal、protected、public等修饰

Kotlin关于this的处理规则:

  • 在类的方法或属性中,this代表调用该方法或属性的对象;

  • 在类的构造器中,this代表了该构造器即将返回的对象;

  • 在扩展函数或者带接收者的匿名函数字面值中,this代表了点左边的接收者;

  • 如果this没有限定符,那么它优先代表包含该this的最内层的接收者,并且会自动向外搜索。如果要让this明确引用特定的接收者,则可使用标签限定符;

7.1、嵌套类

嵌套类不能访问外部类的其他成员,只能访问外部类的其他嵌套类。因为嵌套类是静态成员,静态成员是不能访问非静态成员的。

class Outter{

var test:String=""

class InnerStatic{

var a = A()//正确

println(test)//错误

}

class A

}

嵌套类相当于外部类的静态成员,因此外部类的所有方法、属性、初始化块都可以使用嵌套类来定义变量、创建对象等。

外部类依然不能直接访问嵌套类的成员,但可以使用嵌套类的对象作为调用者来访问嵌套类的成员。

class AccessNestedClass{

class NestedClass{

var prop = 9

}

fun access(){

println(prop)//错误

println(NestedClass().prop)//正确

}

}

7.2、内部类 - 与Java中的用法一毛一样,不再详述

7.3、匿名内部类 - (Kotlin之对象表达式)

匿名内部类是Java中的概念,在Kotlin中,提供了一个增强版的匿名内部类:对象表达式。

对象表达式的强大之处在于可以指定0~N个父类型(接口或父类),而Java中的匿名内部类只能指定一个父类型(接口或父类)

语法:

object[:0~N个父类型]{

//对象表达式的类体部分

}

对象表达式是一个增强版的匿名内部类,因此编译器处理对象表达式的时候也会像匿名内部类一样生成对象的class文件。

对象表达式的规则:

  • 不允许将对象表达式定义成抽象的。因为系统会在创建对象表达式的时候,立即创建对象。

  • 对象表达式不能定义构造器。但可以定义初始化块,通过初始化块来完成构造器需要完成的事情。

  • 对象表达式可以包含内部类,但是不能包含嵌套类。

var ob = object:Outtable,Product(10.99){//Outtable是接口,Product是抽象类,在继承抽象类的时候,必须调用抽象类的构造器,Productv(10.99)表明调用了抽象类的一个带Double参数的构造器。

init{

......

}

inner class Foo

}

对象表达式还有一个比较重大的特点:

  • 对象表达式在方法或函数的局部范围内,或使用private修饰的对象表达式,Kotlin编译器可以准确的识别对象表达式的真实类型,因此可以调用对象表达式增加的属性和方法。

  • 非private修饰的对象表达式与Java的匿名内部类相似,编译器只会把对象表达式当成它所继承的父类或所实现的接口处理。如果没有父类,系统当它是Any类型。

class ObejctType{

private var ob1 = object{

var name="tom"

}

internal var ob2 = object{

var name = "mat"

}

private fun privateBar = object{

var name="wrw"

}

fun publicBar = object{

var name="eee"

}

fun test(){

println(ob1.name)//正确

println(ob2.name)//错误

println(privateBar().name)//正确

println(publicBar().name)//错误

}}

总结:与Java匿名内部类相比,对象表达式增强了三个方面:

  • 对象表达式可以指定多个父类型;

  • Kotlin编译器可以准确的识别局部范围内的private对象表达式的类型;

  • 对象表达式可访问或修改其所在范围内的局部变量;

7.4、对象声明与单例模式

object objectName[:0~N个父类型]{

//对象表达式的类体部分

}

可以看出,与对象表达式的唯一区别是需要object关键字后面指定名字。

与对象表达式的区别:

  • 对象表达式是一个表达式,因此可以被赋值给变量,而对象声明不是表达式,因此不可以用于赋值;

  • 对象声明可包含嵌套类,不能包含内部类;而对象表达式可以包含内部类,不能包含嵌套类;

  • 对象声明不能定义在函数和方法内;但对象表达式可嵌套在其他对象声明或非内部类中。

var ob3 = object MyObejct{

fun outPut(){

println("hahahahhaha")

}}

MyObject.outPut()

对象声明是专门用于实现单例模式的,对象声明所定义的对象也是该类的唯一实例,程序通过对象声明的名称直接访问该类的唯一实例。

7.5、伴生对象与静态成员

语法:

companion object objectName[:0~N个父类型]{//伴生对象的名称不重要,可以省略。

//对象表达式的类体部分

}

每个类最多定义一个伴生对象,伴生对象相当于外部类的对象,可通过外部类直接调用伴生对象的成员。

interface Outputable{

fun output(msg:String)

}

class MyClass{

companion object MyObject:Outputable{

val name="name的属性值"

override fun output(msg:String){

。。。。。。

}}}

==》 MyClass.output("hshshsh")

注意:虽然伴生对象的主要作用是为它所在的类模拟静态成员,但只是模拟,伴生对象的成员依然是伴生对象本身的实例成员,并不属于伴生对象所在的外部类。

在JVM平台上,可以通过@JvmStatic注解让系统根据伴生对象的成员为其所在的外部类生成真正的静态成员。

伴生对象也可以被扩展,通过Companion访问伴生对象。

class MyClass{

companion object{

const val name = "test"

}}

fun MyClass.Companion,test(){

。。。。

}

7.6、枚举类

Koltin中的枚举类与Java的差别不大。

Kotlin使用enum class关键字定义一个枚举类。

与普通类的区别:

  • enum定义的枚举类默认继承自kotlin.Enum类,而不是默认继承Any类。因此枚举类不能显式继承其他父类了。其中Enum类实现了kotlin.Comparable接口。

  • 枚举类不能派生子类,因此不能使用open修饰。

  • 枚举类的构造器只能使用private修饰。

  • 枚举类的所有实例必须在枚举类的第一行显式列出,列出枚举实例后最好使用分号结尾。

 

8、委托 - Kotlin的特色功能,Java不具备的功能

通过关键字“by”指定委托对象。

8.1、类委托

类委托是代理模式的应用,本质是将本类需要实现的部分方法委托给其他对象 - 相当于借用了其他对象的方法作为自己的实现。

interface Outputable{

var content:Stirng

fun printContent()

}

class DefaultOutput:Outputable{

override var content:String = ""

override fun printContent(){

println("打印了" + content)

}}

//类委托的两种方式:构造器、创建对象的方式

//1

class Printer(b:DefaultOutput) :Outputable by b{//这种构造器委托的方式比较常用

}

//2

class  Projector() :Outputable by DefaultOutput(){

override fun printContent(){

println("Projector打印了" + content)

}}

fun main(args:Array<String>){

var out =DefaultOutput()

var p = Printer(out)

var pj = Projector()

pj.content = "www"//调用委托类的属性

pj.printContent()//调用Projector类本身的方法。Kotlin优先使用该类自己实现的方法

}

8.2、属性委托

属性委托将多个类的类似属性统一交给委托对象集中实现,这样就避免了每个类都需要单独实现这些属性。

对于指定了委托对象的属性而言,由于它的实现逻辑都交给了委托对象进行了处理,因此开发者不能为委托属性提供getter和setter方法。kotlin也不会为委托属性提供幕后字段、getter和setter方法的默认实现。

一旦将某个对象指定为属性的委托对象,该对象将全面接管属性的读写工作,因此属性的委托对象无需实现任何接口,但一定要提供setValue和getValue方法(val属性只提供getter方法即可)。

委托对象只需提供使用operator修饰的getValue/setValue方法。

getValue:

  • thisRef:该参数代表属性所属的对象。

  • property:目标属性,参数的类型必须是KProperty<*>或其超类型

  • 返回值:返回与目标属性相同的类型或其子类型。

setValue:

  • thisRef:该参数代表属性所属的对象。

  • property:目标属性,参数的类型必须是KProperty<*>或其超类型

  • newValue:设置的新值。

提示:

kotlin.properties库下提供了ReadOnlyProperty和ReadWriteProperty两个接口,实现这两个接口的类均可作为属性的委托对象。

示例:

class PropertyDelegation{

var name:String by MyDelegation()

}

class MyDelegation{

private var _backValue ="默认值"

operator fun getValue(thisRef:PropertyDelegation,property:KProperty<*>):String{

return _backValue

}

operator fun setValue(thisRef:PropertyDelegation,property:KProperty<*>,newValue:String){

_backValue = newValue

}}

==》var pd = PropertyDelegation();

pd.name = "tom"

println(pd.name)

8.3、延迟属性(lazy)

kotlin提供了一个lazy()函数,该函数接受一个Lambda表达式作为参数,并返回一个Lazy<T>对象。

Lazy<T>对象包含了一个符合只读属性委托要求的getValue方法,因此Lazy<T>只能作为只读属性的委托对象。

val lazyProp:String by lazy{

println("我是延迟属性")

"我是延迟属性"

}

==》

println(lazyProp)

println(lazyProp)

处理逻辑:当第一次访问lazyProp的时候,会计算Lambda表达式的值,并返回。第二次则不再计算,直接返回第一次计算得到的返回值。

lazy函数提供了如下两个版本:

fun<T> lazy(initializer:()->T):Lazy<T>

fun<T> lazy(mode:LazyThreadSafetyMode,initializer:()->T):Lazy<T>

上面我们使用的是第一个,自动添加的Mode为线程安全的同步锁(LazyThreadSafetyMode.SYNCHRONIZED),因此开销略大。

如果不需要保证线程安全性,希望更好的性能,可以设置为如下Mode:

  • LazyThreadSafetyMode.PUBLICATION:不使用排他锁,多个线程可同步执行。

  • LazyThreadSafetyMode.NONE:不会有任何线程安全相关的操作和开销。

8.4、属性监听

Kotlin的kotlin.properties包下提供了一个Delegates对象声明(单例对象),该对象包含了两个方法:observable、vetoable可以用于属性的监听。

var observStr:String by Delegates.observable("默认值"){

prop,old,new->

println("${prop} 的${old}被改为${new}")

}

8.5、MutableMap对象可以作为读写属性的委托对象

8.6、委托工厂

委托工厂只需提供如下方法:

  • operator fun provideDelegate(thisRef:Any?,prop:KProperty<*>):该方法的两个参数与委托对象的getValue()方法的两个参数的意义相同。

如果上述方法返回ReadOnlyProperty对象,则该对象可作为只读属性的委托对象;返回ReadWriteProperty对象,则可作为读写属性的委托对象。

provideDelegate方法的好处是:Kotlin会保证对象的属性被初始化时调用该方法来生成委托。这样我们可以在该方法中加入任何自定义代码,完成任何自定义的逻辑了。

class MyTarget{

var name:String by PropertyChecker()

}

class  PropertyChecker(){

operator fun provideDelegate(thisRef:Any?,prop:KProperty<*>):ReadWriteProperty<MyTarget,String>{

//插入自定义代码,可执行任意业务操作

checkProperty()

//返回委托对象

return MyDelegation()

}}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值