《程序员修炼之道:从小工到专家》读书笔记

Chap1 注重实效的哲学

程序员所应该遵循的实用主义原则。

我的源码让猫给吃了:出现错误时,要诚实,不要推诿或者找借口。要提供各种可能的解决方案与后果并与他人沟通,而不是提供借口。

软件的熵:这是著名的破窗户原理。项目中一个小的、无人料理的问题可能带来后续编码时的懈怠,从而造成更大的问题。不要容忍任何小的错误,解决它或至少打上TODO标签。

石头汤与煮青蛙:这个小节很有趣,它讲述了小的变化如何能渐进式地演变为大的变化。

一方面,在面对一个毫无生气的团体、试图催生积极的变化时,可以去做第一个带来改变的人。这样,就会有人随之作出改变。可能每个改变都非常微小,但可以渐变式地带来大的改变。

另一方面,不管是个人和团体都很容易对于小的改变疏忽大意。这意味着有些时候小的改变会在不经意间催生出巨大的改变。不管是有人恶意为之还是意外累积而成,小的坏习惯、坏改变可能会积累成很大的问题,而程序员又可能最后才看出问题来。

这里有一个有趣的问题。石头汤显示我们可以通过小的积极改变催生大的积极改变,而煮青蛙显示我们可能会被小的消极改变迷惑,而忽略了他们在催生大的消极改变。在个体试图催生变化时如何判断是哪一种,消极与积极的改变又以什么作为判断标准?

我以为,催生改变有些时候是有用的,但仅当团体已经死气沉沉、无法通过常规方式作出改变时。当一个团体活力旺盛时,不应用欺骗式的方式催生改变,而应该对所有改变保持警觉,并保持良好的沟通,保证大家时刻都知道自己在做什么。这同样适用于个人。

足够好的软件:完美的软件是不存在的,幸运的是我们只需要制作足够好的软件。判断软件是否“足够好”时要让用户需求加入判断。

你的知识资产:cs行业变化迅速,所以不应止步不前。只有对知识投资才能有回报。要定期学习新的东西,并学习多元化的知识、管理风险。本书建议了一个知识投资计划:

每年至少学习一种新语言;每季度阅读一本技术书籍;也要阅读非技术书籍;上课;参加本地用户组织;试验不同的环境;跟上潮流;上网。

学习过程中,遇到问题时,要作为挑战并积极地寻找解答。另外,学习时要养成批判性的思维,不要迷信任何知识(大概也包括这本书提供的知识)。

交流:交流非常有用,但要学会高效交流。注意这些问题:想清楚自己要说什么,了解自己的听众,选择时机、选择风格,让文档保持美观并让用户加入文档的编写,也要倾听并回应他人。

Chap2 注重实效的途径

程序需要遵守的实用主义原则。

重复的危害:如果某个事物在代码中重复多次,就可能会在维护过程中带来问题,因为改动了一处而忘记改动另一处造成自相矛盾。这加大了维护难度。要遵守DRY原则,即Don’t repeat yourself。

重复通常由这些东西引起:

强加的重复,由文档或用户需求决定。这通常可以依照情况消除。需要重复表示的信息可以用元数据schema与代码生成器消除重复。注释与代码会重复,但实际上这种重复没有必要:注释不应重复代码中显而易见的东西,而应该表达更高级的东西。文档与代码也会重复,可以利用文档生成工具。有些语言会强加一些重复,这比较难解决,只能依情况而定,一些基本的技术是cpp中不要在其他文件中引用函数,而应该使用头文件。

无意的重复,由设计的疏忽造成,需要积极的检查和重构。如果为了性能需要违反DRY原则,记得将重复本地化,不要泄漏到模块外,并保持模块内行为良好。

无耐性的重复,可能造成远大于一时麻烦的痛苦后果。程序员要学会约束自己。

开发者之间的重复,需要加强开发者之间的沟通。

正交性:指系统各同层次的组件之间没有依赖关系,改变一个不影响另一个。如果系统不符合正交性,测试与维护会非常痛苦。而正交性的系统在出问题时更容易隔离修复,进行拓展时也不必改动已有模块,增加了生产力。

