C#を使用してアプリケーションを開発する際、文字列の比較や検索が意図した通りに動作しないという問題に直面することがあります。

その原因の多くは、Unicodeにおける「同じ文字なのに内部データ(バイナリ)が異なる」という性質にあります。

例えば、日本語の「が」という文字は、一つの文字コードで表現されることもあれば、「か」と「濁点」の組み合わせで表現されることもあります。

このような表記の揺れを解消し、文字列の比較や保存を正しく行うために欠かせないのが「正規化(Normalization)」です。

C#では、String.Normalizeメソッドを使用することで、簡単に文字列を特定の形式に統一できます。

本記事では、C#における文字列正規化の基礎から、NFC・NFDといった各形式の違い、そして実務での活用シーンまでをプロの視点で詳しく解説します。

Unicode正規化が必要な理由

Unicodeでは、見た目が同じ文字を複数の方法で表現できる「等価性」という概念があります。

これには大きく分けて「正準等価性」と「互換等価性」の2種類が存在します。

例えば、日本語の「ぱ」という文字を考えてみましょう。

  1. 結合済み文字:「ぱ」(U+3071)という単一のコードポイント
  2. 結合文字の組み合わせ:「は」(U+306F)+「半濁点」(U+309A)

ユーザーがキーボードで入力した文字と、外部ファイルやMacのファイルシステムから取得した文字では、内部的なデータ構造が異なる場合があります。

この状態で == 演算子による比較を行うと、見た目が同じでも「等しくない」と判定されてしまいます

これを防ぐために、あらかじめ特定のルールに従って文字データを変換しておく作業が「正規化」です。

C#での正規化:String.Normalizeの使い方

C#で文字列を正規化するには、System.String クラスのインスタンスメソッドである Normalize() を使用します。

基本的な構文

C#
// デフォルト(NFC)で正規化する
string normalizedString = originalString.Normalize();

// 特定の形式を指定して正規化する
string normalizedStringNFD = originalString.Normalize(NormalizationForm.FormD);

引数を指定しない場合は、後述する NFC(Normalization Form C) が適用されます。

また、文字列が既に正規化されているかどうかを確認する IsNormalized() メソッドも用意されています。

4つの正規化形式(NFC・NFD・NFKC・NFKD)

Unicodeには4つの正規化形式が定義されています。

それぞれの特徴を理解することが、適切な実装への第一歩です。

形式名称特徴
NFCNormalization Form C正準等価性に基づき、文字を分解してから再度結合する。一般的。
NFDNormalization Form D正準等価性に基づき、文字を単一の要素に分解する。
NFKCNormalization Form KC互換等価性に基づき、分解してから再度結合する。全角・半角も統一。
NFKDNormalization Form KD互換等価性に基づき、文字を分解する。全角・半角も統一。

NFCとNFD:正準等価性

正準等価性とは、文字の「意味」や「見た目」が完全に同一であることを指します。

NFC(Canonical Composition)

NFCでは文字を可能な限り単一のコードポイントに合成します。

WindowsやWebの世界で最も一般的に使われる正規化形式です。

NFD(Canonical Decomposition)

NFDでは文字を基底文字と結合文字(濁点など)に分解します。

macOSのファイルシステム(HFS+APFSの一部)で採用されていることで知られます。

NFKCとNFKD:互換等価性

互換等価性は、正準等価性よりも広い範囲をカバーします。

意味的に同じであれば、見た目が多少異なっていても同じ文字とみなします。

NFKC / NFKD

Unicode正規化の形式の一つで、表記ゆれを統一する処理です。

例えば、全角の「A」を半角の「A」に、丸囲み数字の「①」「1」に、半角カナを全角カナに変換するといった正規化を行います。

検索システムのインデックス作成など、表記ゆれを強力に排除したい場合に使用されます。

実践的なコード例

実際にC#でどのように挙動が変わるのか、コードで確認してみましょう。

C#
using System;
using System.Text;

