从RawByteString转换为string是否会自动调用UTF8Decode函数?

5
我希望将任意二进制数据以BLOB形式存储到SQLite数据库中。
使用以下函数将数据作为value添加:
procedure TSQLiteDatabase.AddParamText(name: string; value: string);

现在我想将一个WideString转换为它的UTF8表示形式,以便可以存储到数据库中。调用UTF8Encode并将结果存储到数据库后,我发现数据库中的数据没有进行UTF8解码。相反,在我的计算机地区设置中,它被编码为AnsiString。
我运行了以下测试来检查发生了什么:
type
  {$IFDEF Unicode}
  TBinary = RawByteString;
  {$ELSE}
  TBinary = AnsiString;
  {$ENDIF}

procedure TForm1.Button1Click(Sender: TObject);
var
  original: WideString;
  blob: TBinary;
begin
  original := 'ä';
  blob     := UTF8Encode(original);

  // Delphi 6:   ä (as expected)
  // Delphi XE4: ä  (unexpected! How did it do an automatic UTF8Decode???)
  ShowMessage(blob);
end;

在将字符“ä”转换为UTF8后,内存中的数据是正确的(“ä”),但是,一旦我将TBinary值传递给一个函数(作为stringAnsiString),Delphi XE4会出现“魔法类型转换”,原因我不知道。

我已经找到了一个解决方法来避免这种情况:

function RealUTF8Encode(AInput: WideString): TBinary;
var
  tmp: TBinary;
begin
  tmp := UTF8Encode(AInput);
  SetLength(result, Length(tmp));
  CopyMemory(@result[1], @tmp[1], Length(tmp));
end;

procedure TForm1.Button2Click(Sender: TObject);
var
  original: WideString;
  blob: TBinary;
begin
  original := 'ä';
  blob     := RealUTF8Encode(original);

  // Delphi 6:   ä (as expected)
  // Delphi XE4: ä (as expected)
  ShowMessage(blob);
end;

然而,我认为使用 RealUTF8Encode 的解决方法很麻烦,我想知道为什么简单的调用 UTF8Encode 没有生效以及是否有更好的解决方案。

1个回答

8
在 Delphi 的 Ansi 版本中(D2009 之前),UTF8Encode() 返回一个 UTF-8 编码的 AnsiString。在 Unicode 版本(D2009 及以后版本)中,它返回一个 UTF-8 编码的 RawByteString,并将其赋予 CP_UTF8(65001)的代码页。
在 Ansi 版本中,ShowMessage() 接收一个 AnsiString 作为输入,而 UTF-8 字符串是一个 AnsiString,因此它会按原样显示。在 Unicode 版本中,ShowMessage() 接收一个 UTF-16 编码的 UnicodeString 作为输入,因此使用分配给它的 CP-UTF8 代码页将 UTF-8 编码的 RawByteString 转换为 UTF-16 编码。
如果您实际上直接将 blob 数据写入数据库,则会发现它可能是 UTF-8 编码的,具体取决于您如何编写它。但是,您的方法是错误的;在这种情况下使用 RawByteString 是不正确的。RawByteString 只应用作过程参数。不要将其用作局部变量。这是您问题的根源。来自 文档

RawByteString 的目的是减少读取字符串数据的多个程序重载的需求。这意味着处理字符串的例程的参数通常应为 RawByteString 类型,而不考虑字符串的代码页。

RawByteString 应仅用作参数类型,并且仅在需要 AnsiStrings 的多个代码页的多个重载的情况下使用。这样的例程需要注意运行时字符串的实际代码页。

对于 Delphi 的 Unicode 版本,我建议您使用 TBytes 来保存您的 UTF-8 数据,并使用 TEncoding 进行编码:
var
  utf8: TBytes;
  str: string;
...
str := ...;
utf8 := TEncoding.UTF8.GetBytes(str);

您正在寻找一种数据类型,当传递时不执行隐式文本编码,TBytes就是这种类型。
对于Delphi的Ansi版本,您可以像以前一样使用AnsiStringWideStringUTF8Encode
但是,我个人建议您始终使用TBytes来处理您的UTF-8数据。因此,如果您需要一个支持Ansi和Unicode编译器(呃!)的单一代码库,则应创建一些帮助程序:
{$IFDEF Unicode}
function GetUTF8Bytes(const Value: string): TBytes;
begin
  Result := TEncoding.UTF8.GetBytes(Value);
end;
{$ELSE}
function GetUTF8Bytes(const Value: WideString): TBytes;
var
  utf8str: UTF8String;
begin
  utf8str := UTF8Encode(Value);
  SetLength(Result, Length(utf8str));
  Move(Pointer(utf8str)^, Pointer(Result)^, Length(utf8str));
end;
{$ENDIF}

Ansi版本比必要的堆分配更多。您可能希望编写一个更有效的辅助程序,直接调用WideCharToMultiByte()

在Delphi的Unicode版本中,如果由于某种原因您不想使用TBytes来处理UTF-8数据,则可以改用UTF8String代替。这是一种特殊的AnsiString,它始终使用CP_UTF8代码页。然后您可以编写如下内容:

var
  utf8: UTF8String;
  str: string;
....
utf8 := str;

编译器将在幕后为您从UTF-16转换为UTF-8。但我不建议这样做,因为它不支持移动平台或Delphi的Ansi版本(UTF8String自Delphi 6以来一直存在,但直到Delphi 2009才成为真正的UTF-8字符串)。这也是我建议您使用TBytes的原因之一。在Unicode时代,我的理念是,有本地的string类型,任何其他编码都应该保存在TBytes中。


顺便说一句,有趣的事实是调用两次 WideCharToMultiByte(一次用于确定输出大小,一次用于实际转换)比使用内存中的 StrRec.codePage 操纵并进行单个 UTF8Encode 要快 (而不是调用 SetCodePage,这会导致不必要的 UniqueString)。 我知道这将是一个肮脏的黑客,但我想测试一下是否仍然可以比 WinAPI 更快。 也许我在实验过程中做错了什么。 - Daniel Marschall
你可以通过一次调用WideCharToMultiByte函数来实现。看一下UTF8编码的GetBytes是如何实现的。它使用过度分配,然后再缩小大小。 - David Heffernan
这是 UTF-16 元素数量的三倍,而不是 Unicode 代码点数量的三倍。BMP 之外的点需要两个 UTF-16 元素来编码。三倍是正确的。 - David Heffernan
1
反思后,两次调用和一次分配可能比一次调用和两次分配更快。这可能取决于输入。如果您在意速度,请使用真实数据进行测试。 - David Heffernan
为什么要两次分配内存?如果我使用 SetLength 将字符串/数组缩短到实际长度,数据将不会被重新定位。 - Daniel Marschall
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接