为了达到正交性,需要将团队分为几个清晰的小组分别工作,对系统进行模块化的设计。可以通过询问“讨论某个改动时需要设计多少人”判断小组分工是否正交,通过询问“改动某个模块背后的需求,会有多少模块受到影响?”判断系统设计是否正交。

引入第三方库时可能会破坏正交性,要小心不要使第三方库对整体代码造成改动。如果第三方库的接口存在问题,可以将第三方库用适合自己代码的方式进行封装。

编码也有可能破坏正交性,需要注意:使代码保持解耦,除了需要的功能不要暴露其它细节。避免使用全局数据。避免编写相似的函数。

对项目进行测试与debug也可以检查项目的正交性。如果测试或修正一个小模块会带来许多其它的影响,那么系统不够正交。

正交性同样适用于文档,文档内容与表达形式应该解耦。这让我想到了markdown……

可撤销性:许多需求会改变,许多政策会改变,所以编写项目时任何决策都应该可以撤销。项目结构应该保持灵活,不要依赖某个已有决策。

曳光弹:这个比喻有点晦涩。作者介绍了一种方法,编码之初先搭建一个大致框架,然后慢慢填充编码。这样既可以方便编码,又可以随时与用户沟通项目是否符合他们的需求。假如现有成果不符合需求,可以立马进行修改,而不必等代码基本固定时候再进行重构。

原型与便笺:不同于曳光弹,曳光弹在使用之后继续保留,只是逐渐“生长”成更完整的系统。原型是为了分析某个功能或需求建立的简化模型,将细节遮蔽以方便分析,用来找到最好的实现方式,然后便丢弃不用。如分析UI需求时先用绘图板确定最合适的UI,再用编码实现。

领域语言:任何领域都有自己的语言,如用于配制的语言,用于文档的语言,用于描述需求的语言。可以发展一种小型语言。小型语言分两种,配置语言方便解析但难以阅读,命令语言相当于小型的脚本语言,更近似于自然语言,但解析难度更高。小型语言可以是独立的语言,也可以嵌入高级语言的代码,方便直接执行。

这是一个非常有趣的设想,然而很可惜,在我们的项目中,受限于我们的项目规模,这个设想不太可行。然而这给了我两个启发:

  1. 进行编程时,首先将用户需求抽象化。用户需求常常是用自然语言表达的,通常包含许多复杂的要求,如在某些情况下将某个数据传输到另一个服务器,在某些情况下丢弃数据,在某些情况下反馈警告等。直接试图将自然语言描述的需求转化成代码很可能会非常困难,这时候先将需求抽象成一个无关具体细节实现的逻辑框架会更容易,不管是流程框图、伪代码还是别的什么。这也是自顶而下的思想。

  2. 很多时候编写代码需要满足的需求并不是用户的,而是直接对接的下一层的需求。比如负责数据库层的程序员可能需要满足负责算法层的程序员的需求。在这种情况下,交流时需要尽量良好地描述需求的逻辑。这也需要清晰的层与层之间的接口以及风格良好的文档。

  3. 模块与模块之间的信息交流需要清晰、易于解析并易于扩展。

估算:要学会估算。估算的方法在于限定问题的情景范围,对系统建立模型,对模块进行拆分并分别估算,抹去可忽略不计的量。估算不需要过于精确,但需要细心。

Chap3 基本工具

纯文本的威力:

优点:可读性远大于二进制,且不依赖特定的应用解码,因此不会过时。为了增加纯文本可读性,应该使用能够理解的词语。另外纯文本可由任何应用读取,因此适合作为应用之间的接口,将应用拼接成功能更强大、更丰富的应用,如linux shell的管道。

纯文本的缺点在于占用空间更大和解析更难。

shell的游戏:比起大型workbench,cli更加简洁清晰,可以用短小有力的命令完成强大的功能,并可以将功能拼接。

不过,与gui相比,shell不便之处在于功能非常零散,如果要做一件事情可能需要使用很多工具。这样的拼接确实使得自由度最大化,但当需要专心工作于一个工程、希望有一个集成的工具能清晰地显示自己的工作并可以完成大部分常用功能时便有些不方便。可说各有所长,不过此前的我没怎么做过大型项目,所以cli的不方便之处不太明显。

