Julia之初体验(十四)流程控制

控制流

Julia提供了多种控制流方法:

前五个控制流机制是高级编程语言的标准。Tasks并不是那么标准:它们提供了非本地控制流,从而可以在临时暂停的计算之间进行切换。这是一个强大的结构:使用任务在Julia中实现异常处理和协作式多任务处理。日常编程不需要直接使用任务,但是使用任务可以更轻松地解决某些问题。

复合表达式

有时,使用单个表达式按顺序计算多个子表达式,然后返回最后一个子表达式的值作为其值,会很方便。有两个Julia结构可完成此任务:begin块和(;)链。这两个复合表达式构造的值都是最后一个子表达式的值。这是一个begin块的示例:

julia> z = begin
           x = 1
           y = 2
           x + y
       end
3

由于这些都是很小的简单表达式,因此可以轻松地将它们放在一行中,这是使用(;)链语法的方便之处:

julia> z = (x = 1; y = 2; x + y)
3

对于Functions中引入的简洁的单行函数定义形式,此语法特别有用。尽管很典型,但并不需要begin块为多行或(;)链为单行:

julia> begin x = 1; y = 2; x + y end
3

julia> (x = 1;
        y = 2;
        x + y)
3

条件评估

条件评估允许根据布尔表达式的值评估或不评估部分代码。这里是解剖ifelseifelse有条件的语法:

if x < y
    println("$x is less than $y")
elseif x > y
    println("$x is greater than $y")
else
    println("$x is equal to $y")
end

如果条件表达式x < ytrue,则对相应的块求值;否则为0。否则对条件表达式x > y求值,如果为true,则对相应的块求值;如果两个表达式都不为真,则对else块进行求值。它在起作用:

julia> function test(x, y)
           if x < y
               println("$x is less than $y")
           elseif x > y
               println("$x is greater than $y")
           else
               println("$x is equal to $y")
           end
       end
test (generic function with 1 method)

julia> test(1, 2)
x is less than y

julia> test(2, 1)
x is greater than y

julia> test(1, 1)
x is equal to y

elseifelse块是可选的,并且尽可能多的elseif块可根据需要使用。在条件表达式ifelseifelse构建体进行计算,直到第一个计算结果为true,相关联的块之后其被评估,并且没有进一步的条件表达式或块被进行评价。

if块是“泄漏的”,即它们不引入局部作用域。这意味着在if子句中定义的新变量可以在if块之后使用,即使之前未定义也可以使用。因此,我们可以将test上面的函数定义为

julia> function test(x,y)
           if x < y
               relation = "less than"
           elseif x == y
               relation = "equal to"
           else
               relation = "greater than"
           end
           println("x is ", relation, " y.")
       end
test (generic function with 1 method)

julia> test(2, 1)
x is greater than y.

该变量relationif块内部声明,但在外部使用。但是,根据这种行为,请确保所有可能的代码路径都为变量定义了一个值。对以上函数的以下更改导致运行时错误

julia> function test(x,y)
           if x < y
               relation = "less than"
           elseif x == y
               relation = "equal to"
           end
           println("x is ", relation, " y.")
       end
test (generic function with 1 method)

julia> test(1,2)
x is less than y.

julia> test(2,1)
ERROR: UndefVarError: relation not defined
Stacktrace:
 [1] test(::Int64, ::Int64) at ./none:7

if块还返回一个值,这对于来自许多其他语言的用户来说似乎并不直观。该值只是所选分支中最后执行的语句的返回值,因此

julia> x = 3
3

julia> if x > 0
           "positive!"
       else
           "negative..."
       end
"positive!"

注意,很短的条件语句(单行)经常使用Julia中的“短路评估”来表示,如下一节所述。

与C,MATLAB,Perl,Python和Ruby不同-但与Java和其他一些更严格的类型化语言类似-如果条件表达式的值不是trueor ,则错误false

julia> if 1
           println("true")
       end
ERROR: TypeError: non-boolean (Int64) used in boolean context

此错误表明条件的类型错误:Int64而不是required Bool

所谓的“三元运算符”,?:被密切相关的ifelseifelse语法,但被用在需要单个表达值之间的条件的选择,相对于代码长块的条件执行。它是大多数语言中唯一采用三个操作数的运算符而得名的:

a ? b : c

表达a,以前?,是一个条件表达式,和三元操作计算表达式b,前:,如果条件atrue或表达c,之后:,如果是false

理解这种行为的最简单方法是看一个例子。在上一个示例中,println所有三个分支共享该调用:唯一的实际选择是打印哪个文字字符串。使用三元运算符可以更简洁地编写该代码。为了清楚起见,让我们首先尝试一个双向版本:

julia> x = 1; y = 2;

julia> println(x < y ? "less than" : "not less than")
less than

julia> x = 1; y = 0;

julia> println(x < y ? "less than" : "not less than")
not less than

如果该表达式x < y为true,则整个三元运算符表达式的计算结果为字符串"less than",否则为字符串"not less than"。原始的三向示例要求将三元运算符的多种用法链接在一起:

julia> test(x, y) = println(x < y ? "x is less than y"    :
                            x > y ? "x is greater than y" : "x is equal to y")
test (generic function with 1 method)

julia> test(1, 2)
x is less than y

julia> test(2, 1)
x is greater than y

julia> test(1, 1)
x is equal to y

为了便于链接,操作员从右到左进行关联。

它是显著像if- - elseifelse表述前后:仅评估如果条件表达式求truefalse分别:

julia> v(x) = (println(x); x)
v (generic function with 1 method)

julia> 1 < 2 ? v("yes") : v("no")
yes
"yes"

julia> 1 > 2 ? v("yes") : v("no")
no
"no"

短路评估

短路评估与条件评估非常相似。在具有&&||布尔运算符的大多数命令式编程语言中都发现了这种行为:在由这些运算符连接的一系列布尔表达式中,仅对最小数量的表达式进行求值,以确定确定整个链的最终布尔值。明确地,这意味着:

  • 在表达式中a && bb仅当a对求值时,才对子表达式求值true

  • 在表达式中a || bb仅当a对求值时,才对子表达式求值false

理由是,无论is 的值如何,a && b必须为falseif ais falseb同样,无论is 的值如何,a || bif 的值都必须为true 。两者和都关联到右侧,但是具有比更高的优先级。尝试这种行为很容易:atrueb&&||&&||

julia> t(x) = (println(x); true)
t (generic function with 1 method)

julia> f(x) = (println(x); false)
f (generic function with 1 method)

julia> t(1) && t(2)
1
2
true

julia> t(1) && f(2)
1
2
false

julia> f(1) && t(2)
1
false

julia> f(1) && f(2)
1
false

julia> t(1) || t(2)
1
true

julia> t(1) || f(2)
1
true

julia> f(1) || t(2)
1
2
true

julia> f(1) || f(2)
1
2
false

您可以轻松地以相同的方式对&&||运算符的各种组合的关联性和优先级进行试验。

Julia经常使用此行为来代替非常简短的if语句。可以代替if <cond> <statement> end编写<cond> && <statement>(可以读为:<cond> 然后是 <statement>)。类似地,if ! <cond> <statement> end可以写一个<cond> || <statement>(而不是,可以写成:<cond>  <statement>)。

例如,可以这样定义递归析因例程:

julia> function fact(n::Int)
           n >= 0 || error("n must be non-negative")
           n == 0 && return 1
           n * fact(n-1)
       end
fact (generic function with 1 method)

julia> fact(5)
120

julia> fact(0)
1

julia> fact(-1)
ERROR: n must be non-negative
Stacktrace:
 [1] fact(::Int64) at ./none:2

布尔操作没有短路评价可以在推出的按位布尔运算符来进行数学运算和基本功能&|。这些是普通函数,碰巧支持中缀运算符语法,但始终会评估其参数:

julia> f(1) & t(2)
1
2
false

julia> t(1) | t(2)
1
2
true

就像在使用条件表达式ifelseif或三元运算符,的操作数&&||必须是布尔值(truefalse)。在条件链中除了最后一个条目之外的任何地方都使用非布尔值是一个错误:

julia> 1 && true
ERROR: TypeError: non-boolean (Int64) used in boolean context