public class NormalizeExample
{
    public static void Main()
    {
        // 「が」:NFC形式(1つのコードポイント)
        string nfcString = "\u304C"; 
        
        // 「か」+「濁点」:NFD形式(2つのコードポイント)
        string nfdString = "\u304B\u3099"; 

        Console.WriteLine($"NFC形式の長さ: {nfcString.Length}"); // 1
        Console.WriteLine($"NFD形式の長さ: {nfdString.Length}"); // 2
        
        // 単純な比較では false になる
        Console.WriteLine($"比較結果 (直接): {nfcString == nfdString}"); 

        // 両方をNFCに正規化して比較
        string normalized1 = nfcString.Normalize(NormalizationForm.FormC);
        string normalized2 = nfdString.Normalize(NormalizationForm.FormC);

        Console.WriteLine($"比較結果 (正規化後): {normalized1 == normalized2}");
        Console.WriteLine($"正規化後の長さ: {normalized1.Length}");

        // NFKCによる全角・半角の統一
        string fullWidth = "ABC 123 カナ";
        string nfkcResult = fullWidth.Normalize(NormalizationForm.FormKC);
        Console.WriteLine($"NFKC変換結果: {nfkcResult}");
    }
}
実行結果
NFC形式の長さ: 1
NFD形式の長さ: 2
比較結果 (直接): False
比較結果 (正規化後): True
正規化後の長さ: 1
NFKC変換結果: ABC 123 カナ

この結果からわかる通り、Normalize を通すことで、内部データの不一致を解消し、論理的に正しい比較が可能になります。

実務における活用シーンと注意点

文字列の正規化は、単なるテキスト処理以上の重要性を持ちます。

特に以下のケースでは、正規化を意識した実装が強く推奨されます。

1. ファイル名の取り扱い

macOSで作成されたファイル名は、多くの場合NFD形式で保存されています。

これをWindows環境で読み取ったり、クラウドストレージにアップロードしたりすると、「ファイルが存在するのに読み込めない」あるいは「文字化けが発生する」といったトラブルの原因になります。

外部からの入力を受け取る際は、まずNFCに正規化するのが安全なプラクティスです。

2. データベースの検索とインデックス

データベースに「が」という文字を保存する際、NFCとNFDが混在していると、検索クエリで期待した結果が得られなくなります。

  • データの保存時:Normalize(NormalizationForm.FormC) を適用
  • 検索キーワード:入力された瞬間に同じ形式で正規化

このように、システムの入り口で正規化を徹底することがデータの整合性を保つ鍵となります。

3. NFKC/NFKDによるデータ損失の懸念

互換正規化(NFKC/NFKD)は非常に強力ですが、注意が必要です。

例えば、上付き文字(²)を「2」に変換したり、分数を数値に変換したりすることがあります。

意図せず意味が変わってしまう可能性があるため、契約書や学術論文といった「原文の維持」が重要なデータに対しては、NFCを選択するのが無難です。

4. パフォーマンスへの影響

正規化処理は、内部で文字列の再構築を行うため、非常に長い文字列や大量のデータをループ内で処理する場合には負荷がかかります。

正規化が必要なのは、外部システムからデータが入ってくる境界線(バリデーションやインポート処理など)に限定し、システム内部では既に正規化されていることを前提に設計するのが効率的です。

Unicode正規化とStringComparisonの使い分け

C#には、正規化を行わずに比較を工夫する StringComparison オプションもあります。

C#
bool result = string.Equals(str1, str2, StringComparison.OrdinalIgnoreCase);

しかし、StringComparison はあくまで「比較のルール」を決めるものであり、「結合文字と結合済み文字」の差異を完全に埋めるものではありません(Ordinal比較ではNFCとNFDを同一視しません)。

したがって、多言語対応やクロスプラットフォーム環境を想定する場合は、比較前に Normalize を実行する方が確実です。

まとめ

C#の String.Normalize メソッドは、Unicode特有の複雑な文字構造を整理し、アプリケーションの堅牢性を高めるための重要なツールです。

  • NFC:一般的なシステムで標準的に利用する(結合済み形式)。
  • NFD:macOSのファイルシステムなどで見られる分解形式。
  • NFKC:全角・半角や記号の揺れを強力に補正したい場合に利用。

データの比較、検索、保存といった各フェーズで、どの正規化形式が最適かを判断することは、バグの少ない洗練されたコードを書くために不可欠なスキルです。

まずは、「外部からの入力にはまずNFC正規化」というルールを徹底することから始めてみてはいかがでしょうか。

Unicodeの深い理解に基づいた正規化処理は、グローバルに通用するC#アプリケーション開発の強力な武器となるはずです。