.NET Framework対応のアプリケーションが扱う文字列に4バイト長で1文字を表す「サロゲート・ペア」が含まれる場合,文字列操作にどのような影響があるだろうか。文字列処理で最も基本的で頻繁に利用されるstringクラス(stringオブジェクト)に着目した検証記事の後編である。今回は,サロゲート・ペアが含まれていても正常に動作する例などを紹介しよう。

 前編で検証したように,stringオブジェクトに関する,文字数や文字単位のアクセスに関するメソッドや,インデックス値を求めるメソッドは,扱う文字列にサロゲート・ペアが含まれると,文字の見た目(人間が認識している文字の形や数)から予想されるのとは異なる結果が得られることがわかった。

 しかしだからといって,文字列を操作するメソッドがすべて,サロゲート・ペアによって「正しく動作しなくなる」というわけではない。そうではない例を,いくつか示そう。

文字列の比較と加工など

 まずは,文字列の比較や加工に関するメソッドを検証する(表3)。これらのメソッドの中には,理論的に考えて,サロゲート・ペアを使用する影響がないと言えるメソッドがある。

 例えば,単に二つのstringオブジェクトを連結する操作なら,文字数の情報は特別重要ではない。表で「×印」が付いているプロパティは,サロゲート・ペアが存在すると,「正しく動作しなくなる」メソッド(見た目の文字数単位での処理にはならないメソッド)であるが,×が付かないメソッドは,サロゲート・ペアの影響を受けない。

表3●文字列の比較や加工に関するメソッド
メソッド名 備考・説明 サロゲート・ペア対応
Compare 比較,インデックスによる部分比較あり ×
CompareOrdinal 比較,インデックスによる部分比較あり ×
CompareTo ソートなどで利用する大小比較  
Contains 含有のチェック  
StartsWith 特定の文字列で開始するかチェック  
EndsWith 特定の文字列で終了するかチェック  
Equals 等しいか比較  
== 演算子 等しいか比較  
!= 演算子 等しくないか比較  
ReferenceEquals 参照が等しいか比較  
Concat 文字列の連結  
Join 文字列の連結  
Copy コピー  
CopyTo コピー,インデックスによる部分指定あり ×
Insert 挿入,インデックスによる部分指定あり ×
Remove 削除,インデックスによる部分指定あり ×
Replace 置換,インデックスによる部分指定あり ×
Split 分割,インデックスによる部分指定あり ×
Substring 抽出,インデックスによる部分指定あり ×

 ×がつくのは,引数で文字列の一部を指定する際に,インデックスを指定するメソッドである。これらのインデックスは,charオブジェクト単位である。もし,「文字数」という単位でインデックスを指定する必要があるのなら,そのまま引数に指定して,利用してはならない。

 しかし,文字列の操作全般にわたり,インデックスがchar単位であるという一貫性が保たれているので,かえって好都合な場合もある。リスト7のInsertMyData1メソッドとInsertMyData2メソッドでは,簡単な文字列の加工を行うもので,どちらも,特定の文字を検出して,その直前に文字列を追加している。前者のメソッドでは,IndexOfメソッドとSubstringメソッド,後者のメソッドではIndexOfとInsertメソッドを使用している。

リスト7:char単位のインデックスを指定して文字列の加工

// 検証用メソッド char型インデックスを前提にした一貫性ある処理
private void TestProc04()
{
    string str1 = "好物は\u9BADです。";
    string str2 = "好物は\uD867\uDE3Dです。";
    string str1after = InsertMyData1(str1, "など", "です");
    string str2after = InsertMyData1(str2, "など", "です");
    WriteData(str1after);
    WriteData(str2after);

    str1after = InsertMyData2(str1, "等", "です");
    str2after = InsertMyData2(str2, "等", "です");
    WriteData(str1after);
    WriteData(str2after);
}
private string InsertMyData1(string org, string added, string target)
{
    //ここでは,エラーは考えない
    //(target文字列が org文字列の2文字目以降にあることが前提)
    int ndx = org.IndexOf(target);  //(1)
    WriteData("target index :" + ndx);
    string before = org.Substring(0, ndx); // (2) target から前の文字列
    // target から後の文字列
    string after = org.Substring(ndx, org.Length - before.Length);
    return before + added + after;
}
private string InsertMyData2(string org, string added, string target)
{
    //ここでは,エラーは考えない
    //(target文字列が org文字列の2文字目以降にあることが前提)
    int ndx = org.IndexOf(target);
    WriteData("target index:" + ndx);
    return org.Insert(ndx, added); //(3)
}

 この例では,TestProc04メソッドから,前述の二つのメソッドを呼び出して,文字列の「です」の直前に,「など」や「等」を追加している。

 この二つのメソッドでは,一貫してchar単位のインデックスを使っているので,問題なく動作する。(1)では,特定の文字を検索しているが,その位置(変数ndx)は,char単位であって,本当の文字の位置ではない。文字列にサロゲート・ペアが含まれると,実際の可読できる文字の位置と一致しなくなる。しかし,(2)で文字を抽出するときも,その引数は,文字単位ではなくchar単位であるため,問題なく正しい位置の文字を抽出できる。

 (2)では,Substringメソッドの2番目の引数によって,先頭からndx個分のcharオブジェクトを抽出するので,検索位置より前の文字列がうまく抽出できる。同様に(3)でも,char単位の正しい位置に文字列を挿入できる。

 実行した結果は,図7のようになる。「サケ」と「ホッケ」の二つの文字列の「です」の位置は,文字数としては同じだが,InserMydata1メソッドを呼び出す際のインデックスは,4や5と異なります(インデックスは0から始まるので,charオブジェクト数でいうと,それぞれ,5個目と6個目です)。しかし,「です」の前に「など」を正しく追加できています。

図7●リスト7の実行結果。文字を正しく追加できている
図7●リスト7の実行結果。文字を正しく追加できている

 ただし,このようなロジックを実装する際,落とし穴があることに気をつけていただきたい。それは,文字列内において「一つ前の文字」や「一つ後の文字」を参照するとき,インデックス値を1減算したり,1加算したりしてはならないということだ。charオブジェクト単位のインデックスが,サロゲート・ペアの中途半端な位置を示してしまう可能性があるからだ。