另一方面,条件链的末尾可以使用任何类型的表达式。它将根据前面的条件进行评估并返回:

julia> true && (x = (1, 2, 3))
(1, 2, 3)

julia> false && (x = (1, 2, 3))
false

重复评估:循环

有两种重复计算表达式的构造:while循环和for循环。这是while循环的示例:

julia> i = 1;

julia> while i <= 5
           println(i)
           i += 1
       end
1
2
3
4
5

while循环计算的条件表达式(i <= 5在这种情况下),并且只要它保持true,保持还评估的主体while环。如果条件表达式是首次到达循环falsewhile,则永远不会评估主体。

for循环使常见的重复评估习惯用法更易于编写。由于像上面的while循环一样向上和向下计数非常普遍,因此可以使用for循环更简洁地表示:

julia> for i = 1:5
           println(i)
       end
1
2
3
4
5

此处1:5是一个Range对象,代表数字1、2、3、4、5的序列。for循环遍历这些值,依次将每个值分配给变量i。先前的while循环形式和for循环形式之间的一个相当重要的区别是变量可见的范围。如果i没有以for循环形式在其他作用域中引入变量,则该变量仅在for循环内部可见,而在此之后则不可见。您将需要一个新的交互式会话实例或一个不同的变量名称来对此进行测试:

julia> for j = 1:5
           println(j)
       end
1
2
3
4
5

julia> j
ERROR: UndefVarError: j not defined

参见变量的作用域的变量范围的详细说明,以及它是如何工作的朱莉娅。

通常,for循环构造可以遍历任何容器。在这些情况下,通常使用替代(但完全等效)关键字inor 代替=,因为它使代码更清晰地阅读:

julia> for i in [1,4,0]
           println(i)
       end
1
4
0

julia> for s ∈ ["foo","bar","baz"]
           println(s)
       end
foo
bar
baz

各种类型的可迭代容器将在手册的后续部分中介绍和讨论(例如,参见“ 多维数组”)。

有时很方便的是while在伪造测试条件之前终止重复a 或在for到达可迭代对象的末尾之前停止循环迭代。这可以通过break关键字完成:

julia> i = 1;

julia> while true
           println(i)
           if i >= 5
               break
           end
           i += 1
       end
1
2
3
4
5

julia> for i = 1:1000
           println(i)
           if i >= 5
               break
           end
       end
1
2
3
4
5

如果没有break关键字,上述while循环将永远不会自行终止,并且该for循环最多可重复执行1000次。这些循环都可以通过使用提前退出break

在其他情况下,能够停止迭代并立即继续进行下一个迭代很方便。该continue关键字实现这一点:

julia> for i = 1:10
           if i % 3 != 0
               continue
           end
           println(i)
       end
3
6
9

这是一个有些人为的示例,因为我们可以通过消除条件并将println调用放置在if块内来更清楚地产生相同的行为。在实际使用中,在之后需要评估更多的代码continue,并且经常有多个要调用的点continue

多个嵌套for循环可以组合成单个外部循环,从而形成其可迭代对象的笛卡尔积:

julia> for i = 1:2, j = 3:4
           println((i, j))
       end
(1, 3)
(1, 4)
(2, 3)
(2, 4)

break在这样一个循环中的一条语句会退出整个循环嵌套,而不仅仅是内部循环。

异常处理

当发生意外情况时,函数可能无法将合理的值返回给其调用方。在这种情况下,对于特殊情况,最好终止程序,打印诊断错误消息,或者如果程序员提供了处理此类特殊情况的代码,则允许该代码采取适当的措施。

内置Exception

Exception发生意外情况时将抛出s。Exception下面列出的内置s中断了正常的控制流程。

Exception
ArgumentError
BoundsError
CompositeException
DivideError
DomainError
EOFError
ErrorException
InexactError
InitError
InterruptException
InvalidStateException
KeyError
LoadError
OutOfMemoryError
ReadOnlyMemoryError
RemoteException
MethodError
OverflowError
ParseError
SystemError
TypeError
UndefRefError
UndefVarError
UnicodeError

例如,该sqrt()函数将DomainErrorif应用于负实数值:

julia> sqrt(-1)
ERROR: DomainError:
sqrt will only return a complex result if called with a complex argument. Try sqrt(complex(x)).
Stacktrace:
 [1] sqrt(::Int64) at ./math.jl:434

您可以通过以下方式定义自己的异常:

julia> struct MyCustomException <: Exception end

throw()功能

可以使用显式创建异常throw()。例如,如果参数为负,则可以将仅为非负数定义的函数写入throw()DomainError

julia> f(x) = x>=0 ? exp(-x) : throw(DomainError())
f (generic function with 1 method)

julia> f(1)
0.36787944117144233

julia> f(-1)
ERROR: DomainError:
Stacktrace:
 [1] f(::Int64) at ./none:1

请注意,DomainError不带括号不是一个例外,而是一种例外。需要调用它以获得一个Exception对象:

julia> typeof(DomainError()) <: Exception
true

julia> typeof(DomainError) <: Exception
false

此外,某些异常类型采用一个或多个用于错误报告的参数:

julia> throw(UndefVarError(:x))
ERROR: UndefVarError: x not defined

遵循以下UndefVarError编写方式,可以通过自定义异常类型轻松实现此机制:

julia> struct MyUndefVarError <: Exception
           var::Symbol
       end

julia> Base.showerror(io::IO, e::MyUndefVarError) = print(io, e.var, " not defined")
注意

编写错误消息时,最好使第一个单词小写。例如,size(A) == size(B) || throw(DimensionMismatch("size of A not equal to size of B"))

优先于

size(A) == size(B) || throw(DimensionMismatch("Size of A not equal to size of B"))

但是,有时候保留大写的第一个字母是有意义的,例如,如果函数的参数是大写字母:size(A,1) == size(B,2) || throw(DimensionMismatch("A has first dimension..."))

失误

error()函数用于产生ErrorException中断正常控制流程的。

假设如果要取负数的平方根,我们想立即停止执行。为此,我们可以定义sqrt()函数的挑剔版本,如果其参数为负,则会引发错误:

julia> fussy_sqrt(x) = x >= 0 ? sqrt(x) : error("negative x not allowed")
fussy_sqrt (generic function with 1 method)

julia> fussy_sqrt(2)
1.4142135623730951

julia> fussy_sqrt(-1)
ERROR: negative x not allowed
Stacktrace:
 [1] fussy_sqrt(::Int64) at ./none:1

如果fussy_sqrt从另一个函数用负值调用了if ,而不是尝试继续执行该调用函数,而是立即返回,并在交互式会话中显示错误消息:

julia> function verbose_fussy_sqrt(x)
           println("before fussy_sqrt")
           r = fussy_sqrt(x)
           println("after fussy_sqrt")
           return r
       end
verbose_fussy_sqrt (generic function with 1 method)

julia> verbose_fussy_sqrt(2)
before fussy_sqrt
after fussy_sqrt
1.4142135623730951

julia> verbose_fussy_sqrt(-1)
before fussy_sqrt
ERROR: negative x not allowed
Stacktrace:
 [1] fussy_sqrt at ./none:1 [inlined]
 [2] verbose_fussy_sqrt(::Int64) at ./none:3

警告和信息性消息

Julia还提供了其他功能,这些功能可以将消息写入标准错误I / O,但不抛出任何Exceptions,因此不中断执行:

julia> info("Hi"); 1+1
INFO: Hi
2

julia> warn("Hi"); 1+1
WARNING: Hi
2

julia> error("Hi"); 1+1
ERROR: Hi
Stacktrace:
 [1] error(::String) at ./error.jl:21

try/catch声明

try/catch语句允许对Exception进行测试。例如,可以编写自定义平方根函数来使用Exceptions 自动按需调用实数或复数平方根方法:

julia> f(x) = try
           sqrt(x)
       catch
           sqrt(complex(x, 0))
       end
f (generic function with 1 method)

julia> f(1)
1.0

julia> f(-1)
0.0 + 1.0im

重要的是要注意,在实际代码中计算此功能时,一个将与x零进行比较,而不是捕获异常。这个例外比简单地比较和分支慢得多。

