Delphi 2009とUnicode:Part I

提供: Support
移動先: 案内検索

この記事は以前EDNサイトに作成されていた記事を転載したものです。

概要

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のプレーンが存在します。

D2009 uni01 1.png

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には存在します。

結合文字

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

違います。

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

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

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

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

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

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

関連記事