Delphi 2009 と Unicode : Part I

投稿者:: Hideaki Tominaga

概要: Delphi 2009 でのUnicode の扱い方と基本的な Unicode の構造について解説します。

Delphi 2009 に於いて、全面的に Unicode がサポートされる事になりました。コーディングスタイルは、以前のプロダクトのものとほぼ変わっておらず、ソースコードだけ見せられてもそれが Unicode 対応のものなのかを瞬時に判断する事は難しいでしょう。その位には Delphi 2009 で Unicode アプリケーションを作るのが簡単になっています。

…ただ、”Unicode アプリケーションでの問題点を探し出すのが難しい”という側面も併せ持っているのは事実です。Unicode アプリケーションを作成する上で、躓きそうなポイント/注意すべきポイントをUnicode の基礎知識を交えながら解説する事にしたいと思います。

第一回目は Unicode の基礎知識です。

    Unicode とは?

Unicode は大規模な文字集合です。Unicode 1.0 は 16bit の文字集合で 65,536文字を扱えました。しかし、それでは文字数が足りずに、現在では 21bitの文字集合 となっています。

Unicode の文字はコードポイントを ”スカラー値” と呼ばれる値で表します。Unicode は21bit の文字集合なので、コードポイントは U+0000 ~ U+10FFFF の範囲で表現されます。

