UnicodeとUTF-8の違い、Rustでの扱いも

知らなかったため備忘録です。

UnicodeとUTF-8

Unicode - Wikipediaの受け売り情報です。

Unicodeは文字コードの規格、UTF-8はUnicodeの文字符号化形式のひとつ。

文字と、実際のファイル等での表現の間には、ざっくり2段階の対応があります。 文字と符号位置の対応と、符号位置と符号単位列の対応です。

実際は、エンディアンの問題が絡みもう一段ありますが、今回は取り扱いません。

文字と符合位置

これがいわゆる「文字コード」にあたるもので、現実世界の文字(抽象文字)に符号位置と呼ばれる整数値を割り当てたものです。

Unicodeにおける例をいくつか挙げます。

抽象文字符号位置
A65
12354
🤔129300

符号位置と符号単位列

先ほどの対応関係によって数値となった文字たちを、コンピューターで扱える形(符号単位列)にするのがこの変換です。

多くのコンピューターではデータをバイト単位で管理していますが、たとえば12354をどのようにバイト区切りにするかというのは自明ではありません。 ここで登場するのが、UTF-8などの符号化形式です。

UTF-8では、ASCII文字と互換性を保ったまま、符号位置を1~4バイトに変換します。 詳しくはUTF-8 - Wikipediaを見ていただきたいですが、「あ」の場合は111000111000000110000010となります。

111000111000000110000010 (全体)
    0011  000001  000010 (意味を持つビット)

3バイトで表現される「あ」の場合、下の行に抜き出した16ビットが実際に符号位置を表しています。 桁を詰めて0011000001000010とし、10進法に変換すると12354となり、先ほどの表に出てきた符号位置がちゃんと表現されていたことがわかります。

Rustでの取り扱い

さて、非常にややこしいことに、StringはUTF-8が、charではUnicodeが使われています。

fn main() {
    let as_string = "あ";
    let as_char = 'あ';

    for c in as_string.bytes() {
        print!("{:b} ", c);
    }
    println!();

    println!("{:b}", as_char as u32);
}
11100011 10000001 10000010 
11000001000010

たとえば、上記のコードを実行してみると上のようになります。 これでは見にくいので、桁だけ揃えましょう。

11100011 10000001 10000010 
           110000 01000010

このように、同じ「あ」でもStringcharでは内部表現が違い、前者はUTF-8を用いてエンコーディングされ、後者は符号位置がそのままになっています。

現在テキストエディタを開発していて、キーボードから入力されたUTF-8形式の文字をcharとして得たい場面があり、見事にハマったためこの記事が生まれました。 結局のところ、ビット演算をして符号位置を取り出してからchar::from_u32()する汚いコードを書くはめになりました。 何かもっと良い方法があるように思いますが、とりあえずは仕方ありません。