文字って何かね?

  • 60
    いいね
  • 0
    コメント

元ネタ: 「文字列を文字の列とみなす単純化」ってどういうこと?解説編 - 西尾泰和のはてなダイアリー

Shift JISおじさん(半角文字は1バイト、全角文字は2バイト派)

今どきShift JISもないだろうと思いますが、レガシーな業務システムなんかだと割と普通に残っていますね。
C#でShift JISな文字を扱いたければ、System.Text.Encodingクラスを使っていろいろすればいいです。

var text = "あいう";
var enc = System.Text.Encoding.GetEncoding("Shift_JIS"); // コードページ932でも可。
                                                         // 日本語WindowsならDefaultプロパティでもいい。
var bytes = enc.GetBytes(text);

後で書きますが、C# のchar型やstring型はUnicodeをベースにしていますから、使える文字集合はShift JISよりずっと広いです。たとえば "你好" (ニーハオ) の「你」なんてのはJIS第三水準漢字ですがShift JISでは使えません。そういうのをEncodingクラスで処理すると "?好"のように「?」に化けます。
「?」に化けるのは、エンコーディング失敗時に文字を「?」に置換するフォールバック設定がデフォルトだからです。失敗時には例外をスローしたければこんな感じ。

var text = "你好";
var enc = System.Text.Encoding.GetEncoding("Shift_JIS",
                                           System.Text.EncoderFallback.ExceptionFallback,
                                           System.Text.DecoderFallback.ReplacementFallback);
try
{
    var bytes = enc.GetBytes(text);
}
catch (System.Text.EncoderFallbackException ex)
{
    // 変換に失敗した文字は ex.CharUnknown プロパティで参照できる
}

失敗した時に"?"に変換されるのも嫌だし、例外がスローされるのも嫌だという人は、独自のEncoderFallback実装クラスを作る必要があります。

文字はcharだよ派

C#のstringはそういう発想で作られています(LengthプロパティとかIndexOf()メソッドとかSubstring()メソッドとかそういうの全部)。C#的には自然な発想ですね。さっきの"你好"も普通に扱えます。

char型は2バイト。これはすべての文字を2バイトで表そうとしたUnicode 1.1の名残りですが、ご存じのようにその構想は失敗しました。現在のUnicodeコードポイントは最大21ビットでして2バイト(16ビット)には当然収まりません。収まらない文字は「サロゲートペア」と呼ばれるものを使って、char型2つで表現します。その結果、"𩸽" (ほっけ)は2文字ということになってしまいます。

UIを作ってて、単純にテキストボックスにMaxLengthプロパティを設定すると、Windows FormsでもWPFでもそういうことになります。

文字はUnicodeコードポイントだよ派

言い換えれば、文字は4バイト固定だよ派です。UTF-32がそういう文字エンコーディングですね。4バイトと言っても上で書いたようにUnicodeでは21ビットしか使わないので、符号付32ビット整数すなわちint型に安全に収まります(uint型にする必要はない)。

C#のstringを直接int配列に変換することはできませんが、for文などのループを使えばこんな感じに書けます。

static IEnumerable<int> ToUtf32(string text)
{
    for (var i = 0; i < text.Length; i += (char.IsSurrogate(text, i) ? 2 : 1))
    {
        yield return char.ConvertToUtf32(text, i);
    }
}

実は最近のInternet Explorerがこういう(Unicodeコードポイントを1文字と数える)挙動をします。<input type="text"/>maxlength属性を指定して、サロゲートペアで表される文字(「𩸽」など)や、次で紹介している結合文字列やIVSを入れてみるとわかりますよ。

複数のUnicodeコードポイントで1文字を表すこともあるよ派

言い換えれば、固定長の文字エンコーディングなんて幻想だよ派です(Rubyは文字列についてCSIという方針を採用していたため、結果としてこのグループに入ることになりました)。

実際、Unicodeという規格にはそのような文字が含まれます。

結合文字列

ものかの >> archive >> Unicode正規化 その1 というページが正確でかつ分かりやすいと思います。

"がぎぐげご" ←これは普通の濁音ひらがな(Unicodeでは合成済み文字という扱い)です。Unicodeコードポイントで数えると5文字です。
"がぎぐげご" ←これは結合用濁点(文字コード U+3099)を使用していますので、「か゛き゛く゛け゛こ゛」と同じように、Unicodeコードポイントで数えると10文字です。C#で書く場合は"か\u3099き\u3099く\u3099け\u3099こ\u3099"となります。

Windowsでは(通常の運用では)そういう文字を入力することはほとんどありませんが、Max OS Xではファイルシステムがそのような文字体系を使用していますのでFinderでコピペをするとそういう文字がクリップボードに入ってきます。

異体字セレクタ(IVS)

異体字セレクタとは、それ自身は表示されないけれども、付加された文字の字形を指定する機能を持った特殊なUnicode文字です。未来情報産業株式会社の解説ページにありますが、「葛」(文字コード U+845B) の後ろに文字コード U+E0100 を組み合わせると葛󠄀城市の「葛󠄀」として表示されるし、文字コード U+E0101 を組み合わせると葛飾区の「葛」として表示されるというものです。そして、IVS自体がサロゲートペアで表される領域の文字だったりします。なので、C#でIVSを使って「葛󠄀城市」を表すには "葛\uDB40\uDD00城市"みたいな書き方になります。

結合文字列やIVSを扱うには

結合文字列や異体字セレクタをうっかり切り離さずに扱いたい場合は、System.Globalization.StringInfoクラスやSystem.Globalization.TextElementEnumeratorクラスを使います。

static IEnumerable<string> ToTextElements(string text)
{
    for (var itor = StringInfo.GetTextElementEnumerator(text); itor.MoveNext(); )
    {
        yield return itor.GetTextElement();
    }
}

このメソッドの引数に "か\u3099き\u3099く\u3099け\u3099こ\u3099""葛\uDB40\uDD00城市"を渡すと、きちんと1文字を1文字として取り出せていることがわかります。

この投稿は C# Advent Calendar 201413日目の記事です。