try/catch语句还允许Exception将_保存在变量中。在这个人为的示例中,以下示例计算xif 的第二个元素的平方根x是可索引的,否则假定x为实数并返回其平方根:

julia> sqrt_second(x) = try
           sqrt(x[2])
       catch y
           if isa(y, DomainError)
               sqrt(complex(x[2], 0))
           elseif isa(y, BoundsError)
               sqrt(x)
           end
       end
sqrt_second (generic function with 1 method)

julia> sqrt_second([1 4])
2.0

julia> sqrt_second([1 -4])
0.0 + 2.0im

julia> sqrt_second(9)
3.0

julia> sqrt_second(-9)
ERROR: DomainError:
Stacktrace:
 [1] sqrt_second(::Int64) at ./none:7

请注意,后面的符号catch将始终被解释为异常的名称,因此try/catch在单行上编写表达式时需要格外小心。如果发生错误,以下代码将无法返回值x

try bad() catch x end

而是使用分号或在以下位置插入换行符catch

try bad() catch; x end

try bad()
catch
    x
end

catch条款并非严格必要;省略时,默认返回值为nothing

julia> try error() end # Returns nothing

在的力量try/catch结构就在于能立即放松身心的深度嵌套计算在调用函数的栈一个更高的水平。在某些情况下没有发生错误,但是希望能够使堆栈退卷并将值传递到更高的级别。朱莉娅提供rethrow()backtrace()以及catch_backtrace()更先进的错误处理功能。

finally 条款

在执行状态更改或使用资源(如文件)的代码中,通常需要在代码完成后执行清理工作(例如关闭文件)。异常可能会使此任务复杂化,因为它们可能导致代码块在到达正常末端之前退出。该finally关键字提供了一种方式来运行一些代码,当程序退出的给定块,不管它是如何退出。

例如,这是我们可以保证关闭打开的文件的方法:

f = open("file")
try
    # operate on file f
finally
    close(f)
end

当控制离开该try块时(例如,由于return或刚刚正常完成),close(f)将执行。如果该try块由于异常而退出,则该异常将继续传播。阿catch块可结合tryfinally为好。在这种情况下,该finally块将在catch处理完错误后运行。

任务(又名协程)

任务是一种控制流功能,它允许以灵活的方式暂停和恢复计算。有时会用其他名称来调用此功能,例如对称协程,轻量级线程,协作式多任务处理或单次连续。

当一个计算工作(实际上是执行一个特定功能)指定为a时Task,可以通过切换到另一个来中断它TaskTask稍后可以恢复原件,这时它将在停下来的位置重新拾取。首先,这似乎类似于函数调用。但是,有两个主要区别。首先,切换任务不占用任何空间,因此可以在不消耗调用堆栈的情况下进行任意数量的任务切换。其次,与函数调用不同,任务之间的切换可以按任何顺序进行,在这种情况下,被调用函数必须在控制返回到调用函数之前完成执行。

这种控制流程可以使解决某些问题变得更加容易。在某些问题中,各种所需的工作与功能调用之间并不是很自然的联系。在需要完成的工作中,没有明显的“呼叫者”或“被呼叫者”。一个例子是生产者-消费者问题,其中一个复杂的过程正在产生值,而另一个复杂的过程正在消耗它们。消费者不能简单地调用生产者函数来获取值,因为生产者可能要生成更多的值,因此可能还没有准备好返回。有了任务,生产者和消费者都可以根据需要运行,并根据需要来回传递值。

Julia提供了Channel解决此问题的机制。A Channel是可等待的先进先出队列,可以有多个任务对其进行读写操作。

让我们定义一个生产者任务,该任务通过put!调用产生值。要消耗值,我们需要安排生产者在新任务中运行。Channel接受1-arg函数作为参数的特殊构造函数可用于运行绑定到通道的任务。然后,我们可以take!()从channel对象重复进行赋值:

julia> function producer(c::Channel)
           put!(c, "start")
           for n=1:4
               put!(c, 2n)
           end
           put!(c, "stop")
       end;

julia> chnl = Channel(producer);

julia> take!(chnl)
"start"

julia> take!(chnl)
2

julia> take!(chnl)
4

julia> take!(chnl)
6

julia> take!(chnl)
8

