这两天用一个常用编辑的软件打开一个最近更新的xml配置文件时突然报编码错误,说不支持utf-16。看到这个错误,我愣了一下,这个软件不支持utf-16的xml我是知道的,但是这个xml配置文件可是用XmlSerializer序列化的一个class,一直都是使用utf-8编码的,怎么突然出utf-16了?用Notepad++打开这个xml,看了一下右下角的编码,显示的还是utf-8,看来文件输出没有错啊,等等,突然注意到第一行:

<?xml version="1.0" encoding="utf-16"?>


咦?文件明明是utf-8,可是xml declaration怎么成utf-16了?赶紧打开以前的xml配置文件对比了一下,发现早期的配置文件的encoding都是utf-8,而从某一天后,都是utf-16了。开始查阅SVN,翻看这段时间涉及到这个序列化的代码变更,发现这段时间唯一可疑的变更就是之前的这个序列化是直接写到文件里的,之前的代码形如:

    XmlSerializer xs = new XmlSerializer(typeof(Site));
    
using (StreamWriter sw = new StreamWriter("output.xml"))
    
{
        xs.Serialize(sw, site);
    }

    Console.WriteLine(File.ReadAllText(
"output.xml"));


但是后来由于数据库也需要序列化后的结果,所以将序列化代码移进这个类的ToString()了,之后的代码形如:

    XmlSerializer xs = new XmlSerializer(typeof(Site));
    StringBuilder sb 
= new StringBuilder();
    
using (XmlWriter writer = XmlWriter.Create(sb))
    
{
        xs.Serialize(writer, site);
    }

    
return sb.ToString();


其中唯一的区别就是一个使用的是TextWriter,而另一个用的是XmlWriter。难道说XmlWriter的Encoding默认是utf-16不成?不是啊,MSDN明明说:

The text encoding to use. The default is Encoding.UTF8.

好吧,我手动加入XmlWriterSettings,结果形如:

    XmlSerializer xs = new XmlSerializer(typeof(Site));
    StringBuilder sb 
= new StringBuilder();
    XmlWriterSettings settings 
= new XmlWriterSettings();
    settings.Encoding 
= Encoding.UTF8;
    settings.Indent 
= true;
    
using (XmlWriter writer = XmlWriter.Create(sb, settings))
    
{
        xs.Serialize(writer, site);
    }

    
return sb.ToString();


手动将XmlWriter设置为utf-8,这回该好了吧?呵呵,如果好了就没这篇Blog了。很不幸,依旧输出encoding="utf-16"。见鬼了,难不成XmlWriter不支持utf-8?用脚趾头也可以想象这不大可能。又翻开MSDN Library,好好看看到底问题在哪里。

终于在XmlWriterSettings.Encoding的文档里面发现下面一段话:

This property only applies to XmlWriter instances that output text content to a stream; otherwise, this setting is ignored.

看来XmlWriter只支持在Stream类的输出中设置Encoding。其实仔细想想也可以理解,只有stream类的输出,Encoding才可以用来进行编码,对于我们现在使用的StringBuilder来说,Xml的输出直接就变成了字符串了,没有任何编码过程,因此Encoding失效了。

可以说当我们使用StringBuilder的时候,StringBuilder的Encoding overwrite了XmlWriter的Encoding,而StringBuilder将会用StringWriter来包装,StringWriter.Encoding是Encoding.Unicode,也就是utf-16。因此,当我们使用StringBuilder作为XmlWriter的输出时,XmlWriter的Encoding就成了utf-16。而当我们把这个内存中的字符串,以utf-8写入文件的时候,虽然此时编码实际上为utf-8,但是并没有人负责把Xml声明的encoding="utf-16"改回"utf-8"了。错误就这样发生了。

问题明确了,解决起来并不复杂,我们用MemoryStream替换StringBuilder,形如:

    XmlSerializer xs = new XmlSerializer(typeof(Site));
    MemoryStream stream 
= new MemoryStream();
        XmlWriterSettings setting = new XmlWriterSettings();
       setting.Encoding = new UTF8Encoding(false);
       setting.Indent = true;
    
using (XmlWriter writer = XmlWriter.Create(stream, settings))
    
{
        xs.Serialize(writer, site);
    }

    
return Encoding.UTF8.GetString(stream.ToArray());


这回输出就正常了。这里需要注意的是Encoding那一行,这是设置Encoding不要输出BOM,否则生成的字符串前会有几个字节表示Byte Order。

因此,提醒诸位,如果使用非Stream类的输出,如StringBuilder/StringWriter,作为XmlWriter输出的话,请注意你的xml的Encoding。
 
关于BOM,请参考:

Byte Order Mark (BOM) FAQ
http://unicode.org/faq/utf_bom.html#BOM


Byte-order mark (wikipedia)
http://en.wikipedia.org/wiki/Byte_Order_Mark