UTF-16とは?サロゲート・BOM・UTF-8比較から実務での注意点までを徹底解説

UTF-16 とは

UTF-16 は Unicode の符号化方式(エンコーディング)の一つで、16ビット単位(2バイト単位)のコードユニットを用いて Unicode の文字(コードポイント)を表現します。Unicode は世界中の文字を一元的に扱うための標準で、UTF-16 はその表現方法の一つとして広く使われてきました(特に OS やプログラミング言語の内部表現として)。

基本概念:コードポイント、コードユニット、サロゲート

まず用語の整理です。

  • コードポイント:Unicode が定義する個々の文字番号(例:U+0041 は 'A'、U+1F600 は 😀)。
  • コードユニット:エンコーディングが扱う最小単位。UTF-16 の場合は 16 ビット(0x0000〜0xFFFF)。
  • サロゲート(代理ペア):UTF-16 は 16 ビット単位で表現するため、基本多言語面(BMP, Basic Multilingual Plane、U+0000〜U+FFFF)外の文字(U+10000〜U+10FFFF)を表すために「高サロゲート」と「低サロゲート」の組を使います。

サロゲートの仕組み(具体例と計算方法)

Unicode の U+10000 以上のコードポイントは 20 ビットで表現され得ますが、UTF-16 のコードユニットは 16 ビットしかないため、2 つの 16 ビット値(サロゲートペア)で表現します。範囲は次の通りです:

  • 高サロゲート範囲:0xD800〜0xDBFF
  • 低サロゲート範囲:0xDC00〜0xDFFF

エンコードの手順(例:U+1D11E、楽譜記号「G-clef」):

  • コードポイントから 0x10000 を引く:U' = U - 0x10000 → 0x0D11E
  • 高サロゲート = 0xD800 + (U' >> 10) → 0xD800 + 0x34 = 0xD834
  • 低サロゲート = 0xDC00 + (U' & 0x3FF) → 0xDC00 + 0x11E = 0xDC1E
  • よって U+1D11E は UTF-16 上で 0xD834 0xDC1E のペアで表されます。

バイト順(エンディアン)と BOM

UTF-16 のコードユニットは 16 ビットであるため、ファイルや通信でバイト列にする際に「どちらのバイト順で並べるか(エンディアン)」が問題になります。

  • UTF-16BE:ビッグエンディアン(上位バイト→下位バイト)
  • UTF-16LE:リトルエンディアン(下位バイト→上位バイト)
  • BOM(Byte Order Mark):U+FEFF を先頭に置くことでエンディアンを示す慣習があります。エンコードされた BOM のバイト列は BE なら 0xFE 0xFF、LE なら 0xFF 0xFE です。BOM はエンディアン検出に便利ですが、必須ではありません(特にエンディアンが既に固定されているプロトコルでは不要)。

無効なシーケンスとデコーディングエラー

UTF-16 では、単独の高サロゲート(0xD800〜0xDBFF)や単独の低サロゲート(0xDC00〜0xDFFF)は有効な文字を表しません。これらが単独で現れると「不正(unpaired surrogate)」であり、正しくデコードできないため多くの実装は置換文字(U+FFFD)に置き換えます。

UTF-16 と UTF-8 の使い分け・利点と欠点

どのエンコーディングを使うかは用途によります。代表的な違いは次のとおりです:

  • UTF-8:バイト指向、ASCII と互換(ASCII は 1 バイト)。Web やファイル保存、プロトコルでの標準的選択。小さなラテン文字列では効率が良い。
  • UTF-16:16 ビット単位。BMP 領域(多くの漢字・かな等)を扱う場合、UTF-8 より短くなることがある(例:多くの日本語・中国語の場合は UTF-16 が有利)。一方で絵文字や BMP 外の文字を多用する場合はサロゲートペアが必要になり、扱いが複雑になります。

実際の選択は、プラットフォームの慣習や互換性(例:Web は UTF-8 が事実上の標準)、性能、メモリ効率、既存 API の要件に依存します。Windows API は歴史的に UTF-16(UTF-16LE)を使うことが多く、Java や JavaScript の文字列概念も内部的に UTF-16 コードユニットに基づいています(実装の詳細は各環境で差があります)。

プログラミング上の注意点(文字数・長さの違い)

UTF-16 は「コードユニット」ベースなので、文字列長(length)が Unicode の「文字」の数(グラフムクラスタやユーザが見た文字)と一致しないことがあります。たとえば:

  • サロゲートペアで表される絵文字はコードユニット数が 2、しかし一つの見た目の文字(グラフム)である。
  • 結合文字(合字やダイアクリティカルマーク)は複数のコードポイントで形成されるため、表示上の 1 字とプログラム上の長さが異なる。

そのため、文字列操作(文字列の分割、長さ判定、正規化など)を行う際は「コードユニット」ではなく「コードポイント」や「グラフムクラスタ(Grapheme Cluster)」単位で扱うライブラリを使うのが安全です。

正規化(Normalization)は別問題

UTF-16 はあくまで符号化方式で、ある文字が複数のコードポイント列(合字と分解済みの組合せなど)で表現されうることは変わりません。例えば「é」は単一のコードポイント U+00E9 でも、'e'(U+0065)とアキュートアクセント(U+0301)の組合せでも表せます。これらを一致させるには Unicode 正規化(NFC, NFD, NFKC, NFKD)を行う必要があります。

セキュリティと互換性の問題

UTF-16 特有の問題点としては次が挙げられます:

  • 不正なサロゲート(未対比サロゲート)の存在はパーサや正規表現の挙動を狂わせ、セキュリティ問題(バッファ処理の誤りや XSS に類似する問題)を引き起こすことがある。
  • BOM の有無やエンディアンの勘違いにより mojibake(文字化け)が発生することがある。
  • エンコード間変換での誤り(例:CESU-8 のような非標準形式の混入)に注意が必要です。CESU-8 はサロゲートを個別の UTF-8 シーケンスとしてエンコードする非標準的な方式で、互換性の問題を招きます。

実務上の取り扱い・ベストプラクティス

  • データ交換(特に Web)では UTF-8 を第一選択とする(互換性と普及率の高さ)。
  • プラットフォームの標準(Windows のネイティブ API など)では UTF-16 を利用する場合が多いので、入出力時の変換を正しく行う。
  • 必ず信頼できるライブラリでエンコーディング変換を行い、不正なシーケンスは安全に処理(置換やエラー報告)する。
  • 文字列の長さや切り出しは「コードユニット」ではなく「コードポイント」や「グラフムクラスタ」ベースで操作するライブラリを使う。
  • BOM の扱いを明文化する(通信プロトコルやファイルフォーマットの仕様に従う)。

まとめ

UTF-16 は 16 ビット単位のコードユニットを使い、BMP 内の文字は 1 ユニット、BMP 外(U+10000〜U+10FFFF)はサロゲートペアで表現する符号化方式です。歴史的に多くのシステムや言語で採用されてきましたが、データ交換の場面では UTF-8 の方が一般に優勢です。UTF-16 を扱う際はサロゲート、エンディアン(BOM)、不正シーケンス、正規化といった点に注意し、適切なライブラリを使うことが重要です。

参考文献