パートIで解説したように、Delphi 2009がデフォルトでUTF-16ベースの文字列を使用するのを確認しました。その結果、既存コードの中の特定のコーディングパターンを変更しなければならないことがあります。一般的に、既存コードの大部分はDelphi 2009で正しく動作します。これから見ていきますが、行わなければならないコード修正のほとんどは、とても独特なもので、かつ、少々難解なものです。しかし、いくつかの特有なコーディングパターンには再検証が必要でしょうし、おそらくは、コードが確実にUnicodeStringとともに正しく動作するように修正が必要でしょう。
例えば、文字列を操作または文字列に対してポインター処理を行うあらゆるコードが、Unicodeに対応できているかどうか検証すべきです。より具体的には、次のようなコードです。
- SizeOf(Char) は 1 だと想定している
- 文字列の長さはその文字列内のバイト数と同じであると想定している
- 何らかの永続化ストレージに対して文字列を読み書きしている、または、データバッファとして文字列を利用している
コード中にこのような仮定が絶対に残らないようにコードを再検証すべきです。1つのバイトはもはや1つの文字を表すわけではないので、永続化ストレージに対して文字列を読み書きしているコードは、正しいサイズのバイト配列を確実に読み書きする必要があります。
一般的に、全てのコード修正は分かり易いものであるはずですし、最小限の労力で行えます。
「そのままで動く」べき領域
この節では、そのまま動作し続けるはずの、且つ、新しいUnicodeStringで適切に動作するための変更は不要であるはずのコードの領域について解説します。Delphi 2009では、ほとんど例外無く、全VCLおよび全RTLは期待した通りに動作するよう更新されました。このようなケースがまさにそうです。例えば、TStringListは完全にUnicode対応になり、既存のTStringListのコードは全て以前と同じように動作するはずです。しかし、TStringListはUnicode特有の動作を行うよう改良されましたが、その新機能を利用したい場合には利用できますし、利用したくない場合には利用する必要はありません。
文字列型の一般的な利用方法
一般的には、文字列型を使用しているコードは以前と同じように動作するはずです。次に解説する場合を除き、文字列変数をAnsiString型で宣言しなおす必要はありません。ストレージのバッファを処理している場合や、データバッファとして文字列を利用しているタイプのコードの場合にのみ、文字列の宣言をAnsiStringへ変更すべきです。
ランタイムライブラリ
ランタイムライブラリに追加された内容についてはパートIIで詳細に解説しました。
この記事ではRTLに新しく追加されたユニット「AnsiString.pas」には言及しません。このユニットは、AnsiStringを利用すると決めたコードや、AnsiStringを利用しなければならないコード用に、後方互換性のために存在しています。
ランタイムライブラリを利用するコードは期待通りに動作しますし、一般的には変更を必要としません。変更を必要とする領域は以下で解説します。
VCL
VCL全体がUnicodeに対応しました。全ての既存のVCLコンポーネントは、以前と同じようにインストールしてすぐに正しく動作します。VCLを使用しているコードの大部分は普通に動作し続けるはずです。私たちは、VCLがUnicodeに対応しつつ後方互換性を保つことを確実にするために多大な労力をかけました。特定の文字列操作を行っていない通常のVCLコードは以前と同様に動作します。
インデックスによる文字列へのアクセス
インデックスによる文字列へのアクセスは以前と全く同じように動作し、文字列にインデックスでアクセスするコードは変更する必要はありません。
var
S: string;
C: Char;
begin
S := ‘This is a string’;
C := S[1];
end;
文字列とLength/Copy/Delete/SizeOf
Copyは変更無しで以前と同じように動作します。DeleteやSysUtilsベースの文字列操作ルーチンも同様です。
Length(SomeString)の呼び出しも、今までどおり渡された文字列の要素数を返します。
全ての文字列の宣言は参照であり、ポインターのサイズは4なので、どの文字列識別子に対するSizeOf呼び出しも4を返します。
いかなる文字列に対するLength呼び出しも、その文字列の要素数を返します。
次のコードを見てください。
var
S: string;
begin
S:= 'abcdefghijklmnopqrstuvwxyz';
WriteLn('Length = ', Length(S));
WriteLn('SizeOf = ', SizeOf(S));
WriteLn('TotalBytes = ', Length(S) * SizeOf(S[1]));
ReadLn;
end.
上のコードは出力内容は以下のようになります。

PCharに対するポインター演算
PCharに対するポインター演算は以前と同じように動作し続けるはずです。コンパイラはPCharのサイズを知っているので、次のようなコードは期待したとおりに動作し続けるはずです。
var
p: PChar;
MyString: string;
begin
...
p := @MyString[1];
Inc(p);
...
end;
このコードは旧バージョンのDelphiと全く同じように動作します。しかしもちろんその型は異なっており、PCharはPWideCharに、 MyStringはUnicodeStringになっています。
ShortString
ShortStringは機能・宣言ともに変更されておらず、以前と同じように動作します。
ShortStringの宣言は指定した個数のAnsiCharをバッファとして割り当てます。次のコードを見てください。
var
S: string[26];
begin
S:= 'abcdefghijklmnopqrstuvwxyz';
WriteLn('Length = ', Length(S));
WriteLn('SizeOf = ', SizeOf(S));
WriteLn('TotalBytes = ', Length(S) * SizeOf(S[1]));
ReadLn;
end.
次のように出力します。

このアルファベットの総バイト数は26で、その変数は複数のAnsiCharを保持していることを示しています。
さらに次のコードをみてください。
type
TMyRecord = record
String1: string[20];
String2: string[15];
end;
このレコードは、以前と全く同じメモリー配置になります。複数のAnsiCharを持つ2つのAnsiStringからなるレコードになります。複数の短い文字列を持つレコードを含むファイルがある場合には、上のコードは以前と同じように動作します。また、レコードを読み書きするいなかるコードも変更せずに以前と同じように動作します。
しかし、CharはWideCharになったことを思い出してください。そのため、もし以下のようにファイルからそれらのレコードを取り出してアクセスし、何かを呼び出すコードがある場合、
var
MyRec: TMyRecord;
SomeChar: Char;
begin
SomeChar := MyRec.String1[3];
...
end;
SomeCharでは、String1[3]のAnsiCharがWideCharへ変換されることを覚えておいてください。このコードが以前と同じように動作するには、SomeCharの宣言を変更してください。
var
MyRec: TMyRecord;
SomeChar: AnsiChar;
begin
SomeChar := MyRec.String1[3];
...
end;
再検証すべき領域
次の節では、Unicodeとの互換性のため、既存のコードで再検証すべき様々なコードパターンについて説明します。CharはWideCharと同じになったので、文字配列や文字列のバイト数に関する想定は無効なこともあります。確実に新しいUnicodeString型と互換性があるように検証すべき、数多くの特定のコードパターンを以下に列挙しています。
SaveToFile/LoadFromFile
SaveToFileとLoadFromFileの呼び出しは、以前のように読み書きしているので、上述した「そのままで動く」の節にあるように正しく動作するでしょう。しかし、それらを利用する際にUnicodeのデータを処理しようとする場合には、新しくオーバーロードされた版を呼び出したいと思うかもしれません。
例えば、TStringsは、以下のようなオーバーロードされたメソッド群を含むようになりました。
procedure SaveToFile(const FileName: string); overload; virtual;
procedure SaveToFile(const FileName: string; Encoding: TEncoding); overload; virtual;
上の2番目のメソッドは、データをどのようにファイルに書き出すかを指定するエンコーディングパラメータを持つ、新しくオーバーロードされたメソッドです。(パートII にTEncoding型の説明があります) 上の最初のメソッドを呼び出した場合は、文字列データは以前と同じようにANSIのデータとして保存されます。そのため、既存のコードは以前と全く同じように動作します。
しかし、もし何らかのUnicodeの文字列データをテキストに書き出したい場合には、特定のTEncoding型を渡して2番目のオーバーロードされたほうを使用する必要があります。そうでない場合には、文字列はANSIのデータとして書き出され、データの欠落が発生することがあります。
そのため、この場合の最適な方法とは、SaveToFileとLoadFromFileの呼び出しを再検証し、データをどのように保存しようとしているのかを示す2番目パラメータを追加することでしょう。しかし、もし今後Unicodeの文字列を追加したり使用しないと思うのであれば、そのままにしておけます。
Chr関数の使用
整数値からCharを生成する必要のある既存のコードはChr関数を利用していることがあります。Chr関数の使い方によっては、以下のようなエラーが発生することがあります。
[DCC Error] PasParser.pas(169): E2010 Incompatible types: 'AnsiChar' and 'Char'
もしChr関数を使用しているコードが戻り値をAnsiCharに代入しているのあれば、Chr関数をAnsiCharへのキャストに置き換えることでこのエラーを除去できます。
つまり、次のコードは
MyChar := chr(i);
以下のように変更可能です。
MyChar := AnsiChar(i);
文字の集合
おそらく、コンパイラが注目するもっとも基本的なコードパターンは文字集合でしょう。かつて文字は1バイトでしたので、複数の文字を集合型に収めるのは何も問題ありませんでした。しかし今やCharはWideCharと宣言されているため、もはや集合型に収めることができなくなりました。ですから、次のようなコードがある場合、
procedure TDemoForm.Button1Click(Sender: TObject);
var
C: Char;
begin
C := Edit1.Text[1];
if C in ['a'..'z', 'A'..'Z'] then
begin
Label1.Caption := 'It is there';
end;
end;
コンパイル時に、以下のような警告が出力されます。
[DCC Warning] Unit1.pas(40): W1050 WideChar reduced to byte char in set expressions. Consider using 'CharInSet' function in 'SysUtils' unit.
これで構わないのであれば、コードをそのままにしておくことができます。コンパイラはあなたが何を行おうとしているのかを「知っています」し、正しいコードを生成します。しかし、もしこの警告を除去したい場合には、新しいCharInSet関数を使うことがでます。
if CharInSet(C, ['a'..'z', 'A'..'Z']) then
begin
Label1.Caption := 'It is there';
end;
CharInSet関数は、Boolean値を返し、警告が出力されることなくコンパイルできます。
データバッファとして文字列を使用する
基本テクニックとして、データバッファとして文字列を使用するというものがあります。これは簡単なので良く用いられるテクニックです。文字列を操作するのは一般的にとても分かり易いものです。しかし、これを行っている既存のコードはほとんどの場合、文字列がUnicodeStringになった事実に適応させる必要があります。
データバッファとして文字列を使用しているコードに対処するには幾つかの手法があります。1番目は、データバッファとして使用している変数を単純にstringではなくAnsiStringとして宣言することです。もし、バッファ中のバイトを操作するのにCharを使用しているコードであれば、それらの変数をAnsiCharとして宣言してください。この手法を選択した場合、全てのコードは以前と同じように動作しますが、文字列バッファにアクセスする全ての変数を明示的にANSI型で宣言してしまったことに注意する必要があります。
2番目は、このような場面に対処にする好ましい手法で、バッファをstring型からバイト配列またはTBytesへ変換することです。TBytesはこの目的のために設計され、これまでstring型を使用していたように動作します。
バッファに対してSizeOfを呼び出す
文字配列に対するSizeOfの呼び出しは、正しく動作させるために再検証すべきです。次のコードを見てください。
procedure TDemoForm.Button1Click(Sender: TObject);
var
var
P: array[0..16] of Char;
begin
StrPCopy(P, 'This is a string');
Memo1.Lines.Add('Length of P is ' + IntToStr(Length(P)));
Memo1.Lines.Add('Size of P is ' + IntToStr(SizeOf(P)));
end;
このコードはMemo1に以下のように表示します。
Length of P is 17
Size of P is 34
上のコードでは、Lengthは与えられた文字列の中の文字数を返します(終端のnull文字も含みます)。しかしSizeOfは、配列によって使用されている総バイト数を返します(この場合は34で、1文字あたり2バイト)。旧バージョンでは、このコードはどちらも17を返していました。
FillCharの使用
FillCharの呼び出しは、文字列や文字とともに利用されている場合には、再検証すべきです。次のコードを見てください。
var
Count: Integer;
Buffer: array[0..255] of Char;
begin
Count := Length(Buffer);
FillChar(Buffer, Count, 0);
Count := SizeOf(Buffer);
Count := Length(Buffer) * SizeOf(Char);
FillChar(Buffer, Count, 0);
end;
Lengthは文字数を返しますが、FillCharはCountにバイト数を要求します。この場合、Lengthの代わりにSizeOf(または、LengthにCharのサイズを掛け合わせたもの)を使用すべきです。
さらに、Charのデフォルトのサイズは2ですから、FillCharは以前のようなChar単位ではなくバイト単位で文字列を埋めます。
例:
var
Buf: array[0..32] of Char;
begin
FillChar(Buf, SizeOf(Buf), #9);
end;
このコードは、配列を「code point $09」ではく「code point $0909」で埋めます。期待する結果を得るには、コードを次のように変更する必要があります。
var
Buf: array[0..32] of Char;
begin
..
StrPCopy(Buf, StringOfChar(#9, Length(Buf)));
..
end;
文字定数の利用
次のコードは、ユーロ記号を認識し、ほとんどのANSIコードページにおいてTrueと評価されるでしょう。
if Edit1.Text[1] = #128 then
しかし、#128 はほとんどのANSIコードページでユーロ記号であるにもかかわらず、Delphi 2009ではFalseと評価されます。#128 はUnicodeでは制御文字なのです。Unicodeではユーロ記号は #$20AC です。
開発者は、Delphi 2009に移行する際、#128 から #255 までの文字を文字定数に変換すべきです。
if Edit1.Text[1] = '€' then
なぜなら、このコードは、ANSIにおける #128 と同様に動作しますし、Delphi 2009において '€' は #$20AC としても動作します(つまりユーロ記号として認識される)。
Moveの呼び出し
文字列や文字配列を使用したMoveの呼び出しは再検証する必要があります。次のコードを見てください。
var
Count: Integer;
Buf1, Buf2: array[0..255] of Char;
begin
Count := Length(Buf1);
Move(Buf1, Buf2, Count);
Count := SizeOf(Buf1);
Count := Length(Buf1) * SizeOf(Char);
Move(Buf1, Buf2, Count);
end;
Lengthは文字数を返しますが、MoveはCountにバイト数を要求します。この場合、Lengthの代わりにSizeOf(または、LengthにCharのサイズを掛け合わせたもの)を使用すべきです。
TStreamのRead/ReadBufferメソッド
文字列や文字配列を使用したTStreamのRead/ReadBufferメソッドの呼び出しは再検証する必要があります。次のコードを見てください。
var
S: string;
L: Integer;
Stream: TStream;
Temp: AnsiString;
begin
Stream.Read(L, SizeOf(Integer));
SetLength(S, L);
Stream.Read(Pointer(S)^, L);
Stream.Read(L, SizeOf(Integer));
SetLength(S, L);
Stream.Read(Pointer(S)^, L * SizeOf(Char));
Stream.Read(L, SizeOf(Integer));
SetLength(Temp, L);
Stream.Read(Pointer(Temp)^, L * SizeOf(AnsiChar));
S := Temp;
end;
補足:この解決法は、読み込まれるデータのフォーマットに依存しています。ストリーム中で適切にテキストをエンコーディングするには、上述した新しいTEncodingクラスが役立ちます。
Write/WriteBuffer
Read/ReadBufferと同様に、文字列や文字配列を使用したTStreamのWrite/WriteBufferメソッドの呼び出しは再検証する必要があります。次のコードを見てください。
var
S: string;
Stream: TStream;
Temp: AnsiString;
begin
Stream.Write(Pointer(S)^, Length(S));
Stream.Write(Pointer(S)^, Length(S) * SizeOf(Char));
Temp := S;
Stream.Write(Pointer(Temp)^, Length(Temp) * SizeOf(AnsiChar));
end;
補足:この解決法は、書き込むデータのフォーマットに依存しています。ストリーム中で適切にテキストをエンコーディングするには、上述した新しいTEncodingクラスが役立ちます。
LeadBytes
次のような呼び出しは
if Str[I] in LeadBytes then
IsLeadChar関数に置き換えてください。
if IsLeadChar(Str[I]) then
TMemoryStream
テキストファイルを出力するのにTMemoryStreamが使われている場面では、そのファイルの先頭に Byte Order Mark (BOM) を出力するのが便利でしょう。以下は、ファイルにBOMを出力する例です。
var
BOM: TBytes;
begin
...
BOM := TEncoding.UTF8.GetPreamble;
Write(BOM[0], Length(BOM));
記述したコードは全てUnicode文字列をUTF8にエンコードするよう変更する必要があります。
var
Temp: Utf8String;
begin
...
Temp := Utf8Encode(Str);
Write(Pointer(Temp)^, Length(Temp) * SizeOf(AnsiChar));
TStringStream
TStringStreamは、新しいTByteStream型から派生するようになりました。TByteStreamには、TStringStreamのバイト配列に直接アクセスできるようにBytesという名前のプロパティが追加されました。文字列がUnicodeベースであるという例外はありますが、TStringStreamは以前と同じように動作します。
MultiByteToWideChar
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に変更した後は、この呼び出しはANSIとUnicodeの両方でコンパイルできるように変更されました。
procedure TWideCharStrList.AddString(const S: string);
var
L, D: Integer;
begin
FList[FUsed] := StrNew(PWideChar(S));
Inc(FUsed);
end;
SysUtils.AppendStr
このメソッドは非推奨となり、AnsiStringを使用するようハードコードされているので、オーバーロードされたUnicode版はありません。
次のような呼び出しは、
AppendStr(String1, String2);
以下のように置き換えてください。
String1 := String1 + String2;
もしくは、こちらのほうがより良いのですが、文字列を連結するには新しいTStringBuilderクラスを使用してください。
GetProcAddress
GetProcAddressの呼び出しは常にPAnsiCharを使用すべきです(SDKにはW版の関数はありません)。
例:
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;
補足:Windows.pasには、この変換を行ってくれるオーバーロードされたメソッドが用意されています。
非charベースのポインター型に対するポインター演算を可能にするためにPChar()キャストを使用する
旧バージョンでは、全てのポインター型がポインター演算をサポートしているわけではありませんでした。このため、ポインター演算を可能にするために様々な非charのポインターをPCharにキャストするというテクニックが使われています。Delphi 2009では、コンパイラ指示子を使うことでポインター演算が可能であり、PByte型に対してのみ可能です。そのため、以下のように、ポインター演算を行う目的でポインターデータをPCharにキャストしているコードがある場合、
function TCustomVirtualStringTree.InternalData(Node: PVirtualNode): Pointer;
begin
if (Node = FRoot) or (Node = nil) then
Result := nil
else
Result := PChar(Node) + FInternalDataOffset;
end;
PCharではなくPByteを使用するよう変更すべきです。
function TCustomVirtualStringTree.InternalData(Node: PVirtualNode): Pointer;
begin
if (Node = FRoot) or (Node = nil) then
Result := nil
else
Result := PByte(Node) + FInternalDataOffset;
end;
上のコードの断片では、Nodeは実際には文字データではありません。Node以後の特定のバイト数分のデータにアクセスするため、ポインター演算を使用したいがためにPCharにキャストしています。従来は、SizeOf(Char) = Sizeof(Byte) であったため動作しました。もはやこれは真ではなく、コードが正しいことを保証するためPCharではなくPByteを使用するように変更する必要があります。変更を行わないと、不正なデータを指す結果になってしまうでしょう。
Variantのオープン配列パラメータ
Variantのオープン配列パラメータを処理するためにTVarRecを利用するコードがある場合は、UnicodeStringの処理に対応させる必要があるかもしれません。UnicodeStringとともに使用するために、新たにvtUnicodeString型が定義されました。UnicodeStringのデータはvUnicodeStringとして保持されています。DesignIntf.pasにある次のコードの断片を見ると、UnicodeString型を処理するために新しいコードが追加された場面が分かります。
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;
CreateProcessW
Unicode版の CreateProcess (CreateProcessW) は、ANSI版に比べると少し動作が異なります。MSDNのlpCommandLineパラメータに関する記述を引用します。
"The Unicode version of this function, CreateProcessW, can modify the contents of this string. Therefore, this parameter cannot be a pointer to read-only memory (such as a const variable or a literal string). If this parameter is a constant string, the function may cause an access violation."
このことにより、CreateProcessを呼び出しているコードがDelphi 2009でコンパイルされると、アクセス違反を引き起こすようになることがあります。
問題のあるコードの例:
文字列定数で渡す:
CreateProcess(nil, 'foo.exe', nil, nil, False, 0, nil, nil, StartupInfo, ProcessInfo);
定数表現で渡す:
const
cMyExe = 'foo.exe'
begin
CreateProcess(nil, cMyExe, nil, nil, False, 0, nil, nil, StartupInfo, ProcessInfo);
end;
-1の参照カウントで文字列を渡す:
const
cMyExe = 'foo.exe'
var
sMyExe: string;
begin
sMyExe := cMyExe;
CreateProcess(nil, PChar(sMyExe), nil, nil, False, 0, nil, nil, StartupInfo, ProcessInfo);
end;
検索すべきコード
コードが適切にUnicode対応していることを確認するため、検索したいであろうコードパターンを以下に列挙しました。
- バッファがUnicode用に適切に使用されていることを確認するため、CharやAnsiCharが使われている箇所を検索する
- 文字への参照がChar(つまりWideChar)になっていることを確認するため、「string[」が使用されている箇所を検索する
- AnsiString/AnsiChar/PAnsiCharが明示的に使用されている場合、それが依然として必要、且つ、適切なものであるかどうかチェックする
- ShortStringが明示的に使用されている場合、それが依然として必要、且つ、適切なものであるかどうかチェックする
- LengthがSizeOfと同じであると想定していないことを確認するため、「Length(」が使用されている箇所を検索する
- 文字列や文字配列に対する適切な処理であることを確認するため、Copy(,Seek(,Pointer(,AllocMem(,GetMem( が使用されている箇所を検索する
これらは、新しいUnicodeString型をサポートするために変更が必要になる可能性があるコードパターンを表しています。
まとめ
ここまで、Unicodeの世界での正しく動作するために再検証すべきコーディングパターンの種類をまとめてきました。一般的には、コードの大部分は動作するはずです。出力された警告のほとんどは簡単に修正可能です。再検証する必要のあるコードパターンは、大抵、一般的なものではないので、ほとんどの既存のコードは動作すると思います。
Connect with Us