不过书中提到windows下的linux shell工具cygwin很有趣,打算试一试。

强力编辑:文本是编程的基本原料,所以编辑器很重要。vim、emacs等编辑器都功能强大,适用于各个平台,有gui和cli版本。应该使用一个特定的强大编辑器并精通,这样生产率会大大提高。

在shell下这很有道理,不过在visual studio这样的大型平台上似乎没有必要?

源码控制:由源码控制工具控制版本和分支可以让工作更加方便有序,减少出错的可能。没有源码控制工具非常痛苦,只能将代码一份份备份,而且很容易丢或者忘记顺序。

我所知道的源码控制工具只有git。

调试:作者似乎异常强调debug过程中的心理因素。debug确实容易让人崩溃。

在debug过程中要获得尽可能具体的信息。随意地、泛泛地调试往往是徒劳。获得具体的错误信息并进行bug的复现才能尽可能高效地debug。

有这样一些策略:

多用ide的可视化功能,如watch窗口。这是最直观的。

在程序中加入打印语句看起来非常愚蠢的,不过在某些情况确实管用,甚至比ide的debug功能还要管用。

在某个数据出问题时,可以检查附近内存。

可以采用橡皮鸭策略,即用语言将代码逻辑解释一遍。这个过程中可能会发现隐藏的问题。

在无法找到问题时,可以用二分查找。(但是在条件不是一维的时候如何二分查找?)

文本操纵:用文本操纵语言实现,简单的工具如emacs、vi的内置语言、脚本语言如perl、python(我只会python好凉啊)。

文本操纵可以用来维护数据库schema,如自动生成代码、填充空位;自动生成重复代码;生成测试数据;生成不同语言之间接口;生成特殊格式的文档。

代码生成器:分两种。

被动代码生成器,为了减少编码时手工编码。仅使用一次,然后将生成的代码嵌入代码中。被动代码生成器不需要完全精确,只需要简便,事后由人工校对一遍。

主动代码生成器,是代码的有机部分,每次都被调用,如用来根据数据生成不同语言的接口。主动代码生成器符合DRY的原则,可以减少错误。

代码生成器不一定要很复杂,也不一定要生成代码,可以生成任何文本。

Chap4 注重实效的偏执

在生活中检查每一个可能的问题似乎是一种病态,然而编码时对输入数据、接口的误用以及自己的可能问题保持警惕十分必要。

这一章介绍了许多工具,用来对代码进行约束、检查,以免出现问题而带来更大的问题。

按合约设计:Designed by Contract,俗称DBC。

合约:合约约定了进入一个函数/方法/模块的前条件,即进入模块必须满足的条件,常常指满足模块运行的情景或变量取值范围等等;后条件,即模块需要达成的结果,模块运行之后所能达到的状态;不变量,是对于模块的约束,在调用模块前后始终为真的一些描述。

合约可以是动态的,由“合约代理”在不同模块之间协商出一套合约,但我不知道相应技术。

这是一个很好的想法,其优点在于让代码早崩溃,此时问题更小更单纯。但是在常用的c、cpp中这个想法并没有那么可行。可以通过断言实现一部分功能。

不过通过良好的文档,可以对前条件、后条件、不变量进行描述,从而达到合约的想法,一定程度上获得其优点。

死程序不说谎:一个出现问题的程序可能会因为异常操作造成很大的破坏,所以运行出现问题时,崩溃好于破坏。要利用异常机制。

断言式编程:对于“不可能发生”的情况进行断言,以保证系统的健壮与安全,以免因错误数据或恶意攻击出问题。

然而断言时要避免加入执行代码及其他有副作用的代码,避免“海森堡bug”:不当调试改变了被调试系统的行为。

异常:检查每一个可能的错误,尤其是意料之中的错误是有必要的。

要将异常用于真正异常情况的处理而非模块逻辑的一部分。检查这一点的方式是去掉异常,观察模块能否正常运作。

怎样配平资源:谁申请的资源谁解除该资源,降低代码耦合度。

对于嵌套分配,以与资源分配相反的顺序解除资源分配以免资源的遗弃,在代码不同地方申请资源时以相同顺序申请以免死锁。

在异常处理机制中,一个模块可以通过捕捉异常再抛出从而退出和正常退出两条退出途径。为了遵守对资源利用有始有终并减少重复的原则,有以下思路:对于c++,使用类封装而非指针以保证析构函数会解除相应的资源;对于java,可以使用finally语句解除资源。在更多复杂的情况下,比如树状结构中一个资源指向更多资源,可以用类封装等方式减少出错率。

练习中指出了两个小技巧:c/c++中为了避免释放后的指针被错误地引用,修改不该修改的内存,可以将指针值改为null;java为惰性内存管理,所以使用完某个变量后可以将变量值改为null,从而解除引用,使变量更快被回收。

Chap5 弯曲,或折断

解耦:任何一个单独的模块尽量不要依赖其他模块的特性,除了有些特殊情况下会违背这个原则换取一定的效率。

元数据:用于将代码功能灵活化。将容易改变的、不确定的数据用“元数据”进行配置而不要固定地编织到代码中;可以将某些并非模块固定功能的逻辑(比如客户的个性化需求)通过配置而非代码的方式表示,这样就不用反复为了适应需求而修改代码、重新编译。可以时不时检查加载新的配置以免修改配置便需要重启的麻烦状况。

时间的解耦:进行流程分析,分析各任务时间上的相互依赖,进行并发编程,可以提高程序的效率。要对全局变量、静态数据的访问与调用进行保护。接口要设计得更整洁,避免接口对调用时间的依赖。

发布/订阅协议:某个模块只关心特定的事件,并对不同事件分别作出响应。模块通过“订阅”(subscribe)某个事件对事件作出响应。而模块引发某个事件时,依次调用各个订阅者通知事件发生。

MVC:将Model(数据模型), View(视图)和Controller(视图控制器)解耦。MVC模型虽然用于GUI开发,但可以更泛化地理解为Model为抽象数据模型;View为解释模型的方式,向Model与Controller订阅事件;Controller控制View并向Model提供新数据,向Model与View发布事件。

黑板:将各个信息放在一个信息流里,各模块与信息流进行交互,从而可以达到统一简便的交互,而不需要在各个模块之间两两规定严密的发布/订阅协议,发布者与订阅者不需要了解对方。

这个设计用于较为复杂,每个模块的行为都会带来新的改变,随时有可能有事件发生并引起其他模块的行为的系统,此时在各个模块之间一一设计接口几乎不可能。

这个设计很优美,但我产生了两个疑惑:这样大的数据量是否会对数据的维护和模块的查找带来负担?是否会出现信息泄漏的风险?对于第一个问题,维护问题我没有想好,但查找可以通过良好地组织数据结构简化;对于第二个问题,可以通过限制权限得到改善,但似乎还是会有风险。

Chap6 当你编码时

编码过程中应该遵循的原则。

靠巧合编程:不可靠。不要使模块可能依赖特定的输入数据、语境,否则模块的正常工作可能只是偶然。积极地查找可能的问题并修改。

为了避免这个问题,编码过程中要依靠可靠的、了解的东西,而不要碰巧拼凑一些代码,希望它能成功。比如如果不会使用某个第三方库,应该去学习而非随意堆积代码,试图得到希望得到的结果。为模块编写文档有利于发现隐藏的假设和巧合。

不要让自己成为过去的代码的奴隶,尤其是在开发阶段。对于模棱两可、可能出问题的代码,要积极地进行检查与重构。

算法速率:这里讲到了对算法运行时间进行O( )的估算,即数据结构课程中提到的时间复杂度。算法速率的估算可以通过常识估算。如果算法有多重循环,达到了O(n2)甚至O(n3)的时间复杂度,就要试着用分治法等方法将时间复杂度降低。不过算法速率的估算是理论的极端情况,实际的算法速率要通过测试来获取。可以使用code profiler这一工具获得算法中不同步骤的执行次数。

