Serialisasi objek sebagai UTF-8 XML di .NET

112

Pembuangan objek yang tepat dihapus agar singkat tetapi saya terkejut jika ini adalah cara paling sederhana untuk menyandikan objek sebagai UTF-8 dalam memori. Pasti ada cara yang lebih mudah bukan?

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();
Garry Shutler
sumber
1
Saya bingung ... bukankah pengkodean default UTF-8?
flq
@flq, ya defaultnya adalah UTF-8, meskipun itu tidak terlalu penting karena dia membacanya kembali menjadi string lagi begitu utf8EncodedXmljuga UTF-16.
Jon Hanna
1
@ Garry, dapatkah Anda menjelaskan, karena saya dan Jon Skeet menjawab pertanyaan yang berbeda. Apakah Anda ingin objek diserialkan sebagai UTF-8, atau Anda menginginkan string XML yang mendeklarasikan dirinya sebagai UTF-8, dan karenanya akan memiliki deklarasi yang benar ketika nanti dikodekan dalam UTF-8? (dalam hal ini, cara termudah adalah dengan tidak memiliki deklarasi, karena deklarasi tersebut berlaku untuk UTF-8 dan UTF-16).
Jon Hanna
@Jon Membaca kembali, ada ambiguitas dalam pertanyaan saya. Saya telah mengeluarkannya ke string sebagian besar untuk tujuan debugging. Dalam praktiknya, saya mungkin akan mengalirkan byte, baik ke disk atau melalui HTTP yang membuat jawaban Anda lebih relevan secara langsung dengan masalah saya. Masalah utama yang saya miliki adalah deklarasi UTF-8 dalam XML, tetapi agar lebih akurat saya harus menghindari perantara string sehingga saya sebenarnya mengirim / mempertahankan byte UTF-8 daripada bergantung pada platform (menurut saya) pengkodean.
Garry Shutler

Jawaban:

55

Kode Anda tidak memasukkan UTF-8 ke dalam memori saat Anda membacanya kembali menjadi string lagi, jadi tidak lagi dalam UTF-8, tetapi kembali dalam UTF-16 (meskipun idealnya yang terbaik untuk mempertimbangkan string pada tingkat yang lebih tinggi daripada pengkodean apa pun, kecuali jika dipaksa untuk melakukannya).

Untuk mendapatkan oktet UTF-8 yang sebenarnya, Anda dapat menggunakan:

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();

Saya telah meninggalkan pembuangan yang sama dengan yang Anda tinggalkan. Saya sedikit menyukai yang berikut (dengan pembuangan normal tersisa):

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();
}

Yang merupakan jumlah kompleksitas yang sama, tetapi menunjukkan bahwa pada setiap tahap ada pilihan yang masuk akal untuk melakukan sesuatu yang lain, yang paling mendesak adalah melakukan serialisasi ke tempat lain selain ke memori, seperti ke file, TCP / IP streaming, database, dll. Secara keseluruhan, ini tidak terlalu bertele-tele.

Jon Hanna
sumber
4
Juga. Jika Anda ingin menekan BOM, Anda dapat menggunakan XmlWriter.Create(memoryStream, new XmlWriterSettings { Encoding = new UTF8Encoding(false) }).
ony
Jika seseorang (seperti saya) perlu membaca XML yang dibuat seperti acara Jon, ingatlah untuk memposisikan ulang aliran memori ke 0, jika tidak, Anda akan mendapatkan pengecualian yang mengatakan "Elemen akar hilang". Jadi lakukan ini: memStm.Position = 0; XmlReader xmlReader = XmlReader.Create (memStm)
Sudhanshu Mishra
276

Tidak, Anda dapat menggunakan a StringWriteruntuk menghilangkan perantara MemoryStream. Namun, untuk memaksanya ke dalam XML, Anda perlu menggunakan a StringWriteryang menggantikan Encodingproperti:

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

Atau jika Anda belum menggunakan C # 6:

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

Kemudian:

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

Jelas Anda dapat membuat Utf8StringWriterkelas yang lebih umum yang menerima pengkodean apa pun dalam konstruktornya - tetapi menurut pengalaman saya UTF-8 sejauh ini merupakan pengkodean "khusus" yang paling umum diperlukan untuk StringWriter:)

Sekarang sebagai Jon Hanna mengatakan, ini masih akan UTF-16 internal, tapi mungkin Anda akan menyebarkannya ke sesuatu yang lain di beberapa titik, untuk mengubahnya menjadi data biner ... pada yang titik Anda dapat menggunakan string di atas, mengonversinya menjadi byte UTF-8, dan semuanya akan baik-baik saja - karena deklarasi XML akan menentukan "utf-8" sebagai pengkodeannya.

EDIT: Contoh singkat tapi lengkap untuk menunjukkan ini berfungsi:

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;
    }
}

Hasil:

<?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>

Perhatikan pengkodean yang dideklarasikan dari "utf-8" yang kami inginkan, saya yakin.

Jon Skeet
sumber
2
Bahkan ketika Anda mengganti parameter Encoding pada StringWriter, itu masih mengirimkan data tertulis ke StringBuilder, jadi masih UTF-16. Dan string itu hanya bisa UTF-16.
Jon Hanna
3
@ Jon: Sudahkah Anda mencobanya? Saya punya, dan itu berhasil. Ini adalah pengkodean yang dideklarasikan yang penting di sini; jelas secara internal stringnya masih UTF-16, tetapi itu tidak ada bedanya sampai diubah ke biner (yang dapat menggunakan pengkodean apa pun, termasuk UTF-8). The TextWriter.Encodingproperti digunakan oleh serializer XML untuk menentukan nama encoding untuk menentukan dalam dokumen itu sendiri.
Jon Skeet
2
@ Jon: Dan apa yang dinyatakan pengkodean? Menurut pengalaman saya, pertanyaan seperti ini sebenarnya coba lakukan - buat dokumen XML yang menyatakan dirinya dalam UTF-8. Seperti yang Anda katakan, sebaiknya jangan menganggap teks tersebut dalam penyandian apa pun sampai Anda perlu ... tetapi saat dokumen XML menyatakan penyandian, itu adalah sesuatu yang perlu Anda pertimbangkan.
Jon Skeet
2
@ Garry, yang paling sederhana yang dapat saya pikirkan saat ini adalah mengambil contoh kedua dalam jawaban saya, tetapi ketika Anda membuat XmlWriterdo dengan metode pabrik yang mengambil XmlWriterSettingsobjek, dan OmitXmlDeclarationmenyetel properti ke true.
Jon Hanna
4
+1 Utf8StringWriterSolusi Anda sangat bagus dan bersih
Adriano Carneiro
17

Jawaban yang sangat bagus menggunakan pewarisan, ingatlah untuk mengganti penginisialisasi

public class Utf8StringWriter : StringWriter
{
    public Utf8StringWriter(StringBuilder sb) : base (sb)
    {
    }
    public override Encoding Encoding { get { return Encoding.UTF8; } }
}
Sebastian Castaldi
sumber
terima kasih, menurut saya ini adalah opsi yang paling elegan
Prokurors
5

Saya menemukan entri blog ini yang menjelaskan masalahnya dengan sangat baik, dan menjelaskan beberapa solusi berbeda:

(tautan mati dihapus)

Saya telah menetapkan gagasan bahwa cara terbaik untuk melakukannya adalah dengan sepenuhnya menghilangkan deklarasi XML saat berada di memori. Ini sebenarnya adalah UTF-16 pada saat itu pula, tapi deklarasi XML tampaknya tidak bermakna sampai telah ditulis ke file dengan encoding tertentu; dan bahkan deklarasi tersebut tidak diperlukan. Tampaknya tidak merusak deserialisasi, setidaknya.

Seperti yang disebutkan @Jon Hanna, ini dapat dilakukan dengan XmlWriter yang dibuat seperti ini:

XmlWriter writer = XmlWriter.Create (output, new XmlWriterSettings() { OmitXmlDeclaration = true });
Dave Andersen
sumber