在.NET中将一个对象序列化为UTF-8 XML

138

出于简洁起见,适当的对象处理已被移除,但如果这是将对象编码为UTF-8存储在内存中最简单的方法,我感到震惊。难道没有更简单的方法吗?

var serializer = new XmlSerializer(typeof(SomeSerializableObject));

var memoryStream = new MemoryStream();
var streamWriter = new StreamWriter(memoryStream, System.Text.Encoding.UTF8);

serializer.Serialize(streamWriter, entry);

memoryStream.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(memoryStream, System.Text.Encoding.UTF8);
var utf8EncodedXml = streamReader.ReadToEnd();

1
我有点困惑...默认编码不是UTF-8吗? - flq
@flq,是的,默认值为UTF-8,尽管这并不重要,因为它又被读回到一个字符串中了,所以utf8EncodedXml是UTF-16。 - Jon Hanna
1
@Garry,你能否澄清一下,因为Jon Skeet和我回答了不同的问题。你想要将对象序列化为UTF-8,还是想要一个XML字符串声明自己为UTF-8,并且在以后编码为UTF-8时会有正确的声明? (如果是这种情况,最简单的方法就是没有声明,因为这对于UTF-8和UTF-16都是有效的)。 - Jon Hanna
@Jon 重新阅读我的问题,发现有歧义。我将其输出到字符串中主要是为了调试目的。在实践中,我可能会流式传输字节,无论是到磁盘还是通过HTTP,这使得您的答案更直接地与我的问题相关。我遇到的主要问题是在XML中声明UTF-8,但更准确地说,我应该避免使用字符串中介,以便我实际发送/持久化UTF-8字节,而不是平台相关(我认为)的编码。 - Garry Shutler
@Garry:除非你在任何地方指定了 Encoding.Default,否则你不太可能发送平台相关的编码。如果你能提供更多关于你正在做什么的细节,那会很有帮助——但如果你可以直接流式传输到字节,那肯定会避免在字符串中使用“奇怪”的编码声明所带来的麻烦。 - Jon Skeet
显示剩余2条评论
4个回答

338

不需要使用中间的MemoryStream,你可以使用StringWriter。但是,如果你想将它强制转换为XML格式,你需要使用重写了Encoding属性的StringWriter

public class Utf8StringWriter : StringWriter
{
    public override Encoding Encoding => Encoding.UTF8;
}

如果您还没有使用C# 6:

public class Utf8StringWriter : StringWriter
{
    public override Encoding Encoding { get { return Encoding.UTF8; } }
}

然后:

var serializer = new XmlSerializer(typeof(SomeSerializableObject));
string utf8;
using (StringWriter writer = new Utf8StringWriter())
{
    serializer.Serialize(writer, entry);
    utf8 = writer.ToString();
}

显然,您可以将Utf8StringWriter变成一个更通用的类,它在构造函数中接受任何编码 - 但根据我的经验,UTF-8是StringWriter最常需要的“自定义”编码 :)
现在,正如Jon Hanna所说,这仍然是内部的UTF-16,但是您可能会在某个时候将其传递给其他东西,以将其转换为二进制数据......在那个点上,您可以使用上面的字符串,将其转换为UTF-8字节,一切都会很好 - 因为XML声明将指定“utf-8”作为编码。
编辑:一个简短而完整的示例,以展示这个工作原理:
using System;
using System.Text;
using System.IO;
using System.Xml.Serialization;

public class Test
{    
    public int X { get; set; }

    static void Main()
    {
        Test t = new Test();
        var serializer = new XmlSerializer(typeof(Test));
        string utf8;
        using (StringWriter writer = new Utf8StringWriter())
        {
            serializer.Serialize(writer, t);
            utf8 = writer.ToString();
        }
        Console.WriteLine(utf8);
    }


    public class Utf8StringWriter : StringWriter
    {
        public override Encoding Encoding => Encoding.UTF8;
    }
}

结果:

<?xml version="1.0" encoding="utf-8"?>
<Test xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <X>0</X>
</Test>

请注意声明的编码为"utf-8",这正是我们想要的。

2
即使您在 StringWriter 上覆盖 Encoding 参数,它仍会将写入的数据发送到 StringBuilder,因此仍然是 UTF-16。而且字符串只能是 UTF-16。 - Jon Hanna
5
@Jon:你试过了吗?我试过了,它有效。这里重要的是“声明”的编码;显然在内部,字符串仍然是UTF-16,但除非将其转换为二进制(可以使用任何编码,包括UTF-8),否则这不会产生任何影响。TextWriter.Encoding属性由XML序列化程序用于确定要在文档本身中指定的编码名称。 - Jon Skeet
3
@Jon:那么声明的编码是什么?根据我的经验,这样的问题实际上是想创建一个自我声明为UTF-8编码的XML文档。就像你所说的,最好不要认为文本处于任何编码状态,直到你需要使用它...但是由于XML文档声明了编码,因此你需要考虑这一点。 - Jon Skeet
4
您的 Utf8StringWriter 解决方案非常好且简洁。 - Adriano Carneiro
2
@IanGrainger:确实,那是C# 6代码(它在11月份更新为使用C# 6,不是我...) - Jon Skeet
显示剩余13条评论

59

你的代码没有将UTF-8存入内存,当你将其读回字符串时,它不再是UTF-8,而是UTF-16(虽然最好在任何编码之上将字符串视为更高级别,除非被迫这样做)。

要获取实际的UTF-8八位序列,可以使用:

var serializer = new XmlSerializer(typeof(SomeSerializableObject));

var memoryStream = new MemoryStream();
var streamWriter = new StreamWriter(memoryStream, System.Text.Encoding.UTF8);

serializer.Serialize(streamWriter, entry);

byte[] utf8EncodedXml = memoryStream.ToArray();

我忽略了你所忽略的相同释放。我稍微更喜欢以下方式(保留正常的释放):

var serializer = new XmlSerializer(typeof(SomeSerializableObject));
using(var memStm = new MemoryStream())
using(var  xw = XmlWriter.Create(memStm))
{
  serializer.Serialize(xw, entry);
  var utf8 = memStm.ToArray();
}

这个过程的复杂度基本相同,但是在每个阶段都有一个合理的选择来做其他事情,其中最紧迫的选择是将其序列化到除内存之外的其他位置,例如文件、TCP/IP流、数据库等。总的来说,它并不是那么冗长。


6
同时,如果您想去掉 BOM,可以使用 XmlWriter.Create(memoryStream, new XmlWriterSettings { Encoding = new UTF8Encoding(false) }) - ony
如果有人(比如我)需要阅读像Jon展示的那样创建的XML,请记得将内存流重新定位到0,否则您会收到一个异常,说“根元素丢失”。因此请这样做:memStm.Position = 0; XmlReader xmlReader = XmlReader.Create(memStm) - Sudhanshu Mishra

17

非常好的答案使用了继承,只要记得覆盖初始化函数即可

public class Utf8StringWriter : StringWriter
{
    public Utf8StringWriter(StringBuilder sb) : base (sb)
    {
    }
    public override Encoding Encoding { get { return Encoding.UTF8; } }
}

5
我找到了一篇博客文章,很好地解释了这个问题,并定义了几种不同的解决方案:
(死链接已删除)
我认为,在内存中完全省略XML声明是最好的方法。在那时,它实际上已经是UTF-16了,但是XML声明似乎直到使用特定编码的文件写入后才有意义;即使在那种情况下,声明也不是必需的。至少看起来不会破坏反序列化。
正如@Jon Hanna所提到的,可以使用以下方式创建XmlWriter来完成此操作:
XmlWriter writer = XmlWriter.Create (output, new XmlWriterSettings() { OmitXmlDeclaration = true });

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接