CodeGym /コース /C# SELF /主なエンコーディングの種類: UTF-8

主なエンコーディングの種類: UTF-8, UTF-16, ASCII

C# SELF
レベル 37 , レッスン 1
使用可能

1. はじめに

コンピュータがどれだけ賢くても、文字 "A" や記号 "⌘" の意味をそのまま理解しているわけじゃないよね。コンピュータが理解するのは 0 と 1 だけで、人間にわかる文字に変換するには「通訳」=エンコーディングが必要なんだ。

エンコーディングの歴史は妥協と進化の歴史でもあるよ。最初は単純だったけど、だんだん複雑になって、やがて比較的ユニバーサルな標準が登場した。年代順に見ていこう。

始まり

まずは源流から。前の講義でテキストエンコーディングの祖先である ASCII を思い出したよね(発音は「アスキー」)。正式名称は American Standard Code for Information Interchange で、名前どおりアメリカ向けに作られたんだ。

ASCII は1960年代に作られて、最初に広く使われた文字コードの標準だよ。128 文字のセットになっている:

  • ラテン文字(大文字・小文字): A-Z, a-z
  • 数字: 0-9
  • 句読点: .,!?"' など
  • 制御文字の一部: 改行、タブなど

これら 128 文字はそれぞれ 1 バイトで表現され、実際には 8 ビット中 7 ビットを使ってた(最上位ビットはチェックや未使用だった)。英語にはすごくコンパクトで効率的だよ。


例:
文字 'A' は ASCII ではバイト 0x41 (2進数 01000001) で表される
文字 '!' は ASCII ではバイト 0x21 (2進数 00100001) で表される

制限:
ASCII の最大の弱点は明白で、英語向けに最適化されていること。ロシア語("Привет")、ドイツ語("Grüße")や中国語を書きたい時には役に立たない。テーブルにその文字は存在しないからね。そこで 128 文字を 256 文字に拡張するいろんな「コードページ」が出てきた。例えばロシア語向けには CP1251(Windows Cyrillic)、KOI8-R など。でも問題は、これらのコードページ間で互換性がないこと。同じバイトが別のコードページでは全く別の文字を意味することがあった。まさにバベルの塔状態だよ。

今日の実用面:
純粋な ASCII フォーマットは今ではあまり一般的じゃない(特別な用途か古いシステムぐらい)。でも遺産は生きていて、多くの現代的なエンコーディングは ASCII と互換性があることが多いんだ。

ちょっと実験しよう。まず ASCII に書き込んで読み出してみて、その後にロシア文字を追加して何が起きるか見てみよう。

JetBrains Rider で新しいコンソールプロジェクトを作って、例えば FileEncodingExplorer と名付けてみるよ。

using System;
using System.IO;
using System.Text;

string file = "ascii.txt";
string asciiText = "Hello, world!";
string cyrillicText = "Привет, мир!";

// ASCII に書き込み
using var writer = new StreamWriter(file, false, Encoding.ASCII);
writer.WriteLine(asciiText);
writer.WriteLine(cyrillicText);

// ASCII から読み込み
using var reader = new StreamReader(file, Encoding.ASCII);
string content = reader.ReadToEnd();
Console.WriteLine("ファイルの内容:");
Console.WriteLine(content);

Console.WriteLine("\nキリル文字は '?' に置き換わるよ。ASCII はキリル文字をサポートしてないからね!");

このコードを実行すると、英語部分は正常に読めるけど、ロシア語はクエスチョンマーク(?)や他の「不明」文字に変わるのが分かるよ。これは Encoding.ASCII がキリル文字をバイトに変換する方法を知らないからで、存在しない文字は安全な代替(通常は ?)に置き換えられるからなんだ。あるいは別のエンコーディングでのバイトが ASCII として解釈されて別の文字になることもある。StreamWriter が書き込み時に見つからない文字を強制的に ? に置き換えるのはその典型例だよ。だから正しいエンコーディングを使うのが重要なんだ。

2. UTF-8: インターネットの王様で柔軟性の塊

ここで今日最も重要で、たぶん最も広く使われているエンコーディングのひとつ — UTF-8 に到達するよ。インターネットの大部分、Linux 系、モダンなアプリの多くがこれで動いてるんだ。

どんなもの?
UTF-8 (Unicode Transformation Format - 8-bit) は Unicode の別のエンコーディングで、特に英語テキストに対して UTF-16 の非効率性を解決するために考えられたんだ。UTF-8可変長エンコーディング だけど、賢いやり方をしているよ:

  • 普通の ASCII 文字(コード 0〜127)は 1 バイト でエンコードされる。しかもそのバイトは完全に ASCII と同じだから、UTF-8ASCII と後方互換性があるんだ。
  • その他の文字は 24 バイトで表現される:
    • キリル文字 — 通常 2 バイト。
    • ヨーロッパのダイアクリティカル付き文字、アラビア文字、ヘブライ文字、ギリシャ文字など — 2 バイト。
    • 中国語/日本語/韓国語の漢字 — 多くは 3 バイト。
    • 稀な文字や一部の絵文字 — 4 バイト。

UTF-8 のバイト表現の例:

  • 文字 'A' (ASCII): 01000001 (1 バイト)
  • 文字 'я' (ロシア語): 11010001 10111111 (2 バイト)
  • 文字 '€' (ユーロ記号): 11100010 10000010 10101100 (3 バイト)
  • 文字 '😂' (絵文字): 11110000 10011111 10011000 10000010 (4 バイト)

なぜ UTF-8 が「王様」なの?

  1. 効率性: ASCII 文字が多いテキスト(英語、プログラムのソース、設定ファイルなど)では非常にコンパクト。
  2. ASCII との互換性: ASCII のみを含む UTF-8 ファイルは ASCII として読めるし、そのまま動くことが多い。
  3. BOM が基本的にない: UTF-16 と違って、UTF-8 は通常 BOM を使わない。もし付くことがあっても(例: EF BB BF)、それは任意の「機能」で、場合によっては問題を起こすことがある(例えば一部のフォーマット解析や Linux のスクリプトで)

欠点:

  • 可変長なため、例えば「N 番目の文字にジャンプする」みたいな操作は全体を走査しないと難しい場合がある。でも C# では問題にならない: string はファイルのエンコーディングに関係なくユニコード文字を扱ってくれるからね。

実用例:

  • ウェブページ (HTML, CSS, JavaScript)、
  • API (JSON, XML)、
  • 設定ファイル、
  • 多くのプログラミング言語のソースコード、
  • Linux/Unix 系 OS。

じゃあ UTF-8 でファイルを書いて読み込む例を書いてみよう。

string file = "utf8.txt";
string text = "Hello, 世界! 😀 €";

// UTF-8 に書き込み(デフォルトは BOM なし)
File.WriteAllText(file, text, Encoding.UTF8);

// UTF-8 から読み込み
string readText = File.ReadAllText(file, Encoding.UTF8);
Console.WriteLine(readText); // 正しく読めるよ!

このコードを実行してファイルサイズを比べると、混在したテキストの場合 utf8.txt は通常 UTF-16 より小さいし、完全に英語だけなら ASCII と同等になることが多いよ。

3. UTF-16: ほぼすべてをカバーする Unicode

コードページのバラバラ問題は、アプリがグローバル化すると本当に辛い問題になった。そこで登場したのが UnicodeUnicode はそれ自体が「エンコーディング」ではなく、あらゆる既知の文字に一意の数値コード(code point)を割り当てた巨大なテーブルだよ。

どんなもの?
UTF-16 (Unicode Transformation Format - 16-bit) は、当初はすべての Unicode 文字を 2 バイト(16 ビット)で表せるように考えられていたエンコーディングだよ。

  • ほとんどの文字(BMP、65535 まで)は 2 バイトで表される。
  • BMP 外の文字はサロゲートペア(surrogate pair)で表現されて 4 バイトになる。つまり UTF-16 も可変長だけど、普通は 1 文字 ≒ 2 バイトと考えられることが多いよ。

バイト順(Endianness)と BOM:

  • Big-Endian (BE): 上位バイトが先。
  • Little-Endian (LE): 下位バイトが先。
  • 読み手がバイト順を知るためにファイルの先頭に BOM(Byte Order Mark)を置くことが多い:
    • UTF-16 LE の場合: FF FE
    • UTF-16 BE の場合: FE FF
    C# と Windows ではデフォルトで UTF-16 LE が使われることが多いよ。

利点:

  • 世界中のほとんどの文字をサポートする。
  • BMP 内の文字の扱いは簡単(固定長で 2 バイト)。

欠点:

  • 英語のテキストには非効率: ASCII の文字でも 2 バイト使う。
  • BOM があると、読み手がそれを期待していない場合に問題になることがある。

実用面:
UTF-16 は Windows の内部や、例えば Java の文字列内部表現で広く使われているよ。Windows のメモ帳がキリル文字を含むテキストを保存するときは UTF-16 LE + BOM で保存されることがよくあるんだ。

string file = "utf16.txt";
string text = "Hello, 世界! 👋";

// UTF-16 に書き込み(デフォルトは Little-Endian、BOM 付き)
File.WriteAllText(file, text, Encoding.Unicode);

// UTF-16 から読み込み
string readText = File.ReadAllText(file, Encoding.Unicode);
Console.WriteLine(readText); // 正しく読めるよ!

Console.WriteLine($"ファイルサイズ: {new FileInfo(file).Length} バイト");

実行すると全ての文字が正しく表示されるはず。英語だけのファイルだと UTF-16 はおおよそ ASCII / UTF-8 の 2 倍のサイズになることが多いよ(ASCII 範囲の文字を扱う場合)。

4. エンコーディング比較まとめ表

知識を整理するために、主要な特徴を表にまとめるよ。

エンコーディング 文字あたりの最小バイト数 文字あたりの最大バイト数 ASCII との互換性(直接) .NET でデフォルトで BOM を使うか 利用例
ASCII 1 1 完全 いいえ 古いシステム、非常に単純なテキストデータ、内部プロトコル
UTF-16 2 4 いいえ はい (Encoding.Unicode) Windows の文字列内部表現、Java の内部表現、Windows のテキストファイル
UTF-8 1 4 完全 いいえ (Encoding.UTF8 in .NET 5+); はい (Encoding.UTF8 in .NET Framework) ウェブ(HTML, JSON)、設定ファイル、ソースコード、Linux/Unix

Encoding.UTF8 に関するちょっとした注意:
歴史的に .NET Framework の Encoding.UTF8 はデフォルトで BOM を付けていたよ。モダンな .NET (Core/5+) ではデフォルトの挙動が変わって、通常は BOM を付けないんだ。もし BOM が必要なら new UTF8Encoding(true) を使ってね。

5. C# でエンコーディングを指定する方法

例で見たように、StreamReaderStreamWriter にどの「辞書」を使うかを教えるには、System.Text.Encoding のオブジェクトを渡せばいいんだよ。

System.Text.Encoding はいくつかの定義済みオプションを提供しているよ:

  • Encoding.ASCII: ASCII 向け。
  • Encoding.Unicode: UTF-16 LE(BOM 付き)。
  • Encoding.UTF8: UTF-8(モダンな .NET ではデフォルトで BOM なし)。

他のエンコーディングは Encoding.GetEncoding で利用できるよ(例: "windows-1251", "koi8-r")。でも今は Unicode 系が主なフォーカスだね。

// UTF-8 に書き込み
using var writer = new StreamWriter("my_file.txt", false, Encoding.UTF8);
writer.WriteLine("何かのテキスト。");

// UTF-16 から読み込み
using var reader = new StreamReader("another_file.txt", Encoding.Unicode);
string content = reader.ReadToEnd();
Console.WriteLine(content);

これがすべてのコツだよ!StreamReaderStreamWriter が選ばれたエンコーディングのルールに従って、文字とバイトの変換を全部やってくれるんだ。

6. エンコーディングの問題: 「文字化け」

先ほど ASCII にロシア語を書いたときに「文字化け」が出たのを見たよね。でも一番厄介なのは、あるエンコーディングでファイルを書いて、別のエンコーディングで読んでしまう場合だよ。ここから本当の地獄(面白いけどね)が始まる。

例えばロシア語で書かれたテキストを UTF-8 で保存してあるのに、クライアント側がそれを CP1251 として読もうとしたら、バイト列の解釈が間違ってしまって、"Привет, мир!" の代わりに「文字化け」(英: mojibake) が出るんだ。

原因は一つ: 書き込み時と読み込み時のエンコーディングが一致していない こと。書く側と読む側で同じエンコーディングを使うのが基本。意図的に再エンコードする場合を除いてね。

string file = "mismatch.txt";
string russianText = "Привет, мир!";

// UTF-8 で書き込み(正しい!)
File.WriteAllText(file, russianText, Encoding.UTF8);

// 間違った読み込み: UTF-8 ファイルを ASCII として読む
string readAsAscii = File.ReadAllText(file, Encoding.ASCII);

Console.WriteLine($"オリジナル: {russianText}");
Console.WriteLine($"ASCII として読み込んだ結果: {readAsAscii}"); // ここで文字化けが起きるよ!

プログラムを実行すると、キリル文字がクエスチョンマークや意味のない記号に変わるのが見えるよ。次のレッスンでは、エンコーディングをより柔軟に扱う方法、こうした問題を避けるコツ、そして読み込もうとしているファイルのエンコーディングをどう判別するかを話す予定だよ。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION