Encodingクラスでは、エンコード・デコードできない文字があった場合は代替文字への置き換えが行われる。 GetEncodingメソッドの引数でEncoderFallbackDecoderFallbackを指定すると、エンコード・デコードできない文字があった場合の動作(フォールバック)を変更することができる。

§1 デフォルトの動作

デフォルトでは、Encodingクラスはエンコード・デコードできない文字を代替文字に置き換える。 UTF-8やUTF-16へのエンコード/からのデコードでは(U+FFFD REPLACEMENT CHARACTER)が、ASCIIやShift JISなどへのエンコードでは?(クエスチョンマーク)が代替文字として使用される。

エンコード時の文字化け
using System;
using System.Text;

class Sample {
  static void Main()
  {
    var shiftjis = Encoding.GetEncoding("Shift_JIS");
    var text = "⑳㉑"; // '㉑'はShift JISには存在しないのでエンコードの際に文字化けする

    // バイト配列に変換
    // (ここで文字'㉑'はエンコードされず代替文字'?'に置き換えられる)
    var bytes = shiftjis.GetBytes(text);

    // 再び文字列に戻して表示する
    Console.WriteLine(shiftjis.GetString(bytes));
   }
}
実行結果
⑳?
デコード時の文字化け
using System;
using System.Text;

class Sample {
  static void Main()
  {
    var utf8 = Encoding.GetEncoding("UTF-8");
    var bytes = new byte[] {0x93, 0xFA, 0x96, 0x7B, 0x8C, 0xEA}; // Shift JISでの文字列 "日本語"

    // バイト配列を文字列に変換
    // (ここでデコードできない文字は代替文字'�'(U+FFFD)に置き換えられる)
    Console.WriteLine(utf8.GetString(bytes));
  }
}
実行結果
���{��

§2 代替文字列への置き換え (EncoderReplacementFallback/DecoderReplacementFallback)

EncoderReplacementFallbackクラスDecoderReplacementFallbackクラスを使うと、エンコード・デコード時に使用する代替文字列を指定することができる。

GetEncodingメソッドは引数encoderFallbackdecoderFallbackで任意のEncoderFallbackDecoderFallbackを指定できるようになっている。 この引数にEncoderReplacementFallback・DecoderReplacementFallbackを渡すことでエンコード・デコード時に使用する代替文字列を指定することができるようになる。

エンコード時の代替文字列を指定する例
using System;
using System.Text;

class Sample {
  static void Main()
  {
    // エンコードできない文字を下駄記号'〓'に置き換えるフォールバックを作成
    var encoderFallback = new EncoderReplacementFallback("〓");

    // フォールバックを指定してEncodingを取得
    var shiftjis = Encoding.GetEncoding("Shift_JIS",
                                        encoderFallback,
                                        DecoderFallback.ReplacementFallback /*定義済みの置換フォールバック*/);

    var text = "⑳㉑"; // '㉑'はShift JISには存在しないのでエンコードの際に文字化けする

    // バイト配列に変換
    // (ここで文字'㉑'はエンコードされず下駄記号'〓'に置き換えられる)
    var bytes = shiftjis.GetBytes(text);

    // 再び文字列に戻して表示する
    Console.WriteLine(shiftjis.GetString(bytes));
  }
}
実行結果
⑳〓
デコード時の代替文字列を指定する例
using System;
using System.Text;

class Sample {
  static void Main()
  {
    // デコードできない文字を下駄記号'〓'に置き換えるフォールバックを作成
    var decoderFallback = new DecoderReplacementFallback("〓");

    // フォールバックを指定してEncodingを取得
    var utf8 = Encoding.GetEncoding("UTF-8",
                                    EncoderFallback.ReplacementFallback /*定義済みの置換フォールバック*/,
                                    decoderFallback);

    var bytes = new byte[] {0x93, 0xFA, 0x96, 0x7B, 0x8C, 0xEA}; // Shift JISでの文字列 "日本語"

    // バイト配列を文字列に変換
    // (ここでデコードできない文字は下駄記号'〓'に置き換えられる)
    Console.WriteLine(utf8.GetString(bytes));
  }
}
実行結果
〓〓〓{〓〓

なお、EncoderReplacementFallback・DecoderReplacementFallbackには代替文字列として空文字(String.Empty)を指定することもできるため、エンコード・デコードできない文字を無視して消去することもできる。

§2.1 定義済みのフォールバック

GetEncodingメソッドは引数encoderFallbackdecoderFallbackにnullを指定するとArgumentNullExceptionをスローする。 nullの代わりに、以下のような定義済みのフォールバックを指定することができる。

§2.2 EncoderFallbackプロパティ・DecoderFallbackプロパティ

EncodingクラスのEncoderFallbackプロパティDecoderFallbackプロパティを参照するとインスタンスに設定されているフォールバックを取得することができる。

このプロパティ自体は読み取り専用ではなく設定も可能となっているが、GetEncodingメソッドなどから取得したインスタンスでは読み取りとなっていて、プロパティを設定しようとすると実行時に例外InvalidOperationExceptionがスローされる。

EncoderFallbackプロパティを設定する例
using System;
using System.Text;

class Sample {
  static void Main()
  {
    var shiftjis = Encoding.GetEncoding("Shift_JIS");

    // EncoderFallbackをセットしようとすると実行時にInvalidOperationExceptionがスローされる
    shiftjis.EncoderFallback = new EncoderReplacementFallback("〓");

    // Encodingクラスのプロパティで取得できるEncodingでも同様に例外がスローされる
    //Encoding.UTF8.EncoderFallback = new EncoderReplacementFallback("〓");
  }
}
実行結果
System.InvalidOperationException: インスタンスは読み取り専用です。
   at System.Text.Encoding.set_EncoderFallback(EncoderFallback value)
   at Sample.Main(String[] args)

インスタンスのクローンを作成すると、クローンは読み取り専用ではなくなりプロパティの設定ができるようになる。

using System;
using System.Text;

class Sample {
  static void Main()
  {
    var shiftjis = Encoding.GetEncoding("Shift_JIS");

    // インスタンスのクローンを作成する
    shiftjis = (Encoding)shiftjis.Clone();

    // 作成したクローンのフォールバックを設定する
    shiftjis.EncoderFallback = new EncoderReplacementFallback("〓");


    var utf8 = (Encoding)Encoding.UTF8.Clone();

    utf8.EncoderFallback = new EncoderReplacementFallback("〓");
    utf8.DecoderFallback = new DecoderReplacementFallback("〓");
  }
}

§3 例外のスロー (EncoderExceptionFallback/DecoderExceptionFallback)

EncoderExceptionFallbackクラスDecoderExceptionFallbackクラスを使うと、エンコード・デコードできない文字があった場合に例外をスローさせるようにすることができる。

このフォールバックを指定すると、エンコードできない文字があった場合には例外EncoderFallbackExceptionが、デコードできない場合には例外DecoderFallbackExceptionがそれぞれスローされるようになる。

エンコードできない文字があった場合に例外をスローさせる例
using System;
using System.Text;

class Sample {
  static void Main()
  {
    // エンコードできない文字があったら例外をスローするフォールバックを作成
    var encoderFallback = new EncoderExceptionFallback();

    // フォールバックを指定してEncodingを取得
    var shiftjis = Encoding.GetEncoding("Shift_JIS",
                                        encoderFallback,
                                        DecoderFallback.ReplacementFallback /*定義済みの置換フォールバック*/);

    var text = "⑳㉑"; // '㉑'はShift JISには存在しないのでエンコードの際に文字化けする

    // バイト配列に変換
    // (ここで文字'㉑'はエンコードされず例外がスローされる)
    var bytes = shiftjis.GetBytes(text);

    // 再び文字列に戻して表示する
    Console.WriteLine(shiftjis.GetString(bytes));
  }
}
実行結果
ハンドルされていない例外: System.Text.EncoderFallbackException: インデックス 1 にある Unicode 文字 \u3251 を指定されたコード ページに変換できません。
   場所 System.Text.EncoderExceptionFallbackBuffer.Fallback(Char charUnknown, Int32 index)
   場所 System.Text.EncoderFallbackBuffer.InternalFallback(Char ch, Char*& chars)
   場所 System.Text.DBCSCodePageEncoding.GetByteCount(Char* chars, Int32 count, EncoderNLS encoder)
   場所 System.Text.EncodingNLS.GetByteCount(String s)
   場所 System.Text.Encoding.GetBytes(String s)
   場所 Sample.Main()
デコードできない文字があった場合に例外をスローさせる例
using System;
using System.Text;

class Sample {
  static void Main()
  {
    // デコードできない文字があったら例外をスローするフォールバックを作成
    var decoderFallback = new DecoderExceptionFallback();

    // フォールバックを指定してEncodingを取得
    var utf8 = Encoding.GetEncoding("UTF-8",
                                    EncoderFallback.ReplacementFallback /*定義済みの置換フォールバック*/,
                                    decoderFallback);

    var bytes = new byte[] {0x93, 0xFA, 0x96, 0x7B, 0x8C, 0xEA}; // Shift JISでの文字列 "日本語"

    // バイト配列を文字列に変換
    // (ここでデコードできない文字が見つかった時点で例外がスローされる)
    Console.WriteLine(utf8.GetString(bytes));
  }
}
実行結果
ハンドルされていない例外: System.Text.DecoderFallbackException: インデックス -1 にあるバイト [93] を指定されたコード ページから Unicode へ変換できません。
   場所 System.Text.DecoderExceptionFallbackBuffer.Throw(Byte[] bytesUnknown, Int32 index)
   場所 System.Text.DecoderExceptionFallbackBuffer.Fallback(Byte[] bytesUnknown, Int32 index)
   場所 System.Text.DecoderFallbackBuffer.InternalFallback(Byte[] bytes, Byte* pBytes)
   場所 System.Text.UTF8Encoding.FallbackInvalidByteSequence(Byte* pSrc, Int32 ch, DecoderFallbackBuffer fallback)
   場所 System.Text.UTF8Encoding.GetCharCount(Byte* bytes, Int32 count, DecoderNLS baseDecoder)
   場所 System.Text.UTF8Encoding.GetString(Byte[] bytes, Int32 index, Int32 count)
   場所 Sample.Main()

§4 独自のフォールバックの定義

EncoderFallbackクラスDecoderFallbackクラスを継承して適切に実装することで独自のフォールバックを定義することができる。

EncoderFallback・DecoderFallbackでは、エンコード・デコード時に代替文字列の生成を行うEncoderFallbackBufferDecoderFallbackBufferを取得できるように実装する。 EncoderFallbackBuffer・DecoderFallbackBufferでは、エンコード・デコードできない文字があった場合に代替文字列の生成(もしくはEncoderFallbackExceptionなどのスロー)を行うように実装する。 EncoderFallbackBufferが生成する代替文字列は、対象のエンコーディングで使用できる文字のみで構成されている必要がある。

次の例では、EncoderFallbackクラス・EncoderFallbackBufferクラスを継承し、エンコードできない文字をUnicodeスカラ値表記(U+XXXX)に変換したものを代替文字列として生成するようにしている。

エンコード時のフォールバックを定義する例
using System;
using System.Text;

// エンコードできない文字をUnicodeスカラ値表記(U+XXXX)に置き換えるEncoderFallback
class EncoderScalarValueFallback : EncoderFallback {
  // 置き換えられる代替文字列の最大文字数
  public override int MaxCharCount {
    get { return 6; /* "U+XXXX".Length */ }
  }

  public override EncoderFallbackBuffer CreateFallbackBuffer()
  {
    // EncoderFallbackBufferのインスタンスを作成して返す
    return new EncoderScalarValueFallbackBuffer();
  }
}

// 代替文字列の生成を行うクラス
class EncoderScalarValueFallbackBuffer : EncoderFallbackBuffer {
  private char[] alternative;
  private int offset;

  // 代替文字列の残り文字数
  public override int Remaining {
    get { return alternative.Length - offset; }
  }

  // エンコードできない文字が見つかった場合に呼び出されるメソッド
  public override bool Fallback(char charUnknown, int index)
  {
    // エンコードできない文字をUnicodeスカラ値表記にした文字列を代替文字列とする
    alternative = string.Format("U+{0:X4}", (int)charUnknown).ToCharArray();
    offset = 0;

    return true;
  }

  public override bool Fallback(char charUnknownHigh, char charUnknownLow, int index)
  {
    // サロゲートペアのフォールバックの実装は省略
    throw new NotImplementedException();
  }

  // 代替文字列の次の文字を取得するメソッド
  public override char GetNextChar()
  {
    if (alternative.Length <= offset)
      // MaxCharCountやRemainingプロパティの値は考慮されない(?)ようなので、
      // 代替文字列の末尾に到達したらchar.MinValueを返す必要がある
      return char.MinValue;
    else
      return alternative[offset++];
  }

  public override bool MovePrevious()
  {
    // 実装は省略
    throw new NotImplementedException();
  }

  public override void Reset()
  {
    // 実装は省略
    throw new NotImplementedException();
  }
}

class Sample {
  static void Main()
  {
    // 独自に実装したフォールバックを指定してEncodingを取得
    var shiftjis = Encoding.GetEncoding("Shift_JIS",
                                        new EncoderScalarValueFallback(),
                                        DecoderFallback.ReplacementFallback);

    var text = "⑳㉑";
    var bytes = shiftjis.GetBytes(text);

    // 再び文字列に戻して表示する
    Console.WriteLine(shiftjis.GetString(bytes));
  }
}
実行結果
⑳U+3251