scala读书学习笔记006

1、scala的每个类都继承自Any,Scala程序中的每个对象都可以用==、!=或equals来进行比较,用##或hashCode做哈希,以及用toString做格式化。相等和不等方法(==和!=)在Any类中声明为final,所以它们不能被子类重写。==方法从本质上讲等同于equals,而!=一定是equals的反义。子类可以通过重写equals方法来定制==或!=的含义。根类Any有两个子类:AnyVal和AnyRef。AnyVal是Scala中所有值类的父类。虽然你可以定义自己的值类,但Scala提供了九个内建的值类:Byte、Short、Char、Int、Long、Float、Double、Boolean和Unit。前八个对应Java的基本类型,它们的值在运行时是用Java的基本类型的值来表示的。这些类的实例在Scala中统统写作字面量。不能用new来创建这些类的实例,Unit粗略地对应到Java的void类型;它用来作为那些不返回有趣的结果的方法的结果类型。Unit有且只有一个实例值,写作()。值类以方法的形式支持通常的算术和布尔操作符。例如,Int拥有名为+和*的方法,而Boolean拥有名为||和&&的方法。值类同样继承了Any类的所有方法。值类空间是扁平的,所有的值类都是scala.AnyVal的子类,但它们相互之间并没有子类关系。不同的值类类型之间存在隐式的转换,隐式转换还被用于给值类型添加更多功能。工作原理是这样的:方法min、max、until、to和abs都定义在scalaruntimeRichInt类中,并且存在从Int类到RichInt类的隐式转换。只要对Int调用的方法没有在Int类中定义,而RichInt类中定义了这样的方法,隐式转换就会被自动应用。其他值类也有类似的“助推类”和隐式转换。根类Any的另一个子类是AnyRef类。这是Scala所有引用类的基类。前面我们提到过,在Java平台上AnyRef事实上只是javalangObject的一个别名。因此,Java编写的类和Scala编写的类都继承自AnyRef。因此,我们可以这样来看待java.lang.Object:它是AnyRef在Java平台的实现。虽然可以在面向Java平台的Scala程序中任意换用Object和AnyRef,推荐的风格是尽量都使用AnyRef。

2、scala存放整数的方式跟Java一样,都是32位的词(word)。这对于JVM上的效率以及跟Java类库的互操作都很重要。标准操作比如加法和乘法被实现为基本操作。不过,Scala在任何需要将整数当作(Java)对象时,都会启用“备选”的java.lang.Integer类。我们对整数调用toString或将整数赋值给一个类型为Any的变量时,都会发生这种情况。类型为Int的整数在必要时都会透明地被转换成类型为java.lang.Integer的“装箱整数”。所有这些听上去都很像Java 5的自动装箱(auto-boxing)机制,也的确非常相似。不过有一个重要区别:Scala中的装箱跟Java相比要透明得多。Java并不是一个纯的面向对象语言(boolean isEqual(int x,int y) 和 boolean isEqual(Integer x,Integer) 在计算esEqual(1,1)返回的结果会不一样 )。基本类型和引用类型之间有一个清晰可被观察到的区别。

3、scala的相等性操作==被设计为对于类型的实际呈现是透明的。对于值类型而言,它表示的是自然(数值或布尔值)相等性。而对除Java装箱数值类型之外的引用类型,==被处理成从Object继承的equals方法的别名。这个方法原本定义用于引用相等性,但很多子类都重写了这个方法来实现它们对于相等性更自然的理解和表示。在有些场景下你需要引用相等性而不是用户定义的相等性。AnyRef类定义了一个额外的eq方法,该方法不能被重写,实现为引用相等性。

4、scala.Null和scala.Nothing。它们是Scala面向对象的类型系统用于统一处理某些“极端情况”(corner case)的特殊类型。Null类是null引用的类型,它是每个引用类(也就是每个继承自AnyRef的类)的子类。Null并不兼容于值类型,比如你并不能将null赋值给一个整数变量。Nothing位于Scala类继承关系的底部,它是每个其他类型的子类型。不过,并不存在这个类型的任何值。Scala标准类库的Predef对象有一个error方法,其定义如下:
error的返回类型是Nothing,这告诉使用方该方法并不会正常返回(它会抛出异常)。由于Nothing是每个其他类型的子类型,可以以非常灵活的方式来使用error这样的方法。

