Delphi2009迁移注意事项

现有 Delphi 项目迁移到 Tiburon 中的注意事项


随着 Embarcadero 8 月 25 号发布 RAD Studio 2009 (Tiburon) 以来(Tiburon 的 RTM 日期可能要延后到 9 - 10 月),随着 Tiburon 全面支持 Unicode,现有的 Delphi / C++ Builder 项目要迁移到 Unicode 下应该注意些什么也成为大家最为关心的问题。Tiburon 对 Unicode 的支持不仅仅是将原来 类型映射为 AnsiString 的 String 类型直接改成 WideString,而是对 AnsiString 结构作出修改,同时增加了 UnicodeString 类型来完美支持 Unicode。这意味着,要想平稳迁移到 Unicode 下,程序员不得不对现有代码作出一定的修改。

在 Tiburon 以前的版本中,AnsiString 和 WideString 除了 data size 不同外,在功能上是相同的。早先版本的 AnsiString 的结构如下:

Format of AnsiString Data Type  

Reference Count    Length    String Data (Byte sized)    Null Term  
-8          -4           0           Length   

而这个结构在 Tiburon 中已经发生变化,AnsiString 增加了两个新的 fields, 一个是 CodePage,一个是 ElemSize,这样做可以让新版的 AnsiString 和 UnicodeString 在结构上保持一致。

而 WideString 类型在早先的版本中用来保存双字节数据。其本质和 Windows BSTR 是一样的。在 Tiburon 中 WideString 仍然是为 COM 保持兼容的,也就是说它依然没有引用计数,相比较而言,UnicodeString 在性能和效率上将会是 COM 以外的程序首选的字符类型。

闪亮登场的 UnicodeString 类型

Tiburon 中,新的、默认的 string 就是 UnicodeString。这个类型既可以包含 ANSI 字符,也可以包含 Unicode 字符。下面是 UnicodeString 类型的结构:

Format of UnicodeString Data Type  

CodePage   Element Size   Reference Count    Length    String Data (element sized)    Null Term  
-12    -10    -8    -4    0    Length * elementsize   


UnicodeString 和 AnsiString 都是如上的结构,尽管 UnicodeString 包含是双字节数据,AnsiString 包含的是单字节的。

用 Object Pascal 语言来描述 UnicodeString 的结构,应该是这样:

type
StrRec = record
    CodePage: Word;
    ElemSize: Word;
    refCount: Integer;
    Len: Integer;
    case Integer of
      1: array[0..0] of AnsiChar;
      2: array[0..0] of WideChar;
end;

UnicodeString 增加了 code page 字段和 element size 来描述字符串内容,这使得 UnicodeString 和其它类型的字符串可以很好的相兼容,所以 AnsiString 和 UnicodeString 可以很方便的互相转换,唯一要注意的是,当把 UnicodeString 向下转型到 AnsiString 的时候,可能会丢失数据,因此强烈建议你不要这么做。UnicodeString 保存的是 UTF-16 字符。

在旧的环境下,可以使用编译标志 Unicode 来判断编译环境是否支持 UnicodeString,以便您可以在同一套代码中维护不同版本的字符支持环境。编译指令如下:

Delphi 使用:
     {$IFDEF Unicode}
C++ Builder 使用:
     #ifdef _DELPHI_STRING_UNICODE

变化概要:

String 类型映射为 UnicodeString 而不是 AnsiString
Char 类型映射为 WideChar(2 bytes not 1 byte), 并且是 UTF-16 字符
PChar 类型映射为 PWideChar
C++ 中,System::String 映射到 UnicodeString 类
Delphi 中,AnsiString 映射为早先版本中默认的 string
未变化概要:

AnsiString
WideString
AnsiChar
PAnsiChar
隐式转换仍然可用
用户的活动页代码(The user's active code page)控制着模式(ANSI vs. Unicode),所以 AnsiString 仍然可以支持
由于这些变化,代码编写上也出现了一些值得注意的情况,特别是在你打算将旧有的项目迁移到 Tiburon 下时更是如此。下面就列出一些发生的变化情况以及编写代码时应该注意的注意事项。

下面的操作将不再依赖字符 Size:

合并字符串

+
+
+
Concat( , )
标准字符串函数

Length()返回字符元素的长度,此值可能和字符在字节长度上并不匹配。SizeOf 函数则返回数据的字节长度,这意味着 SizeOf 和 Length 的返回值可能是不同的
Copy(, , )返回的 SubString 基于字符元素
Pos(,)返回第一个字符元素的序号
操作


CompareStr()
CompareText()
...
FillChar()

FillChar(Rect, SizeOf(Rect), #0)
FillChar(WndClassEx, SizeOf(TWndClassEx), #0). 使用的时候注意 WndClassEx.cbSize := SizeOf(TWndClassEx)
Windows API

API 默认使用 WideString (*W)形态的版本
PChar()具有相同的语义
范例:

GetModuleFileName:
function ModuleFileName(Handle: HMODULE): string;
var
   Buffer: array[0..MAX_PATH] of Char;
begin
   SetString(Result, Buffer, GetModuleFileName(Handle, Buffer, Length(Buffer)));
end;

GetWindowText:
function WindowCaption(Handle: HWND): string;
begin
   SetLength(Result, 1024);
   SetLength(Result, GetWindowText(Handle, PChar(Result), Length(Result)));
end;

字符串索引:
function StripHotKeys(const S: string): string;
var
   I, J: Integer;
   LastChar: Char;
begin
   SetLength(Result, Length(S));
   J := 0;
   LastChar := #0;
   for I := 1 to Length(S) do
   begin
     if (S[I] <> '&') or (LastChar = '&') then
     begin
       Inc(J);
       Result[J] := S[I];
     end;
     LastChar := S[I];
   end;
   SetLength(Result, J);
end;


接上文

依赖字符 Size 的代码结构:

在 Tiburon 中,下列列表中列出的这些函数和特性依赖字符 Size,并且已经包含了一个“轻便”的版本,迁移代码的时候只需要将列出的代码迁移到后面提供的轻便版本即可。


SizeOf() 替换为 Length()
范例:
var
   Count: Integer;
   Buffer: array[0..MAX_PATH - 1] of Char;
begin
   // 现有代码 - 当 string = UnicodeString 的时候这段代码是错的
   Count := SizeOf(Buffer);
   GetWindowText(Handle, Buffer, Count);
          
   // 正确的应该是下面这样
   Count := Length(Buffer); // <<-- Count 应该是 Chars 而不是 Bytes
   GetWindowText(Handle, Buffer, Count);
end;
SizeOf 返回的是数组的字节数,而 GetWindowText 的 Counts 参数需要的是字符数,所以这里需要把 SizeOf 换成 Length。

Move(... CharCount) 替换为 Move( ,,, CharCount * SizeOf(Char))
var
    Count: Integer;
    Buf1, Buf2: array[0..255] of Char;
begin
   // 现有代码 - 当 string = UnicodeString (char = 2 bytes) 时,下面的代码是错误的
   Count := Length(Buf1);
   Move(Buf1, Buf2, Count);
  
   // 正确的写法应该是
   Count := SizeOf(Buf1);           // <<-- Specify buffer size in bytes
   Count := Length(Buf1) * SizeOf(Char); // <<-- Specify buffer size in bytes
   Move(Buf1, Buf2, Count);
end;
由于 Length 返回的是字符数,而 Move 的 Count 参数需要的是字节数,所以应该用 SizeOf 或者 Length(Buf1) * SizeOf(Char) 替换 Length(Buf1)。

Stream 的 Read/Write 替换为 AnsiString, SizeOf(Char),或者使用 TEncoding 类
调用 Read/ReadBuffer 方法的范例:
var
   S: string;
   L: Integer;
   Stream: TStream;
   Temp: AnsiString;
begin
   // 现有代码- 当 string = UnicodeString 时它是不正确的
   Stream.Read(L, SizeOf(Integer));
   SetLength(S, L);
   Stream.Read(Pointer(S)^, L);
  
   // 正确的 Unicode 写法如下
   Stream.Read(L, SizeOf(Integer));
   SetLength(S, L);
   Stream.Read(Pointer(S)^, L * SizeOf(Char));   // <<-- Specify buffer size in bytes
  
   //正确的 Ansi 写法如下
   Stream.Read(L, SizeOf(Integer));
   SetLength(Temp, L);           // <<-- 使用临时的变量 AnsiString
   Stream.Read(Pointer(Temp)^, L * SizeOf(AnsiChar));   // <<-- Specify buffer size in bytes
   S := Temp;           // <<-- 放宽 string 到 Unicode
end;
上面的解决方案依赖于您存储在 Stream 中的字符串的编码格式,更好的读取和转换他们建议使用 TEncoding 类。

调用 Write/WriteBuffer 的范例:
var
   S: string;
   Stream: TStream;
   Temp: AnsiString;
begin
   // 现有代码 - 当 string = UnicodeString 时它是错的
   Stream.Write(Pointer(S)^, Length(S));
  
   // 正确的读取 Unicode 的代码
   Stream.Write(Pointer(S)^, Length(S) * SizeOf(Char)); // <<-- Specify buffer size in bytes
  
   // 正确的读取 Ansi 的代码
   Temp := S;           // <<-- Use temporary AnsiString
   Stream.Write(Pointer(Temp)^, Length(Temp) * SizeOf(AnsiChar));// <<-- Specify buffer size in bytes
end;
上面的解决方案依赖于您要存储进 Stream 中的字符串的编码格式,建议使用 TEncoding 类来更好的对格式进行处理。

FillChar(, , ) 如果采用 #0 填充, 替换为   * SizeOf(Char);如果填充其它字符,替换为 StringOfChar 函数
范例:
var
   Count: Integer;
   Buffer: array[0..255] of Char;
begin
    // 现有代码 - 当 string = UnicodeString ( char = 2 字节) 时,这段代码是错的
    Count := Length(Buffer);
    FillChar(Buffer, Count, 0);
          
    // 正确的代码应该写作下面这样
    Count := SizeOf(Buffer);           // <<-- Specify buffer size in bytes
    Count := Length(Buffer) * SizeOf(Char); // <<-- Specify buffer size in bytes
    FillChar(Buffer, Count, 0);
end;
Length 返回的是字符数,而 FillChar 的 Count 参数需要的是字节数,所以必须用 SizeOf 替换 Length,或者使用 Length * SizeOf(Char)。

另外,需要注意的是,Tiburon 中 Char 等于 2 个字节,FillChar 填充的时候确是按照 Bytes 来计算的,所以,下面的代码

var
   Buf: array[0..32] of Char;
begin
   FillChar(Buf, Length(Buf), #9);
end;

并不是向目标中填充 $09,而是 $0909,要得到正确的数值,应该改写成下面这样:

var
   Buf: array[0..32] of Char;
begin
   StrPCopy(Buf, StringOfChar(#9, Length(Buf)));
...
end;

GetProcAddress(, )  
由于 GetProcAddres 没有对应的 *W (Unicode) 版本的 API,所以只能使用下面的代码来正确调用它:
procedure CallLibraryProc(const LibraryName, ProcName: string);
var
   Handle: THandle;
   RegisterProc: function: HResult stdcall;
begin
   Handle := LoadOleControlLibrary(LibraryName, True);
   @RegisterProc := GetProcAddress(Handle, PAnsiChar(AnsiString(ProcName)));
end;


RegQueryValueEx 函数
由于 RegQueryValueEx 函数的 Len 指定的是字节数,而不是字符数,所以 Unicode 版本中它的大小是实际需要大小的 2 倍,所以这样的代码:

Len := MAX_PATH;
if RegQueryValueEx(reg, PChar(Name), nil, nil, PByte(@Data[0]), @Len) = ERROR_SUCCESS
then
   SetString(Result, Data, Len - 1) // Len includes #0
else
   RaiseLastOSError;

应该换成下面这样:

Len := MAX_PATH * SizeOf(Char);
if RegQueryValueEx(reg, PChar(Name), nil, nil, PByte(@Data[0]), @Len) = ERROR_SUCCES
then
   SetString(Result, Data, Len div SizeOf(Char) - 1) // Len includes #0, Len contains the number of bytes
else
   RaiseLastOSError;

CreateProcessW 函数
在 Unicode 版本的 CreateProcess 函数中,其行为和 ANSI 的版本略有不同。Unicode 的 CreateProcessW 会改变参数 lpCommandLine 传入的数据,因此调用 CreateProcess / CreateProcessW 的时候,不可以给 lpCommandLine 赋值常量,或者是一个变量指向的常量,否则函数会抛出 access violations 的异常。下面是错误的代码:

// 传入了一个 string 常量
CreateProcess(nil, 'foo.exe', nil, nil, False, 0,
   nil, nil, StartupInfo, ProcessInfo);

// 传入了一个常量表达式
   const
     cMyExe = 'foo.exe'
   CreateProcess(nil, cMyExe, nil, nil, False, 0,
     nil, nil, StartupInfo, ProcessInfo);

// 传入了一个引用计数为 -1 的字符串:
const
   cMyExe = 'foo.exe'
var
   sMyExe: string;
   sMyExe := cMyExe;
   CreateProcess(nil, PChar(sMyExe), nil, nil, False, 0, nil, nil, StartupInfo, ProcessInfo);

LeadBytes 常量
早先的版本中 LeadBytes 常量包含了本地系统中所有可以作为双字节字符 LeadByte 的列表,常常有这样的代码:
if Str[I] in LeadBytes then

现在你需要将它改成调用 IsLeaderChar 函数
if IsLeadChar(Str[I]) then

使用 TMemoryStream 类
当您需要用 TMemoryStream 写入一个文本文件的时候,最好在写入任何字符数据进去之前先写入一个 Byte Order Mark (BOM):
var
   Bom: TBytes;
begin
   ...
   Bom := TEncoding.UTF8.GetPreamble;
   Write(Bom[0], Length(Bom));

而任何写入的字符需要被转换成 UTF-8 编码:
var
   Temp: Utf8String;
begin
   ...
   Temp := Utf8Encode(Str); // Str 是要写入文件的字符
   Write(Pointer(Temp)^, Length(Temp));
//Write(Pointer(Str)^, Length(Str)); 原来写入字符串的代码


接上文


MultiByteToWideChar 函数
调用 Windows API MultiByteToWideChar 函数可以简单的用一个任务替代,下面是一个是用 MultiByteToWideChar 的例子:

procedure TWideCharStrList.AddString(const S: string);
var
   Size, D: Integer;
begin
   Size := SizeOf(S);
   D := (Size + 1) * SizeOf(WideChar);
   FList[FUsed] := AllocMem(D);
   MultiByteToWideChar(0, 0, PChar(S), Size, FList[FUsed], D);
   Inc(FUsed);
end;

转换到 Unicode 下可以写作这样(同时支持 Unicode 和 ANSI 字符):

procedure TWideCharStrList.AddString(const S: string);
{$IFNDEF UNICODE}
var
   L, D: Integer;
{$ENDIF}
begin
{$IFDEF UNICODE}
   FList[FUsed] := StrNew(PWideChar(S));
{$ELSE}
   L := Length(S);
   D := (L + 1) * SizeOf(WideChar);
   FList[FUsed] := AllocMem(D);
   MultiByteToWideChar(0, 0, PAnsiChar(S), L, FList[FUsed], D);
{$ENDIF}
   Inc(FUsed);
end;

SysUtils.AppendStr 函数
AppendStr 函数已经废弃了,因为它与 AnsiString 硬编码在一起,而且没有 Unicode 的版本可以替换,所以下面的代码

AppendStr(String1, String2);

应该换成:

String1 := String1 + String2;

您也可以使用新的 TStringBuilder 类来替换。

使用 Named Threads
现有 Delphi 代码中使用了 Named Threads 的代码必须修改了。在早先的版本中,当你需要在分类(gallery)中用一个新的 Thread Object 去创建一个 Thread 的时候,需要在新的 Thread 单元中建立下面的类型:

type
TThreadNameInfo = record
   FType: LongWord; // must be 0x1000
   FName: PChar; // pointer to name (in user address space)
   FThreadID: LongWord; // thread ID (-1 indicates caller thread)
   FFlags: LongWord; // reserved for future use, must be zero
end;

在调试器中,Named Thread 的处理器期待 FName 成员是 ANSI 字符,不是 Unicode,所以上面的声明必须改成下面这样:

type
TThreadNameInfo = record
   FType: LongWord; // must be 0x1000
   FName: PAnsiChar; // pointer to name (in user address space)
   FThreadID: LongWord; // thread ID (-1 indicates caller thread)
   FFlags: LongWord; // reserved for future use, must be zero
end;

在新版本中上述声明已经修改,提示这段代码是需要您注意早先版本中您手工创建并声明的代码需要您自己修改。

如果您需要在 Named Thread 中使用 Unicode 字符,您必须将字符串格式化成 UTF-8 编码,调试器可以完全支持改编码。例如:

ThreadNameInfo.FName := UTF8String('UnicodeThread_фис');

注意:C++ Builder 里面一直使用的是正确的代码,所以上述问题在 C++ Builder 中并不存在。

使用 PChar 转换的指针运算
在 Tiburon 更早的版本中,并不是所有的指针类型都支持指针运算。因为这样,为了让无类型指针也支持指针运算,许多代码都将其转化成 PChar 操作。现在,可以使用 Tiburon 中的新编译条件 {$POINTERMATH} 来指示编译器允许指针运算,特别是允许 PByte 的指针运算。{$POINTERMATH ON/OFF} 可以打开/禁止对任意指针变量的运算,增减指针实际操作的是指针元素的大小。

下面的例子是一个将某类型指针转换成 PChar 后的指针运算:

function TCustomVirtualStringTree.InternalData(Node: PVirtualNode): Pointer;
begin
   if (Node = FRoot) or (Node = nil) then
     Result := nil
   else
     Result := PChar(Node) + FInternalDataOffset;
end;

您应该将其修改成 PByte 而不是 PChar:

function TCustomVirtualStringTree.InternalData(Node: PVirtualNode): Pointer;
begin
   if (Node = FRoot) or (Node = nil) then
     Result := nil
   else
     Result := PByte(Node) + FInternalDataOffset;
end;

在 上面的例子中,Node 真实的数据不是 PChar 的数据。将其强制转换成 PChar 的操作在早先的版本中是正常的,因为早先版本中 SizeOf(Char) == Sizeof(Byte)。但是现在不同了,所以这样的代码必须从 PChar 改换成 PByte。如果不做这样的更改,返回的 Pointer 将指向错误的数据。

变体开放数组(Variant Open Array)参数
如果你的代码中有使用 TVarRec 类型去处理开放数组的话,你可能需要为其添加对 vtUnicodeString 的支持。参看下列示例:

procedure RegisterPropertiesInCategory(const CategoryName: string;
   const Filters: array of const); overload;
var
I: Integer;
begin
   if Assigned(RegisterPropertyInCategoryProc) then
     for I := Low(Filters) to High(Filters) do
       with Filters[I] do
         case vType of
           vtPointer:
           RegisterPropertyInCategoryProc(CategoryName, nil,
           PTypeInfo(vPointer), );
           vtClass:
           RegisterPropertyInCategoryProc(CategoryName, vClass, nil, );
           vtAnsiString:
           RegisterPropertyInCategoryProc(CategoryName, nil, nil,
           string(vAnsiString));
           vtUnicodeString:
           RegisterPropertyInCategoryProc(CategoryName, nil, nil,
           string(vUnicodeString));
         else
           raise Exception.CreateResFmt(@sInvalidFilter, [I, vType]);
         end;
end;

其他需要注意的代码:

AllocMem(
AnsiChar
of AnsiChar
AnsiString
of Char
Copy(
GetMem(
Length(
PAnsiChar(
Pointer(
Seek(
ShortString
string[
代码中包含上述写法的地方可能需要修改以适应 UnicodeString 的变化。


带字符的集合类型

您可能需要修改下列类型:

in AnsiChar> 这样的代码生成的程序是正确的(>#255 的字符不会包含在集合内)。编译器会提出 "WideChar reduced in set operations" 的警告,基于您代码的需要,您可以关闭这个警告,或者使用 CharInSet 函数替代。
in LeadBytes 全局的 LeadBytes 变量包含的是本地 MBCS Ansi 字符的集合。UTF-16 格式也有 LeadChar 的概念((#$D800 - #$DBFF 是高 surrogate, #$DC00 - #$DFFF 是低 surrogate)。因此建议使用 overload 函数 IsLeadChar 来判断,该函数的 ANSI 版本检测 LeadBytes,WideChar 版本检测 high/low surrogate。
字符分类 使用静态类 TCharacter。Character 单元中提供了一些函数对字符分类:IsDigit, IsLetter, IsLetterOrDigit, IsSymbol, IsWhiteSpace, IsSurrogatePair,等等。
应当心这些结构

您需要检查下列可能引起错误的结构:

模糊的类型转换

AnsiString(Pointer(foo))
检查正确性:代码是什么意图?
可疑的类型转换引发的警告

PChar()
PAnsiChar()
直 接建立、操作、访问 string 的内部结构。例如 AnsiString 的内部结构已经发生变化,所以这样的操作是危险的。您应该使用 StringRefCount, StringCodePage, StringElementSize 等方法来获得额外信息。
控件和类

TStrings: 内部存储的是 UnicodeStrings。
TWideString:(可能被废弃)没有更改,内部使用 WideString (BSTR)
TStringStream

被重写成内部存储 ANSI 字符
字符编码可以被重载
考虑使用 TStringBuilder 替代 TStringStream 来逐步构建字符串
TEncoding

Default 属性是用户活动页码(users’ active code page)
支持 UTF-8
支持 UTF-16, big 和 little endian
支持 Byte Order Mark (BOM)
您可以继承子类实现特殊的编码
Byte Order Mark

BOM 必须添加到文件中以便判断文件的编码方式。

UTF-8 使用 EF BB EF
UTF-16 Little Endian 使用 FF FE
UTF-16 Big Endian 使用 FE FF
做好这些注意事项,将帮助您顺利地把旧有项目迁移到 Tiburon 的 Unicode 下。当然,如果您开发的是多版本控件,或者是希望项目能在多个版本中编译,您最好根据这些特性定义适当的编译条件,以便让代码更好的被更低的版本的编译器支持和编译。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于tensorflow2.x卷积神经网络字符型验证码识别 卷积神经网络(Convolutional Neural Networks, CNNs 或 ConvNets)是一类深度神经网络,特别擅长处理图像相关的机器学习和深度学习任务。它们的名称来源于网络中使用了一种叫做卷积的数学运算。以下是卷积神经网络的一些关键组件和特性: 卷积层(Convolutional Layer): 卷积层是CNN的核心组件。它们通过一组可学习的滤波器(或称为卷积核、卷积器)在输入图像(或上一层的输出特征图)上滑动来工作。 滤波器和图像之间的卷积操作生成输出特征图,该特征图反映了滤波器所捕捉的局部图像特性(如边缘、角点等)。 通过使用多个滤波器,卷积层可以提取输入图像中的多种特征。 激活函数(Activation Function): 在卷积操作之后,通常会应用一个激活函数(如ReLU、Sigmoid或tanh)来增加网络的非线性。 池化层(Pooling Layer): 池化层通常位于卷积层之后,用于降低特征图的维度(空间尺寸),减少计算量和参数数量,同时保持特征的空间层次结构。 常见的池化操作包括最大池化(Max Pooling)和平均池化(Average Pooling)。 全连接层(Fully Connected Layer): 在CNN的末端,通常会有几层全连接层(也称为密集层或线性层)。这些层中的每个神经元都与前一层的所有神经元连接。 全连接层通常用于对提取的特征进行分类或回归。 训练过程: CNN的训练过程与其他深度学习模型类似,通过反向传播算法和梯度下降(或其变种)来优化网络参数(如滤波器权重和偏置)。 训练数据通常被分为多个批次(mini-batches),并在每个批次上迭代更新网络参数。 应用: CNN在计算机视觉领域有着广泛的应用,包括图像分类、目标检测、图像分割、人脸识别等。 它们也已被扩展到处理其他类型的数据,如文本(通过卷积一维序列)和音频(通过卷积时间序列)。 随着深度学习技术的发展,卷积神经网络的结构和设计也在不断演变,出现了许多新的变体和改进,如残差网络(ResNet)、深度卷积生成对抗网络(DCGAN)等。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值