Сериализация объекта как UTF-8 XML в .NET

113

Правильная утилизация объекта удалена для краткости, но я шокирован, если это самый простой способ кодировать объект как 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();
Гарри Шатлер
источник
возможный дубликат более простого способа сериализации класса C # в виде текста XML
Гарри Шатлер,
1
Я запутался ... Разве кодировка по умолчанию не UTF-8?
flq
@flq, да, по умолчанию используется UTF-8, хотя это не имеет большого значения, поскольку он снова читает его обратно в строку, utf8EncodedXmlкак и UTF-16.
Джон Ханна
1
@ Гарри, не могли бы вы уточнить, поскольку мы с Джоном Скитом отвечаем на разные вопросы. Вы хотите, чтобы объект был сериализован как UTF-8, или вам нужна строка XML, которая объявляет себя как UTF-8 и, следовательно, будет иметь правильное объявление при последующем кодировании в UTF-8? (в этом случае самый простой способ - не иметь декларации, поскольку она действительна как для UTF-8, так и для UTF-16).
Джон Ханна
@Jon Читаю, в моем вопросе есть двусмысленность. У меня он выводился в строку в основном для целей отладки. На практике я, вероятно, буду передавать байты либо на диск, либо через HTTP, что делает ваш ответ более актуальным для моей проблемы. Основная проблема, с которой я столкнулся, заключалась в объявлении UTF-8 в XML, но, чтобы быть более точным, я должен избегать посредничества строки, чтобы я действительно отправлял / сохранял байты UTF-8, а не зависящий от платформы (я думаю) кодирование.
Гарри Шатлер,

Ответы:

55

Ваш код не помещает 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. поток, база данных и т. д. В общем, это не так уж и много подробностей.

Джон Ханна
источник
4
Также. Если вы хотите подавить спецификацию, вы можете использовать XmlWriter.Create(memoryStream, new XmlWriterSettings { Encoding = new UTF8Encoding(false) }).
оны
Если кому-то (например, мне) необходимо прочитать XML, созданный так, как показывает Джон, не забудьте переустановить поток памяти на 0, иначе вы получите исключение с сообщением «Отсутствует корневой элемент». Итак, сделайте следующее: memStm.Position = 0; XmlReader xmlReader = XmlReader.Create (memStm)
Судханшу Мишра 02
278

Нет, вы можете использовать a, StringWriterчтобы избавиться от промежуточного звена MemoryStream. Однако, чтобы принудительно преобразовать его в XML, вам нужно использовать, StringWriterкоторый переопределяет Encodingсвойство:

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 является наиболее часто требуемой "пользовательской" кодировкой для a StringWriter:)

Теперь, как говорит Джон Ханна, внутренне это все еще будет 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
Даже когда вы переопределяете параметр Encoding в StringWriter, он по-прежнему отправляет записанные данные в StringBuilder, поэтому он по-прежнему UTF-16. И строка может быть только UTF-16.
Джон Ханна,
4
@Jon: Вы пробовали это? У меня есть, и это работает. Здесь важна заявленная кодировка; очевидно, что внутренне строка по-прежнему UTF-16, но это не имеет никакого значения, пока она не будет преобразована в двоичную форму (которая может использовать любую кодировку, включая UTF-8). Это TextWriter.Encodingсвойство используется сериализатором XML для определения имени кодировки, которое следует указать в самом документе.
Джон Скит
2
@Jon: А какая была заявленная кодировка? По моему опыту, это то, что на самом деле пытаются решить подобные вопросы - создать XML-документ, который объявляет себя в UTF-8. Как вы говорите, лучше не рассматривать текст как имеющий какую-либо кодировку, пока вам это не понадобится ... но поскольку XML-документ объявляет кодировку, это необходимо учитывать.
Джон Скит
2
@Garry, сейчас самое простое, что я могу придумать, - это взять второй пример в моем ответе, но когда вы создаете его, XmlWriterсделайте это с помощью фабричного метода, который принимает XmlWriterSettingsобъект и для OmitXmlDeclarationсвойства установлено значение true.
Джон Ханна
4
+1 Ваше Utf8StringWriterрешение очень красивое и чистое
Адриано Карнейро
17

Очень хороший ответ с использованием наследования, просто не забудьте переопределить инициализатор

public class Utf8StringWriter : StringWriter
{
    public Utf8StringWriter(StringBuilder sb) : base (sb)
    {
    }
    public override Encoding Encoding { get { return Encoding.UTF8; } }
}
Себастьян Кастальди
источник
спасибо, я считаю, что это самый элегантный из вариантов
Prokurors
5

Я нашел это сообщение в блоге, которое очень хорошо объясняет проблему и определяет несколько различных решений:

(мертвая ссылка удалена)

Я пришел к выводу, что лучший способ сделать это - полностью опустить объявление XML в памяти. Это на самом деле является UTF-16 в этот момент так или иначе, но декларация XML не представляется значимой , пока он не был записан в файл с определенной кодировкой; да и то декларация не требуется. По крайней мере, это не мешает десериализации.

Как упоминает @Jon Hanna, это можно сделать с помощью XmlWriter, созданного следующим образом:

XmlWriter writer = XmlWriter.Create (output, new XmlWriterSettings() { OmitXmlDeclaration = true });
Дэйв Андерсен
источник