5、以定义自己的值类来对内建的值类进行扩充。跟内建的值类一样,你的值类的实例通常也会编译成那种不使用包装类的Java字节码。在需要包装类的上下文里,比如泛型代码,值将被自动装箱和拆箱。
只有特定的几个类可以成为值类。要使得某个类成为值类,它必须有且仅有一个参数,并且在内部除def 之外不能有任何其他东西。不仅如此,也不能有其他类扩展自值类,且值类不能重新定义equals或hashCode。
要定义值类,你需要将它处理成AnyVal的子类,并在它唯一的参数前加上val,要想尽可能发挥Scala类继承关系的好处,请试着对每个领域概念定义一个新的类,哪怕复用相同的类做不同的用途是可行的。即便这样的一个类是所谓的细微类型(tiny type),既没有方法也没有字段,定义这样的一个额外的类有助于编译器在更多的地方帮到你。

6、特质是Scala代码复用的基础单元。特质将方法和字段定义封装起来,然后通过将它们混入(mix in)类的方式来实现复用。它不同于类继承,类继承要求每个类都继承自一个(明确的)超类,而类可以同时混入任意数量的特质。

7、可以用extends关键字来混入特质,在这种情况下隐式地继承了特质的超类,如果想要将特质混入一个显式继承自某个超类的类,可以用extends来给出这个超类,并用with来混入特质。在特质定义中可以做任何在类定义中做的事,语法也完全相同,除了以下两种情况:
首先,特质不能有任何“类”参数(即那些传入类的主构造方法的参数),另一个类和特质的区别在于类中的super调用是静态绑定的,而在特质中super是动态绑定的。如果在类中编写“super.toString”这样的代码,你会确切地知道实际调用的是哪一个实现。在你定义特质的时候并没有被定义。具体是哪个实现被调用,在每次该特质被混入到某个具体的类时,都会重新判定。这里super看上去有些奇特的行为是特质能实现可叠加修改(stackable modification)的关键。

8、特质的一个主要用途是自动给类添加基于已有方法的新方法。也就是说,特质可以丰富一个瘦接口,让它成为富接口。瘦接口和富接口代表了我们在面向对象设计中经常面临的取舍,在接口实现者和使用者之间的权衡。富接口有很多方法,对调用方而言十分方便。使用者可以选择完全匹配他们需求的功能的方法。而瘦接口的方法较少,因而实现起来更容易。不过瘦接口的使用方需要编写更多的代码。由于可供选择的方法较少,他们可能被迫选择一个不那么匹配需求的方法,然后编写额外的代码来使用它。给特质添加具体方法让瘦接口和富接口之间的取舍变得严重倾向于富接口。不同于Java,给Scala特质添加具体方法是一次性的投入。你只需要在特质中实现这些方法一次,而不需要在每个混入该特质的类中重新实现一遍。因此,跟其他没有特质的语言相比,在Scala中实现的富接口的代价更小。
要用特质来丰富某个接口,只需定义一个拥有为数不多的抽象方法(接口中瘦的部分)和可能数量很多的具体方法(这些具体方法基于那些抽象方法编写)的特质。然后,你就可以将这个增值(enrichment)特质混入到某个类,在类中实现接口中瘦的部分,最终得到一个拥有完整富接口实现的类。

9、比较(对象大小)是另一个富接口会带来便捷的领域。当你需要比较两个对象来对它们排序时,如果有这么一个方法可以调用来明确你要的比较,就会很方便。如果你要的是“小于”,可以说<,而如果你要的是“小于等于”,可以说<=。如果用一个瘦的比较接口,可能只能用<方法,而有时可能需要编写类似“(x < y) || (x == y)”这样的代码。而一个富接口可以提供所有常用的比较操作,这样你就可以直接写下如同“x <= y”这样的代码。Scala提供了专门的特质来解决。这个特质叫作Ordered。使用的方式是将所有单独的比较方法替换成compare方法。Ordered特质为你定义了<、>、<=和>=,这些方法都是基于你提供的compare来实现的。因此,Ordered特质允许你只实现一个compare方法来增强某个类,让它拥有完整的比较操作。

10、每当你需要实现一个按某种比较排序的类,都应该考虑混入Ordered特质。如果你这样做了,将会提供给类的使用方一组丰富的比较方法。
要小心Ordered特质并不会帮你定义equals方法,因为它做不到。这当中的问题在于用compare来实现equals需要检查传入对象的类型,而由于(Java的)类型擦除机制,Ordered特质自己无法完成这个检查。因此,需要自己定义equals方法,哪怕你已经继承了Ordered。

11、Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。

12、对于实现可叠加修改的特质,这样的安排通常是需要的。为了告诉编译器你是特意这样做的,必须将这样的方法标记为abstract override。这样的修饰符组合只允许用在特质的成员上,不允许用在类的成员上,它的含义是该特质必须混入某个拥有该方法具体定义的类中。混入特质的顺序是重要的。你调用某个带有混入的类的方法时,最靠右端的特质中的方法最先被调用。如果那个方法调用super,它将调用左侧紧挨着它的那个特质的方法,以此类推。

13、特质是一种从多个像类一样的结构继承的方式,不过它们跟许多其他语言中的多重继承有着重大的区别。其中一个区别尤为重要:对super的解读。在多重继承中,super调用的方法在调用发生的地方就已经确定了。而特质中的super调用的方法取决于类和混入该类的特质的线性化(linearization)。正是这个差别让前一节介绍的可叠加修改变为可能。

14、scala特质的线性化。当你用new实例化一个类的时候,Scala会将类及它所有继承的类和特质都拿出来,将它们线性地排列在一起。然后,当你在某一个类中调用super时,被调用的方法是这个链条中向上最近的那一个。如果除了最后一个方法,所有的方法都调用了super,那么最终的结果就是叠加在一起的行为。在任何线性化中,类总是位于所有它的超类和混入的特质之前。因此,当你写下调用super的方法时,那个方法绝对是在修改超类和混入特质的行为,而不是反过来

15、在Scala中,可以通过两种方式将代码放进带名字的包里。第一种方式是在文件顶部放置一个package子句,让整个文件的内容放进指定的包,由于Scala代码是Java生态的一部分,对于你打算发布出来的Scala包,建议你遵循Java将域名倒过来作为包名的习惯。另一种将Scala代码放进包的方式更像是C#的命名空间。可以在package子句之后加上一段用花括号包起来的代码块,这个代码块包含了进入该包的定义。这个语法称为打包(packaging,我们把代码按照包层次结构划分以后,不仅有助于人们浏览代码,同时也是在告诉编译器,同一个包中的代码之间存在某种相关性。在访问同一个包的代码时,Scala允许我们使用简短的,不带限定前缀的名称。再次,使用花括号打包语法时,所有在包外的作用域内可被访问的名称,在包内也可以访问到。如果你坚持每个文件只有一个包的做法,那么(就跟Java一样)只有那些在当前包内定义的名称才(直接)可用。scala提供了一个名为__root__的包,这个包不会跟任何用户编写的包冲突。换句话说,每个你能编写的顶层包都被当作是__root__包的成员。

16、在Scala中,我们可以用import子句引入包和它们的成员。被引入的项目可以用File这样的简单名称访问,而不需要限定名称,比如java.io.File。
import子句使得某个包或对象的成员可以只用它们的名字访问,而不需要在前面加上包名或对象名。指定类名表示单类型引用,_表示按需引用。跟Java相比,Scala的import子句要灵活得多。主要的区别有三点,在Scala中,引入可以:
• 出现在任意位置
• 引用对象(不论是单例还是常规对象),而不只是包
• 让你重命名并隐藏某些被引入的成员
还有一点可以说明Scala的引入更灵活:它们可以引入包本身,而不仅仅是这些包中的非包成员。如果你把嵌套的包想象成包含在上层包内,这样的处理很自然。Scala中的引入还可以重命名或隐藏指定的成员。做法是包在花括号内的引入选择器子句(import selector clause)中,这个子句跟在那个我们要引入成员的对象后面。重命名子句的形式永远都是“<原名> => <新名>”。


