您的位置:首页 > 编程语言 > Delphi

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

2011-01-01 23:43 489 查看
现有 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;

并不是向目标中填充 $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
下。当然,如果您开发的是多版本控件,或者是希望项目能在多个版本中编译,您最好根据这些特性定义适当的编译条件,以便让代码更好的被更低的版本的编译器
支持和编译。

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

全文完。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: