2017年3月27日 星期一

使用XmlTextWriter時用UTF8編碼預設有BOM

前言:
會有這個文章主要是因為,有需要比對序列化後字串是否符合預期的情境,但是卻因為沒有注意到這點讓測試程式碼怎樣都過不了 Orz
這個問題坦白說,若沒有踩到還真不會特別去計較這件事。關於BOM(位元組順序記號),就我的實務經驗來講是很少會使用到的資訊,尤其是最常用到的編碼方式為UTF-8,一般來說我不會馬上想到這個方向。
而UTF-8 with BOM的檔案基本上跟 without BOM用肉眼是看不出差異的,只會在使用游標左右移動時發現需要多按一下才會動,在精神不濟時會以為這些都是幻覺 XD。但若是用Notepad++的話,可以開啟16進位的檢視,就能看到UTF-8 BOM的那該死的位元組「ef bb bf」,在windows平台上有時可以看到存檔時可以選擇是否要包含BOM這個選項。

實驗:
XmlTextWriter 實體要 new 出來時會有幾種作法,其中我們要講的就是把Encoding方式傳入的那種。
public class SerializeHelper
{
        /// 
        /// 序列化
        /// 
        /// 物件
/// 編碼
/// 序列化後文字
        public string Serialize(object o, Encoding encoding)
        {
            XmlSerializer xmls = new XmlSerializer(o.GetType());
            var xns = new XmlSerializerNamespaces();
            xns.Add(string.Empty, string.Empty);

            using (MemoryStream ms = new MemoryStream())
            {
                using (var writer = new XmlTextWriter(ms, encoding))
                {
                    xmls.Serialize(writer, o, xns);
                }

                string xml = Encoding.UTF8.GetString(ms.ToArray());
                return xml;
            }
        }
}

文字是否有加入BOM一般來說很難用肉眼看出來有什麼差異,因此我們使用GetByteCount來識別是否有BOM參入其中,看看是否真的多了那三個該死的位元標記符號。
在驗證的程式碼我們這樣寫:

class Program
    {
        static void Main(string[] args)
        {
            var demonItem = new DemonClass()
            {
                Address = "Taipei",
                Id = 1,
                Name = "Ben",
                Refer = new DemonClass()
            };


            var serialize = new SerializeHelper();
            //// Encoding.UTF8預設會帶BOM
            var stringObj = serialize.Serialize(demonItem, Encoding.UTF8);

            Console.WriteLine("stringObj:" + Encoding.UTF8.GetByteCount(stringObj));

            var stringObj2 = serialize.Serialize(demonItem, new UTF8Encoding(false));

            Console.WriteLine("stringObj2:" + Encoding.UTF8.GetByteCount(stringObj2));
       }
   }

執行過後就會發現,我們在.NET平台最常使用到的呼叫方式就是那種會帶入BOM的那種。
這點我們可以從原始碼中略窺一二
public static Encoding UTF8
{
 [__DynamicallyInvokable]
 get
 {
  if (Encoding.utf8Encoding == null)
  {
   Encoding.utf8Encoding = new UTF8Encoding(true);
  }
  return Encoding.utf8Encoding;
 }
}
而在MSDN中 UTF8Encoding 的建構子資訊中提到這個參數會影響到 GetPreamble 這個方法的行為。
在實測之中這個參數不只會影響GetPreamble這個方法的回傳值,也會影響到輸出的文字是否會含有BOM。

會寫這篇主要是這個問題說大不大,但若踩到的話也可以吃足苦頭,寫下來避免下次遇到時問題找個老半天,才發現是這個編碼問題。

2017.03.30補充:
關於UTF-8編碼的檔案是否要帶BOM的問題,在stackoverflow的這篇討論串還不錯~