コードポイント U+0000 ~ U+FFFF の領域は Unicode 1.0 と互換性があります。この領域の事を BMP(基本多言語面) と呼びます。Unicode は プレーン() と呼ばれる領域に分割されており、BMP(Plane#0) の他に #1 ~ #16 のプレーンが存在します。

Hide image
Click to see full-sized image

    UCS/UTFとは?

UCS(Universal multi-octet coded Character Set) には、UCS2 / UCS4 が存在します。これらは元々 Unicode とは別の文字集合でしたが、現在では同等のものと考えて差し支えありません。”Unicode 1.0” または “Unicode の BMP” と互換性を持つのが 2-octet(16bit) の文字集合 UCS2 であり、Win9x の Unicode はこれに該当します。UCS4 は Unicode を内包する 4-octet(32bit) の文字集合です。なお、UCS4 は器としては 4-octet(32bit) あるのですが、文字集合としては 31bit であり、コードポイントは Unicode に準拠しているため、21bit の範囲にしか文字は割り当てられていません。

UTF は処理系に合わせて Unicode / UCS を変形し、扱いやすくするための符号化方式です。有名なものに UTF-7 / UTF-8 / UTF-16 / UTF-32 があります。すべての UTF は相互に可逆変換であり、文字が失われる事はありません。

    UTF-32

Delphi では、UTF-32を UCS4String という型で扱えます。

UTF-32 は 、UCS4 / Unicode をリニアに扱うために、文字の要素サイズは DWORD 或いは LongWord の配列になっています。実際、Delphi では以下のように定義されています。

UCS4Char = type LongWord;
UCS4String = array of UCS4Char;

UCS4Stringは、単なるLongWordの配列なので “メモリ確保 / 解放” こそ不要なものの、String やWideStringのように手軽には扱えません。

Unicode / UCS4 をリニアに扱える半面、Unicode では 32bit – 21bit = 11bit の無駄が生じてしまいます。また、UTF-32 はエンディアンの影響を受けるため、UTF-32BE / UTF-32LE と 2 つの種類があります。

Delphi 2009 のコードエディタはこの形式をUCS-4LE / UCS-4BE としてサポートしていますが、UCS4String のまま文字列操作が可能な関数は一切用意されていませんし、TEncoding クラスもこの形式をサポートしていません。コンバート用途だと思った方がいいでしょう。

    UTF-16

Delphi 2009 では、UTF-16を String(UnicodeString) 型で扱えます。従来の Delphi では WideString 型で扱う事ができます。

NT系のWindows ではこのUTF-16が採用されています。

UTF-16 は Unicode / UCS を WORD 配列で表現するための符号化の一種です。”Unicode 1.0” 或いは UCS2 をリニアに扱う事が可能です。ただ、このままでは Unicode / UCS4 のすべての文字を表せません。そこで、UTF-16ではマルチバイト ANSI のような拡張方式が採られる事となりました。

U+10000 ~ U+10FFFF のコードポイントにある文字は 2つの WORD 値で表されます。最初の WORD は 0xD800 ~0xDBFF の範囲にあり、次の WORD は 0xDC00 ~ 0xDFFF の範囲にあります。この領域をサロゲートと呼び、これら 2つの WORD 値で表される文字を サロゲートペア と呼びます。

1WORD で表される文字は UCS2 或いは “Unicode 1.0”、”Unicode の BMP”と互換性があります。

東アジアの多くの文字エンコードと違い、UTF-16 のサロゲート領域が 第1ワード と 第2ワード とで重複していないため、マルチバイト ANSI に存在する多くの問題は起こりません。

UTF-16 はエンディアンの影響を受けるため、UTF-16BE / UTF-16LE と 2つの種類があります。

Delphi 2009 のコードエディタはこの形式をサポートしていません。但し、UCS-2LE / UCS-2BE のサポートはあります。UTF-8は、Delphi 2009 における 標準のString 型であるため、非常に多くの RTL が用意されています。

    UTF-8

Delphi では、 UTF-8をUTF8String 型で扱えます。但し、Delphi 2009 の UTF8String と、それ以前の Delphi とでは、UTF8String の定義が違います。

// Delphi 2009
UTF8String = type AnsiString(65001);
// Delphi 2007
UTF8String = type string;

UTF-8は、Unicode / UCS4 をbyte 配列で表現するための符号化の一種です。マルチバイト ANSIの一種であるとも言えます。Unicode は最大で4バイト文字、UCS4 は最大で 6 バイト文字が存在します。但し、UCS4 はコードポイントを 21bit の範囲で抑える事になったので、5 ~ 6 バイト文字を実際に使う事はありません。また、1 バイト文字は ANSI の7bit の範囲で互換性があります。

UTF-8 は byte 配列なのでエンディアンの影響を受けません。

Delphi 2009 のコードエディタの内部実装はこの UTF-8 です。当然、コードエディタも UTF-8 をサポートしています。UTF8String は UTF-8 でデータの送受信やファイルを保存する必要があるときに限定して利用すべきでしょう。string 型のように 文字単位で処理を行うことは難しいため、文字単位での処理が必要な場合には一旦 String(UnicodeString) へ変換する事をお勧めします。UTF は相互にロスレス変換が可能なので、データロスの心配をする必要はありません。

    BOM

BOMとはバイト・オーダー・マークの略で、UTF-32 / UTF-16 ではその名の通り、バイトオーダーを調べるために付加されるマーカーです。UTF-8 ではエンディアンの影響を受けないので、本来BOMは必要ないのですが、ANSI と区別が付きにくいために BOMが付けられる事があります。

BOM は U+FEFF(“ZERO WIDTH NO-BREAK SPACE”) という見えない文字で、これをそれぞれの UTF で符号化したものがファイルの先頭に付加されます。UNIX のシェルスクリプトのようにファイルの先頭を見て処理を行うものは、時として正しく動作しない事があるので、UTF-8 の BOM には注意が必要です。但し、Windows に於いては BOM 付の UTF-8 がスタンダードなようです。BOM 無しの UTF-8 を BOM付の UTF-8 と区別するために “UTF-8N” と表現する事があります。

    Unicode の文字幅

ANSI のマルチバイト文字は多少の例外を除いて 2 バイト文字と 1 バイト文字の文字幅は、固定長フォントの場合 ”2:1” でした。ところが Unicode では文字を構成する要素数(エレメント数)から文字幅を判断する事ができません。

以下のコードは、よくある 文字列を空白で埋めて右寄せする サンプルです。

// Delphi2009 での例
procedure TForm1.Button1Click(Sender: TObject);
var
  U: string; // Delphi 2009 ではUnicodeStringと同義
begin
  U := 'Aαあ' + #$20BB7;

  Memo1.Lines.Clear;
  Memo1.Font.Name    := 'MS ゴシック';
  Memo1.Font.Size    := 10;

  Memo2.Lines.Clear;
  Memo2.Font.Name    := 'Courier New';
  Memo2.Font.Size    := 10;

  Memo1.Lines.Add(StringOfChar('*', 10));
  Memo1.Lines.Add(StringOfChar(' ', 10 - Length(U)) + U);

  Memo2.Lines.Add(StringOfChar('*', 10));
  Memo2.Lines.Add(StringOfChar(' ', 10 - Length(U)) + U);
end;

さて、結果はいかがでしたか?プロポーショナルフォントでもないのに揃いませんね。実はこのコードには 3 つの問題点があります。

  1. 'A' も 'あ' も Length() では文字構成要素数(エレメント数)が 1 になる
    Unicode(UTF-16) では 'A' も 'あ' の文字構成要素数(エレメント数)は 1 です。
  2. 'α' は東アジアでは全角サイズ、それ以外の地域では半角サイズで描画される。
    Unicode(UTF-16) に於いて 'α' の文字構成要素数(エレメント数) は 1 です。しかし国や地域で使われる、フォントによっては描画されるサイズが異なります。
  3. #$20BB7 はサロゲートペア。
    U+20BB7 の文字構成要素数(エレメント数)は 2 (#$D842 #$DFB7) となりますが、全角サイズで描画されています。

まず、既成概念を捨てましょう。Length() が返すのは、文字構成要素数(エレメント数)であり、ANSI に於いても、Unicode に於いても 文字数ではありません。そして本来、文字構成要素数(エレメント数)と文字幅には因果関係はありません。UTF-8 の 4 バイト文字と 1 バイト文字では、文字幅が ”4:1” になるでしょうか?なりませんよね?

東アジアの文字幅については、ユニコード・コンソーシアムに於いて、その扱い方が決められています。詳しくは、Unicode.org の “EastAsianWidth.txt” を参考にして下さい。具体的な内容については、wikipedia に “東アジアの文字幅” という秀逸なトピックがあります。

ここで、BOM を思い出して下さい。BOM は “ZERO WIDTH NO-BREAK SPACE” …つまり、文字幅 0 の文字になります。他にも U+200B ("ZERO WIDTH SPACE") 等の見えない文字が Unicode には存在します。

    結合文字

ここに” GA_01 ”という文字があります。この文字の UTF-16 での文字構成要素数は幾つでしょうか?そう、サロゲートペアでない文字なので、文字構成要素数は 1 ですよね?

違います。

実はこの文字、” KA_01 (U+304B)”と” DAKUTEN_01 (U+3099)”の2つの文字が組み合わさって構成されています。これが 結合文字列(Combining Character Sequence)” というものです。

以下の、枠内の文字(” GA_01 ”)をメモ帳にコピー&ペーストして、文字の右側にカーソルを置き、バックスペースキーを押すと” KA_01 ”になります...面白いですね。

が

※環境によっては正しく表示されない場合があります。

KA_01 (U+304B)” と ” DAKUTEN_01 (U+3099)” の結合文字列の他に ” GA_02 (U+304C)” という “文字(合成文字)” もあります。結合文字列を単一の文字に変換する事を 合成(Composition)” 、逆に 合成文字(Composite Character) から複数の結合文字へ変換する事を 分解(Decomposition)” と呼びます。

紛らわしいのですが、結合文字の組み合わせは 文字ではなく文字列 であり、結合文字列 と呼ばれます。結合文字列を構成する文字、つまり結合文字列の分解を行って得られる文字を 結合文字 と呼びます。

結合文字の難点は、結合文字列と合成文字が見た目で判断できない と言う事です。文字列の検索や置換の処理を考えてみましょう。結合文字列と合成文字は見た目が全く同じなのですが、文字列としては一致しない事になります。これを回避するには、事前に 正規化(標準化)の処理が必要 となります。正規化についての詳しい情報は、wikipediaの “Unicode 正規化” を参照してください。


この記事は、自作のDelphi Tips「Unicode」を元にCDN用に編集・加筆したものです。オリジナルのTips集は、こちらのページをご覧ください。

次からのサーバー応答:: ETNASC04