julia> take!(chnl)
"stop"

考虑这种行为的一种方法producer是能够多次返回。在对的调用之间put!(),生产者的执行被挂起,并且消费者拥有控制权。

返回的Channel值可用作for循环中的可迭代对象,在这种情况下,循环变量采用所有产生的值。通道关闭时,循环终止。

julia> for x in Channel(producer)
           println(x)
       end
start
2
4
6
8
stop

注意,我们不必显式关闭生产者中的通道。这是因为将a绑定Channel到a 的行为Task()将通道的开放生存期与绑定任务的生存期相关联。任务终止时,通道对象自动关闭。可以将多个通道绑定到一个任务,反之亦然。

虽然Task()构造函数期望一个0参数的函数,但是Channel()创建通道绑定任务的方法期望一个接受单个type参数的函数Channel。常见的模式是对生产者进行参数化,在这种情况下,需要部分函数应用程序来创建0或1参数匿名函数

对于Task()对象,这可以直接完成,也可以通过使用便捷宏来完成:

function mytask(myarg)
    ...
end

taskHdl = Task(() -> mytask(7))
# or, equivalently
taskHdl = @task mytask(7)

为了编排更先进的工作分布模式,bind()并且schedule()可以配合使用Task(),并Channel()构造明确链接一组信道的一组生产者/消费者的任务。

请注意,当前Julia任务尚未计划在单独的CPU内核上运行。真正的内核线程将在“ 并行计算 ”主题下进行讨论。

核心任务操作

让我们探索底层结构yieldto()以了解任务切换的工作原理。yieldto(task,value)挂起当前任务,切换到指定的任务,task并使该任务的最后一次yieldto()调用返回指定的任务value。注意,这yieldto()是使用任务样式控制流所需的唯一操作;而不是调用并返回,我们始终只是切换到其他任务。这就是为什么此功能也称为“对称协程”的原因;每个任务都使用相同的机制来回切换。

yieldto()它功能强大,但是大多数任务使用并不直接调用它。考虑为什么会这样。如果您退出当前任务,则可能会在某个时候切换回该任务,但是知道何时切换回去,以及知道哪个任务负责切换,可能需要大量的协调。例如,put!()take!()是阻塞操作,当它们在通道的上下文中使用时,它们保持状态以记住使用者是谁。put!()比起底层工具,更易于使用的是无需手动跟踪消耗任务的方法yieldto()

除了yieldto(),还需要一些其他基本功能才能有效地使用任务。

任务和事件

大多数任务切换是由于等待事件(例如I / O请求)而发生的,并且由标准库中包含的调度程序执行。调度程序维护可运行任务的队列,并执行事件循环,该循环根据外部事件(例如消息到达)重新启动任务。

等待事件的基本功能是wait()。几个对象的实现wait(); 例如,给定一个Process对象,wait()将等待其退出。wait()通常是隐式的;例如,wait()在呼叫内可能会发生,read()以等待数据可用。

在所有这些情况下,wait()最终都在Condition对象上运行,该对象负责排队和重新启动任务。当任务调用wait()Condition,该任务被标记为不可运行,被添加到条件队列中,并切换到调度程序。然后,调度程序将选择另一个要运行的任务,或者阻止等待外部事件。如果一切顺利,最终事件处理程序将notify()对该条件进行调用,这将导致等待该条件的任务再次变为可运行状态。

Task最初调度程序不知道通过调用显式创建的任务。这使您可以根据需要手动管理任务yieldto()。但是,当此类任务等待事件发生时,它仍会如您所期望的那样在事件发生时自动重新启动。也可以使调度程序尽可能地运行任务,而不必等待任何事件。这可以通过调用schedule()或使用@schedule@async宏来完成(有关更多详细信息,请参见并行计算)。

任务状态

任务有一个state描述其执行状态的字段。A 是以下符号之一:Task state

符号含义
:runnable当前正在运行,或可以切换到
:waiting阻止等待特定事件
:queued在即将重新启动的调度程序的运行队列中
:done成功完成执行
:failed完成未捕获的异常

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

bowen2006

你的鼓励是我的动力

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

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

打赏作者

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

抵扣说明:

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

余额充值