不过快的算法速率并不一定是好的。有时数据集很小,O(n)与O(ln n)级的算法差别很小,算法速率反而受常数影响更大。这时花过多的时间构想一个高效的算法是没有实际意义的,根据实用主义的原则不必这么做。当然如果随着系统的增加该算法成了效率瓶颈,进行优化就很有必要。

重构:重写、重做及重新架构代码。在出现这些情况时需要重构:代码有自重复、功能不正交、性能不够、内容过时。重构可能是痛苦的,但不要为了一时的痛苦放弃重构,因为有问题的代码可能带来更多问题。重构是将原本能够工作的代码进行修改,所以很可能是危险的。重构要遵循以下原则:不能加入新功能。保持有一系列良好的测试,这样出现任何问题时都可以快速发现。将重构拆分成许多个短小的重构,并在进行每个小改动之后进行测试,以发现可能的错误。

易于测试的代码:进行模块的组装前,对模块进行单元测试,检查其在各种情况下的行为非常重要。对于有依赖关系的模块,从最小的模块向上层层单元测试可以更容易定位错误。建议根据合约对代码进行测试,利用合约的前条件、后条件和不变量设置可以很容易确定测试内容。

编写代码的同时就应该编写代码的单元测试。

编写大型项目时,为了方便单元测试,应该使用测试装备以动态构建测试。比如可以编写一个类,负责测试的常用功能。测试装备有必要提供以下功能:用以指定设置测试条件与清理测试现场的方法,用以选择个别或全部测试的方法,分析输出是否是预期结果的方法,生成标准化错误输出的方法。测试应该是可组合的。

在编写代码的过程中,常会加入一些即兴调试,如printf语句。不要抛弃这些调试语句,将它们加入单元测试。编码过程中出现过问题的部分在运行中可能会再出问题。

代码部署好之后也不要忘记测试。要构建运行日志以方便记录运行状态,日志应该风格良好以方便解析。要构建可用于诊断运行状态的服务。

邪恶的向导:依靠开发环境提供的“向导”进行编码是依靠巧合而编程。可以使用封装良好的第三方库,但在不理解gui生成的“向导代码”时不要使用它,因为它会变成代码有机的一部分,并随时可能带来问题。

Chap7 在项目开始之前

需求之坑:需求往往没有被良好地表达,需求、政策与实现往往模糊不清,然而这对于编程很不利,因为需求需要与实现隔离,需求不应有太大变动而政策时常变化。要从用户的角度思考问题。

为了理清需求,可以建立需求文档。需求文档需要有好的形式化,应该由目标驱动。文档应该保持抽象,规定需要的功能即可,过于拘泥细节会限制后续发挥。文档需要远视,不要对一些可能改变的规则进行默认,比如上个世纪将年数默认为19开头,只用存储后两位。为了达到远视,不需要增加许多莫名其妙的功能,而是应该对功能进行抽象,而不要嵌入代码中。

不要不断地增加新特性;为项目维护一个词汇表便于沟通;将需求文档公开用于讨论,就像许多别的文档一样。

解开不可能的问题:对于问题的约束有真正的约束,也有一些是表面上的约束。要找到那些真正意义上的约束并给予尊重,对表面上的约束可以适当予以摒弃。

解决“不可能的问题”时,可以问自己这些问题:有更容易的方法吗?我是在解决实际问题还是被技术细节转移了注意力?这件事情为什么是一个问题?什么使它难以解决?它必须以这种方式完成吗?它真的必须完成吗?

等你准备好:有时候要依靠直觉,直觉觉得仍有疑虑时不该开始。为了将疑虑与单纯的拖延区分,可以通过开始构建原型检查自己的疑虑在什么地方,或者自己只是单纯懒于工作。

规范陷阱:规范重要,但不应归于细致流于琐碎。自然语言本身十分晦涩,过于琐碎的规范往往不能精确地描述意图描述的细节,而且会限制程序员的发挥空间。

圆圈与箭头:通过形式方法描述项目很流行,但有一些缺点:1. 形式化的表达需要由人来阐释含义,不如原型的展示清楚明白。2. 形式方法似乎鼓励专门化,但专门化会带来人力的浪费和不好的编码体验。一个小组应该分工,但不应该被分裂成几个不必要的部分。每个人都应该了解项目系统的大体。3. 形式方法往往无法描述系统的动态性。

