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

简介:

现有 Delphi 项目迁移到 Tiburon 中的注意事项 
翻译:峪飞鹰
2008-08-30

随着 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;

并不是向目标中填充  0909,而是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; 
{
IFNDEF UNICODE} var   L, D: Integer; {
ENDIF}
begin
{ IFDEF UNICODE} 
  FList[FUsed] := StrNew(PWideChar(S)); 
{
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}来指示编译器允许指针运算,特别是允许 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 - #D800 - #DBFF 是高 surrogate, #DC00 - #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 下。当然,如果您开发的是多版本控件,或者是希望项目能在多个版本中编译,您最好根据这些特性定义适当的编译条件,以便让代码更好的被更低的版本的编译器 支持和编译。

本文基于 Tiburon 帮助编写,如有翻译错误或描述不准确的地方欢迎大家指正!相信这次 Delphi / C++ Builder 2009 将是广大爱好者最喜欢的版本之一。

全文完。


本文转自 不得闲 博客园博客,原文链接:http://www.cnblogs.com/DxSoft/archive/2011/01/01/1923946.html   ,如需转载请自行联系原作者


相关文章
|
小程序 开发工具 开发者
小程序发布注意事项
小程序发布注意事项
90 0
|
开发工具 数据安全/隐私保护 git
团队开发注意事项
团队开发注意事项
|
存储 安全 数据安全/隐私保护
|
安全 数据安全/隐私保护
定制开发混币器软件需要注意事项
定制开发混币器软件需要注意事项
|
搜索推荐 Java 编译器
安装编译器之后使用前的准备工作(常见配置、了解并在过程中不断熟悉常用的快捷键、导入和导出项目)
安装编译器之后使用前的准备工作(常见配置、了解并在过程中不断熟悉常用的快捷键、导入和导出项目)
320 0
安装编译器之后使用前的准备工作(常见配置、了解并在过程中不断熟悉常用的快捷键、导入和导出项目)
|
Serverless PHP
thinkphp项目迁移到函数计算
前言 阿里云函数计算 Function Compute(FC),旨在帮助用户采用弹性伸缩、动态分配资源的方式,来执行业务函数。让用户无需购买部署服务器,无需考虑业务负载,就能快速搭建可处理高并发的后台服务。
28253 0
项目编写时的注意事项
标签(空格分隔): 个人总结 最近学了很多知识点,也通过几个作业、项目将这些知识点串联在一起,相互协作完成一个程序丰富的功能。 在写项目的时候,最大的困难是没有一个很好的编写流程。虽然对于项目的每一个功能都可以单独的分析并提出解决方法和思路,也可以写出对应的知识点来完成此功能。