.NET Framework対応のアプリケーションが扱う文字列に,4バイト長で1文字を表す「サロゲート・ペア」が含まれる場合,文字列操作にどのような影響があるだろうか。.NET Frameworkの文字列操作には,ユーザー・インタフェースやファイル操作,プログラム内部での処理など様々な形態が考えられる。ここでは,基本的な振舞いを見極めるために,文字列処理において最も基本的で頻繁に利用されるStringクラス(Stringオブジェクト)に着目し,2回に分けて検証する。

 Stringクラスには,メソッドやプロパティなどの構成メンバーが,50個くらい定義されており,それを一つひとつ詳細に解説するのはきりがない(注1)。ここでは,今後皆さんが,.NET Framework環境でサロゲート・ペアに対応したアプリケーションを構築する際にヒントや留意点を導き出せるように,Stringクラスの代表的な機能の特徴を探る(注2)。

注1:いくつかのメソッドは,同じメソッド名で引数の数や型だけ違うバリエーション(つまり,オーバーロードされたメソッド)が存在し,Stringクラスでは,それらを合わせるとメンバーは120個程度になる。

注2:プログラムの検証は,Windows Vista Business上で,Visual C# Express Editionを使用した。文字入力には,Microsoft Office IME 2007を使用した。

Stringクラスで処理結果が“おかしくなる”理由

 Stringクラス(Stringオブジェクト)は,0文字以上の文字列をカプセル化したオブジェクトであり,その文字列はUnicode文字列である。このオブジェクト内部では,原則として1文字を,2バイト長のchar型データ(正確には,charオブジェクト)として表現しており,文字列はcharオブジェクトの並びとして表現されている。

 注意すべきなのは,この文字列がUTF-16形式である点だ。UTF-16では,文字列を構成する多くの文字は2バイト長で1文字だが,サロゲート・ペアのような,4バイト長の文字も混在する(注3)。Stringクラスでは,サロゲート・ペアはcharオブジェクト二つを使って表現する。これらは,Stringクラスの元からの仕様である。

注3:UTF-16には,サロゲート・ペア以外にも4バイト長の文字が存在する。詳しくは,今回の最後に掲載するコラム「サロゲート・ペア以外にもある4バイト長の文字」を参照していただきたい。

 一方で,Stringオブジェクトの文字列操作では,仕様としてcharオブジェクト単位(2バイト単位)で文字を参照したり,加工したりできる。そのため,4バイト長で1文字を表すサロゲート・ペアが操作対象のStringオブジェクトに含まれている場合,「見た目」から予想されるのとは異なる結果が得られてしまう。

 このことは見方を変えると,サロゲート・ペアの登場によって,Stringオブジェクトが誤動作するのではないことを意味する。つまり,文字列操作において,二つのcharオブジェクトを,本来の一つの文字(人間が1文字として認識する文字)として扱うのは,プログラマの責任であると言える。

 しかし実際は,今までサロゲート・ペアの存在を意識してStringオブジェクトの操作を行っていたプログラマは,それほど多くないはずだ。そこで,今後そのようなサロゲート・ペアに対応するためにも,サロゲート・ペアのような4バイト文字が文字列に混在した際の「影響」を中心に,Stringオブジェクトの動作を検証する。

 なお,今回の検証で使用するC#では,Stringクラスを予約語として小文字表記するので,以降では「string」と表記する。今回の検証に使ったプログラムについては,今回の最後に掲載するコラム「検証用サンプル・プログラムについて」をご参照いただきたい。

文字数のカウント,文字の参照

 文字数や文字単位のアクセスに関するプロパティ,LengthとChars(C#の場合はインデクサ)を見てみよう(表1)。表で「×印」が付いているプロパティは,サロゲート・ペアが存在すると,「正しく動作しなくなる」メソッド(見た目の文字数単位での処理にはならないメソッド)である。LengthとCharsはいずれも,サロゲート・ペアを扱う際には見た目から予想されるのとは異なる結果が得られるので,注意が必要だ。

表1●文字数や文字を参照するプロパティ
プロパティ名 備考・説明 サロゲート・ペア対応
Length 文字数を返す(ただし,char単位の数)。 ×
Chars (インデクサ) 文字列内の特定文字のアクセスに使う。C#では,このプロパティ名を使用せずに,str[0],str[1] のようにインデックスを付け表記する。 ×

 まず,Lengthプロパティを見てみよう。クラスライブラリのリファレンスには,このプロパティは,stringオブジェクトの文字数を表すとある。しかし,解説の部分をよく見ると,charオブジェクトの数を返す,と書かれている。つまり,サロゲート・ペアの1文字は,charオブジェクトを二つ使用するので,2個とカウントされる。

 図1のコードで確認してみよう。実行結果は図2のようになる。

図1●サケ(2バイト文字)とホッケ(4バイト文字)で文字数の違いを確認するコード
図1●サケ(2バイト文字)とホッケ(4バイト文字)で文字数の違いを確認するコード

図2●図1のプログラムの実行結果。str2の文字数のほうがcharオブジェクト一つぶん多い(出力の6行目)
図2●図1のプログラムの実行結果。str2の文字数のほうがcharオブジェクト一つぶん多い(出力の6行目)

 順を追って説明しよう。"好物は~です。" という二つの文字列は,どちらも見た目は7文字である。前者の文字列に含まれる「魚へん」に「圭」の漢字はご存じ「サケ」で,後者の「魚へん」に「花」という字は「ホッケ」と読む。実は,この「ホッケ」はサロゲート・ペアを使用しており,4バイトで1文字なのである。

 図1のプログラムでは,検証結果をリストボックスに表示するWriteDataメソッドを6回呼び出している。最初の2回では,二つの文字列をそのまま表示して,次の2回では16進数表記でダンプしており,最後の2回では,それぞれの文字列のLengthプロパティを参照して,文字数を求めて表示させている。

 図2からわかるように「ホッケ」の16進数表記の部分は,D867とDE3Dの二つで4バイトである。16進数表記や文字数からわかるように,「ホッケ」のほうが,内部的に1文字(厳密にはcharオブジェクト一つぶん)大きくなる。また,図2の出力結果の5行目と6行目にあるように,Lengthプロパティの値はそれぞれ7と8となり,明らかに,人が可読できる文字数と異なる。

 Lengthプロパティをプログラムで用いる場合に,サロゲート・ペアに対して問題があるか判断するためには,そのプロパティをどういう意味として使用しているのか(可読の文字数か,char単位の数か)を見極める必要がある。可読できる文字数に基づいて処理が変化するアルゴリズムがある場合には,Lengthプロパティで文字数を求めると,正しく動作しない点に注意しなければならない。

 なお,この後のソースコードの掲載は,テキスト・ベースで行う。ブラウザでは,サロゲート・ペアがうまく1文字として表示できない場合もあるので,C#の16進数表記を使用して,「ホッケ」の部分は,\uD867\uDE3Dと表記する。対比する意味で,「サケ」の部分は \u9BADと表記する。

 前述の図1のコードは,リスト1のようになる(実行結果は同じ)。

リスト1:Lengthプロパティの参照(図1と同じ内容)

// 検証用メソッド String.Length
private void TestProc01()
{
    string str1 = "好物は\u9BADです。";
    string str2 = "好物は\uD867\uDE3Dです。";
    WriteData("str1: " + str1);
    WriteData("str2: " + str2);
    WriteData("str1: " + MakeHex(str1));
    WriteData("str2: " + MakeHex(str2));
    WriteData("str1: " + str1.Length);
    WriteData("str2: " + str2.Length);
}