形式方法只是一个工具,可以使用,但不要被限制。……事实上这个原则似乎适用于所有工具,包括ide、语言、框架等。

Chap8 注重实效的项目

项目开发中的注意事项与小技巧。

注重实效的团队:针对团队,前述的技术全都有效。

不要留破窗户:在团队中,不要容许小的、没有人愿意去修改的小错误。

煮青蛙。随时注意项目和环境的新变动,注意项目范围的扩大、新的特性和需求等。不要让变动失控。

交流:文档、术语应该一致。为了加强交流,可以使用一个团队名称,尤其是奇怪的名称,以带来归属感。(我们组的名称就挺奇怪的……)

不要重复自己:要有良好的交流与分工,开发过程中职能不要重复。如果不该自己负责的地方出现了问题,不要自己解决,而应该去找负责人。

正交性:用模块功能将成员分组,而不要根据分析师、程序员、测试人员这样的等级划分将成员分组,每个团队自给自足。分析、编程、测试是不正交的,而且隐含了等级关系,不利于合作。

自动化。

知道何时停止:要给别人空间,尤其是组长要给组员空间。

无处不在的自动化:不要做重复的工作。

用批处理文件/脚本实现可自动化的内容。用cron等工具实现周期性的行为,如备份、网站构建等。

项目编译时,用makefile生成代码、编译、测试。要将构建自动化,定期编译并测试。如果构建与常规构建不同,如有特殊的版本号或某个优化方式,可以用单独的make目标表示,并一定要另外测试。

对项目自动化管理。如果用网站实现内部沟通,网站应该自动生成,这同样是DRY原则的应用。批准流程也可以自动化。

无情的测试:要积极地测试,最好自动化测试。

主要的测试类型有:

单元测试,对单个模块进行测试。

集成测试,对模块集成的子系统进行测试,其实是单元测试的拓展。

验证和校验,检查系统是否满足用户需求、是否能够处理现实数据。

资源耗尽、错误及恢复。可能的限制包括:内存、磁盘、CPU带宽、磁盘带宽、网络带宽、视频分辨率、挂钟时间、调色盘……要尽量检查环境限制。如果失败,要保存状态、避免工作丢失。

性能测试。

可用性测试。

测试的方法包括:

回归测试,改动代码后检查测试输出与之前是否一致,以检查在改动时有没有破坏功能。

测试数据,可以从现实世界获取或人工合成。人工合成的数据包括随机数据和特殊的极端数据。

测试GUI。首先测试GUI背后的逻辑,确定没有问题之后用工具或人工测试GUI。遗憾的是测试GUI的工具大多不完善。

对测试进行测试,故意引发bug,观察测试系统能否捕捉。

彻底测试是不可能的。(完美永远是不可能的……)

普通测试的时间应该尽可能频繁,一旦代码存在就要测试。可以自动化测试。压力测试之类的特殊测试可以不那么频繁,但仍要定期测试。

如果发现过一个bug,那么要将这个bug加入测试集,而不要相信这个bug不会复现。

全都是写:讲文档的写法。文档分为内部文档(源码注释,设计与测试文档)和外部文档。

对于内部文档:

代码中的注释应该解释代码要做何事及为何这么做,而不应该解释如何做。如何做会与代码重复。代码中的变量名应该清晰易懂,贴合功能。注释中不应该出现:代码中的函数列表,修订历史,文件使用的其它文件列表,文件名。这些都可以通过其它工具得到。

可执行文档:可以对文档进行解析,分析出模式自动生成代码,如数据库schema。

文档很容易过时,所以在网站显示往往比打印更方便。

可以使用标记语言,如html(我又想到了markdown)编写文档,这样更方便、功能更强大,且内容和外观可以解耦。

极大的期望:满足用户需求很重要,所以最好时时就需求进行沟通,避免取得的进展不是用户想要的。不过为了避免用户缺乏惊喜,有必要比需求略多一点特性。

傲慢与偏见:有必要对代码进行署名,可以带来自豪感,同时减少糟糕的编程。但是要避免领地意识。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大鹏小站

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值