17、import Fruits.{Pear => _,_}
引入除Pear之外Fruits的所有成员。形如“<原名> => _”的子句将在引入的名称中排除<原名>。从某种意义上讲,将某个名称重命名为“_”意味着将它完全隐藏掉。这有助于避免歧义。比方说你有两个包,Fruits和Notebooks,都定义了Apple类.引入选择器可以包含:
• 一个简单的名称x。这将把x包含在引入的名称集里。
• 一个重命名子句x => y。这会让名为x的成员以y的名称可见。
• 一个隐藏子句x => _。这会从引入的名称集里排除掉x。
• 一个捕获所有(catch-all)的“_”。这会引入除之前子句中提到的成员之外的所有成员。如果要给出捕获所有子句,它必须出现在引入选择器列表的末尾。

18、scala对每个程序都隐式地添加了一些引用,默认引入了一下三个子句:
import java.lang.——
import scala._
import Predef._
Scala对这三个引入子句做了一些特殊处理,后引入的会遮挡前面的。

19、scala的包、类或对象的成员可以标上private和protected这样的访问修饰符。这些修饰符将对成员的访问限定在特定的代码区域。Scala对私有成员的处理跟Java类似。标为private的成员只在包含该定义的类或对象内部可见。在Scala中,这个规则同样适用于内部类。同一个类中,在类中外部访问内部类的私有方式这个在java中是允许的,但是在scala中不合法,因为Java中可以从外部类访问其内部类的私有成员。
跟Java相比,Scala对protected成员的访问也更严格。在Scala中,protected的成员只能从定义该成员的子类访问。而Java允许同一个包内的其他类访问这个类的受保护成员。Scala并没有专门的修饰符用来标记公共成员:任何没有被标为private或protected的成员都是公共的。公共成员可以从任何位置访问到。

20、我们可以用限定词对Scala中的访问修饰符机制进行增强。形如private[X]或protected[X]的修饰符的含义是对此成员的访问限制“上至”X都是私有或受保护的,其中X表示某个包含该定义的包、类或单例对象。
带有限定词的访问修饰符让我们可以对成员的可见性做非常细粒度的控制,尤其是它允许我们表达Java中访问限制的语义,比如包内私有、包内受保护或到最外层嵌套类范围内私有等。这些用Scala中简单的修饰符是无法直接表达出来的。这种机制还允许我们表达那些无法在Java中表达的访问规则。用访问限定符实现灵活的保护域
这个机制在那些跨多个包的大工程中非常有用。可以定义对工程中某些子包可见但对外部不可见的实体。这在Java中是无法做到的。一旦某个定义越过了包的边界,它就对整个世界可见了。
当然,private的限定词也可以是直接包含该定义的包。

21、所有的限定词也可以应用在protected上,跟private上的限定词作用一样。也就是说,如果我们在C类中使用protected[X]这个修饰符,那么C的所有子类,以及X表示的包、类或对象中,都能访问这个被标记的定义。例如,示例13.12中的useStarChart方法在Navigator的所有子类,以及navigation包中的代码都可以访问。这样一来,这里的含义就跟Java的protected是完全一样的。
private的限定词也可以引用包含它的类或对象。例如,示例13.12中LegOfJourney类的distance变量被标记为private[Navigator],因此它在整个Navigator类中都可以访问。这就达到了跟Java中内部类的私有成员一样的访问能力。当C是最外层的嵌套时,private[C]跟Java的private就是一样的效果。最后,Scala还提供了比private限制范围更小的访问修饰符。被标记为private[this]的定义,只能在包含该定义的同一个对象中访问。这样的定义被称作是对象私有(object-private)的。将一个成员标记为private[this],保证了它不会被同一个类的其他对象看到。这对于文档来说是有意义的。同时也方便我们编写更通用的型变(variance)注解。

