はじめに 「Unicode正規化」は“見た目は同じ文字を、本当に同じにそろえる”作業
Unicode正規化は、一言でいうと
「見た目は同じなのに、内部的なバイト列が違う文字列を、同じ形にそろえる」処理です。
アクセント付き文字(é など)、結合文字(濁点・半濁点)、Mac/Windows 間の微妙な違い、日本語入力で混ざる特殊な記号などが絡むと、
「画面では同じに見えるのに、== で比較すると違う」といったことが普通に起こります。
C# では string.Normalize メソッドと NormalizationForm 列挙体を使って、
この「そろえる」処理を簡単に書けます。
ここから、
なぜ正規化が必要なのか → 正規化形式のざっくりした違い → C# のコード例 → 実務ユーティリティとしてのまとめ方
という流れで、初心者向けにかみ砕いて説明していきます。
なぜUnicode正規化が必要になるのか
見た目は同じでも、中身が違うパターンがある
Unicode では、同じ見た目の文字が「1文字」としても、「複数の文字の組み合わせ」としても表現できることがあります。
例えば、「é」という文字は
- 「1文字の é」として持つパターン
- 「e」+「結合アクセント(´)」の2文字で持つパターン
の両方が存在します。
画面上はどちらも「é」に見えますが、バイト列は違うので、
そのまま == で比較すると「違う」と判定されます。
日本語でも、濁点・半濁点が結合文字として扱われたり、
全角チルダ・波ダッシュ問題など、似たような罠がいくつもあります。
「比較する前に、まずそろえる」が正しい順番
こうした問題を避けるために、
「文字列を比較する前に、まず同じ正規化形式にそろえる」
というのが Unicode 正規化の基本的な考え方です。
C# では、string.Normalize を呼ぶだけで、
指定した正規化形式に変換された新しい文字列を得ることができます
正規化形式(NFC / NFD / NFKC / NFKD)をざっくり理解する
4種類あるけど、まずは「NFC」を覚えればOK
Unicode 正規化には、主に次の4種類があります。
- NFC(Normalization Form C)
- NFD(Normalization Form D)
- NFKC(Normalization Form KC)
- NFKD(Normalization Form KD)
細かい定義は公式仕様に譲りますが、
実務でまず押さえておきたいのは「NFC」です。
NFC は「できるだけ1文字にまとめる」方向の正規化で、
「é」を「e+結合アクセント」ではなく「1文字の é」にそろえる、といったイメージです。
.NET の string.Normalize()(引数なし)は、デフォルトで NFC に正規化します。
NFKC / NFKD は「互換文字」もつぶしていく、より強い正規化で、
全角英数字を半角に寄せたり、見た目だけ違う互換文字を統一したりする方向です。
日本語テキストの厳密な正規化では NFKC を使うかどうかがよく議論になりますが、
まずは「NFC で統一する」から始めるのが安全です。
C#での基本:string.Normalize と NormalizationForm
一番シンプルな使い方(NFCでそろえる)
C# の string 型には、インスタンスメソッド Normalize が用意されています。
using System;
using System.Text;
string s1 = "e\u0301"; // "e" + 結合アクセント
string s2 = "\u00E9"; // "é" 単体
Console.WriteLine(s1 == s2); // False
string n1 = s1.Normalize(); // デフォルトは FormC (NFC)
string n2 = s2.Normalize();
Console.WriteLine(n1 == n2); // True
C#Normalize()(引数なし)は、NormalizationForm.FormC(NFC)で正規化します。
この例では、見た目は同じ「é」でも、
元の文字列では == が False、
正規化後は True になっていることが分かります。
「比較する前に Normalize する」だけで、
こうした“見た目同じ・中身違う”問題をかなり減らせます。
正規化形式を明示して指定する
引数付きの Normalize(NormalizationForm form) を使うと、
どの正規化形式にそろえるかを明示できます。
string s = "e\u0301"; // "e" + 結合アクセント
string nfc = s.Normalize(NormalizationForm.FormC);
string nfd = s.Normalize(NormalizationForm.FormD);
string nfkc = s.Normalize(NormalizationForm.FormKC);
string nfkd = s.Normalize(NormalizationForm.FormKD);
C#NormalizationForm は System.Text 名前空間にあります。
実務では、まず FormC(NFC)を使うケースが多く、
「互換文字もつぶしてガッツリ統一したい」場合に FormKC(NFKC)を検討する、
という順番で考えると整理しやすいです。
実務ユーティリティとしてのまとめ方
「とりあえずNFCにそろえる」ユーティリティ
まずは、どこからでも呼べる「NFC正規化ユーティリティ」を用意しておくと便利です。
using System;
using System.Text;
public static class UnicodeNormalizeUtil
{
public static string NormalizeNfc(string? text)
{
if (string.IsNullOrEmpty(text))
{
return string.Empty;
}
return text.Normalize(NormalizationForm.FormC);
}
}
C#使い方はとてもシンプルです。
string s1 = "e\u0301"; // "e" + 結合アクセント
string s2 = "\u00E9"; // "é"
string n1 = UnicodeNormalizeUtil.NormalizeNfc(s1);
string n2 = UnicodeNormalizeUtil.NormalizeNfc(s2);
Console.WriteLine(n1 == n2); // True
C#このユーティリティを「比較の前」「保存の前」「ハッシュ化の前」などに挟むことで、
「同じ文字なのに別扱いされる」事故をかなり防げます。
「比較専用」のラッパーにしてしまうパターン
毎回
NormalizeNfc(a) == NormalizeNfc(b)
C#と書くのが面倒なら、
「正規化してから比較する」メソッドを用意してしまうのも手です。
public static bool EqualsNormalizedNfc(string? x, string? y)
{
string nx = NormalizeNfc(x);
string ny = NormalizeNfc(y);
return string.Equals(nx, ny, StringComparison.Ordinal);
}
C#使い方はこうなります。
Console.WriteLine(
UnicodeNormalizeUtil.EqualsNormalizedNfc("e\u0301", "\u00E9")
); // True
C#ここでは、正規化した後は StringComparison.Ordinal(バイトレベルに近い比較)で比較しています。
「正規化してしまえば、あとは素直に Ordinal で比べてよい」という発想です。
日本語テキストとUnicode正規化
ひらがな・カタカナ・濁点・半濁点
日本語では、ひらがな・カタカナ・濁点・半濁点などが絡むと、
結合文字として表現されるケースがあります。
例えば、「が」が
- 1文字の「が」として
- 「か」+「結合濁点」として
表現される可能性があります。
NFC で正規化しておくと、
こうした「分解された形」を「できるだけ1文字にまとめた形」に寄せてくれます。
これにより、
- 入力チェック(ひらがなのみ、など)
- ファイル名やキーの統一
- ハッシュ値の安定化
といった場面で、「同じものは同じバイト列になる」保証が強くなります。
NFKC を使うかどうかの判断
NFKC(互換正規化)は、全角英数字や互換文字もつぶしていくため、
「ユーザー入力を“見た目ベース”で統一したい」場合には強力です。
一方で、「意味的に違うものまで同じにしてしまう」可能性もあるため、
すべての場面で安易に使うのは危険です。
実務では、
- 内部キーやID → まず NFC(FormC)で統一
- ユーザー表示用の検索・曖昧一致 → 必要に応じて NFKC を検討
といった使い分けを意識すると、安全側に倒しやすくなります。
まとめ 「Unicode正規化ユーティリティ」は“文字列比較の土台をまっすぐにする”
Unicode正規化は、
「見た目は同じなのに、内部表現が違う」問題を解消するための、
とても重要な“下ごしらえ”です。
C# では
string.Normalize()で簡単に正規化できること- デフォルトは NFC(FormC)であり、多くのケースでまずこれを使えばよいこと
NormalizationFormを指定することで、NFD / NFKC / NFKD も選べること- 比較・ハッシュ化・保存の前に「Normalizeしてから処理する」ユーティリティを用意しておくと、実務コードが安定すること
- 日本語テキストでは、濁点・半濁点・全角/半角・互換文字などの揺れを意識し、NFC を基本に、必要に応じて NFKC を検討すること
を押さえておくと、「なんとなく文字列を比較している」状態から一歩進んで、
“土台がまっすぐそろった状態で、安全に文字列処理を組み立てる”ことができるようになります。

