UnicodeとUTF-8の違い、Rustでの扱いも
知らなかったため備忘録です。
UnicodeとUTF-8
Unicode - Wikipediaの受け売り情報です。
Unicodeは文字コードの規格、UTF-8はUnicodeの文字符号化形式のひとつ。
文字と、実際のファイル等での表現の間には、ざっくり2段階の対応があります。 文字と符号位置の対応と、符号位置と符号単位列の対応です。
実際は、エンディアンの問題が絡みもう一段ありますが、今回は取り扱いません。
文字と符合位置
これがいわゆる「文字コード」にあたるもので、現実世界の文字(抽象文字)に符号位置と呼ばれる整数値を割り当てたものです。
Unicodeにおける例をいくつか挙げます。
抽象文字 | 符号位置 |
---|---|
A | 65 |
あ | 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
このように、同じ「あ」でもString
とchar
では内部表現が違い、前者はUTF-8を用いてエンコーディングされ、後者は符号位置がそのままになっています。
現在テキストエディタを開発していて、キーボードから入力されたUTF-8形式の文字をchar
として得たい場面があり、見事にハマったためこの記事が生まれました。
結局のところ、ビット演算をして符号位置を取り出してからchar::from_u32()
する汚いコードを書くはめになりました。
何かもっと良い方法があるように思いますが、とりあえずは仕方ありません。