22、在Java中,静态成员和实例成员同属一个类,因此访问修饰符对它们的应用方式是统一的。你已经知道Scala没有静态成员;而是用伴生对象来承载那些只存在一次的成员。Scala的访问规则在private和protected的处理上给伴生对象和类保留了特权。一个类会将它的所有访问权跟它的伴生对象共享,反过来也一样。具体来说,一个对象可以访问它的伴生类的所有私有成员,一个类也可以访问它的伴生对象中的所有私有成员。

23、Scala和Java在修饰符的方面的确很相似,不过有一个重要的例外:protected static。Java中类C的protected static成员可以被C的所有子类访问。而对于Scala的伴生对象而言,protected的成员没有意义,因为单例对象没有子类。

24、任何你能放在类级别的定义,都能放在包级别。如果你有某个希望在整个包都能用的助手方法,大可将它放在包的顶层。
具体的做法是把定义放在包对象(package object)当中。每个包都允许有一个包对象,任何被放在包对象里的定义都会被当作这个包本身的成员。从语法上讲,包对象跟本章前面展示的花括号“打包”很像。唯一的区别是包对象包含了一个object关键字。这是一个包对象,而不是一个包。花括号括起来的部分可以包含任何你想添加的定义。

25、任何包的任何其他代码都可以像引入类一样引入这个方法。顶层的scala包也有一个包对象,其中的定义对所有Scala代码都可用。
包对象会被编译成名为package.class的类文件,该文件位于它增强的包的对应目录下。

26、在Scala中,断言的写法是对预定义方法assert的调用。如果condition不满足,表达式assert(condition)将抛出AssertionError。assert还有另一个版本:assert(condition, explanation),首先检查condition是否满足,如果不满足,那么就抛出包含给定explanation的AssertionError。explanation的类型为Any,因此可以传入任何对象。assert方法将调用explanation的toString方法来获取一个字符串的解释放入AssertionError。

27、Predef的ensuring断言函数的结果,ensuring这个方法可以被用于任何结果类型,这得益于一个隐式转换。实际上调用的是某个可以从Element隐式转换得到的类型的ensuring方法。该方法接收一个参数,这是一个接收结果类型参数并返回Boolean的前提条件函数。ensuring所做的,就是把计算结果传递给这个前提条件函数。如果前提条件函数返回true,那么ensuring就正常返回结果;如果前提条件返回false,那么ensuring将抛出AssertionError断言可以用JVM的命令行参数-ea和-da来分别打开或关闭。打开时,断言就像是一个个小测试,用的是运行时得到的真实数据。

28、用Scala写测试,有很多选择,从已被广泛认可的Java工具,比如JUnit和TestNG,到用Scala编写的工具,比如ScalaTest、specs2和ScalaCheck。

29、ScalaTest的核心概念是套件(suite),即测试的集合。所谓的测试(test)可以是任何带有名称,可以被启动,并且要么成功,要么失败,要么被暂停,要么被取消的代码。在ScalaTest中,Suite特质是核心组合单元。Suite声明了一组“生命周期”方法,定义了运行测试的默认方式,我们也可以重写这些方法来对测试的编写和运行进行定制。
ScalaTest提供了风格特质(style trait),这些特质扩展Suite并重写了生命周期方法来支持不同的测试风格。它还提供了混入特质(mixin trait),这些特质重写了生命周期方法来满足特定的测试需要。可以组合Suite的风格和混入特质来定义测试类,以及通过编写Suite实例来定义测试套件。
常用结构:继承FunSuite,并以在圆括号中用字符串给出测试的名称,并在花括号中给出具体的测试代码,测试代码是一个以传名参数传入test的函数,test将这个函数登记下来,稍后执行
ScalaTest已经被集成进常见的构建工具(比如sbt和Maven)和IDE(比如IntelliJ IDEA和Eclipse)。也可以通过ScalaTest的Runner应用程序直接运行Suite,或者在Scala解释器中简单地调用它的execute方法。ScalaTest的所有风格,包括AnyFunSuite在内,都被设计为鼓励编写专注的、带有描述性名称的测试。不仅如此,所有的风格都会生成规格说明书般的输出,方便在干系人之间交流。

30、为了在断言失败时提供描述性的错误消息,ScalaTest会在编译时分析传入每次assert调用的表达式。如果你想要看到更详细的关于断言失败的信息,可以使用ScalaTest的Diagrams,断言的错误消息会显示传入assert的表达式的一张示意图。ScalaTest的assert方法并不在错误消息中区分实际和预期的结果,它们仅仅是提示我们左侧的操作元跟右侧的操作元不相等,或者在示意图中显示出表达式的值。如果你想要强调实际和预期的差别,可以换用ScalaTest的assertResult方法。如果你想要检查某个方法抛出某个预期的异常,可以用ScalaTest的assertThrows方法。如果花括号中的代码抛出了不同于预期的异常,或者并没有抛出异常,assertThrows将以TestFailedException异常终止。如果你想要进一步检视预期的异常,可以使用intercept而不是assertThrows。intercept方法跟assertThrows的运行机制相同,不过当异常被抛出时,intercept将返回这个异常。

31、在FlatSpec中,我们以规格子句(specifier clause)的形式编写测试。先写下以字符串表示的要测试的主体(subject),然后是should(must或can),再然后是一个描述该主体需要具备的某种行为的字符串,再接下来是in。在in后面的花括号中,我们编写用于测试指定行为的代码。在后续的子句中,可以用it来指代最近给出的主体。当一个FlatSpec被执行时,它将每个规格子句作为ScalaTest测试运行。FlatSpec(以及ScalaTest的其他规格说明特质)在运行后将生成读起来像规格说明书的输出。

32、ScalaTest的匹配器(matcher)领域特定语言(DSL)。通过混入Matchers特质,可以编写读上去更像自然语言的断言。ScalaTest在其DSL中提供了许多匹配器,并允许你用定制的失败消息定义新的matcher,specs2测试框架是Eric Torreborre用Scala编写的开源工具,也支持BDD风格的测试。BDD的一个重要思想是测试可以在那些决定软件系统应该做什么的人、那些实现软件的人和那些判定软件是否完成并正常工作的人之间架起一道沟通的桥梁。虽然ScalaTest和specs2的任何一种风格都可以这样来用,但是ScalaTest的AnyFeatureSpec是专门为此设计的。AnyFeatureSpec的设计目的是引导关于软件需求的对话:必须指明具体的功能(feature),然后用场景(scenario)来描述这些功能。Given、When、Then方法(由GivenWhenThen特质提供)能帮助我们将对话聚焦在每个独立场景的具体细节上。最后的pending调用表明测试和实际的行为都还没有实现——这里只是规格说明。一旦所有的测试和给定的行为都实现了,这些测试就会通过,我们就可以说需求已经满足。
Scala的另一个有用的测试工具是ScalaCheck,这是由Rickard Nilsson编写的开源框架。ScalaCheck让你能够指定被测试的代码必须满足的性质。对每个性质,ScalaCheck都会生成数据并执行断言,检查代码是否满足该性质。AnyWordSpec是一个ScalaTest的风格类。ScalaCheck PropertyChecks特质提供了若干forAll方法,让你可以将基于性质的测试跟传统的基于断言或基于匹配器的测试混合在一起。whatever子句表达的意思是,只要左边的表达式为true,那么右边的表达式也必须为true。


33、我们可以把一个大型的测试套件看作是Suite,对象组成的树形结构。当你执行这棵树的根节点时,树中所有Suite都会被执行。
可以手动或自动嵌套测试套件。手动的方式是在你的Suite中重写nestedSuite方法,或将你想要嵌套的Suite作为参数传给Suites类的构造方法,这个构造方法是ScalaTest专门为此提供的。自动的方式是将包名提供给ScalaTest的Runner,它会自动发现Suite套件,并将它们嵌套在一个根Suite里,并执行这个根Suite。
可以从命令行调用ScalaTest的Runner应用程序,也可以通过构建工具,比如sbt、maven或ant来调用。通过命令行调用Runner最简单的方式是通过org.scalatest.run。通过-cp参数将ScalaTest的JAR文件包含在类路径中(下载的JAR文件名会包含Scala和ScalaTest的版本号)。接下来的命令行参数,org.scalatest.run,是完整的应用程序类名。Scala将会运行这个应用程序并传入剩余的命令行参数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值