Konverter JSON Anda sendiri atau lebih banyak tentang ExpressionTrees



Serialisasi dan deserialisasi adalah operasi tipikal yang oleh pengembang modern dianggap sepele. Kami berkomunikasi dengan basis data, menghasilkan permintaan HTTP, menerima data melalui REST API, dan sering kali bahkan tidak memikirkan cara kerjanya. Hari ini saya menyarankan untuk menulis serializer dan deserializer saya untuk JSON untuk mencari tahu apa yang ada di balik tudung.

Penafian


Seperti terakhir kali , saya akan perhatikan: kita akan menulis serializer primitif, bisa dikatakan, sepeda. Jika Anda memerlukan solusi turnkey, gunakan Json.NET . Orang-orang ini merilis produk hebat yang sangat dapat dikustomisasi, dapat melakukan banyak hal dan sudah menyelesaikan masalah yang muncul ketika bekerja dengan JSON. Menggunakan solusi Anda sendiri benar-benar keren, tetapi hanya jika Anda membutuhkan kinerja maksimum, penyesuaian khusus, atau Anda suka sepeda seperti yang saya suka.

Bidang subjek


Layanan untuk mengkonversi dari JSON ke representasi objek terdiri dari setidaknya dua subsistem. Deserializer adalah subsistem yang mengubah JSON (teks) yang valid menjadi representasi objek di dalam program kami. Deserialisasi melibatkan tokenization, yaitu, mengurai JSON menjadi elemen logis. Serializer adalah subsistem yang melakukan tugas terbalik: mengubah representasi objek data menjadi JSON.

Konsumen paling sering melihat antarmuka berikut. Saya sengaja menyederhanakannya untuk menyoroti metode utama yang paling sering digunakan.

public interface IJsonConverter { T Deserialize<T>(string json); string Serialize(object source); } 

"Di bawah tenda," deserialisasi termasuk tokenization (parsing teks JSON) dan membangun beberapa primitif yang membuatnya lebih mudah untuk membuat representasi objek di kemudian hari. Untuk tujuan pelatihan, kami akan melewati konstruksi primitif menengah (misalnya, JObject, JProperty dari Json.NET) dan kami akan segera menulis data ke objek. Ini adalah minus, karena mengurangi opsi untuk kustomisasi, tetapi tidak mungkin untuk membuat seluruh perpustakaan dalam kerangka satu artikel.

Tokenisasi


Biarkan saya mengingatkan Anda bahwa proses tokenization atau analisis leksikal adalah penguraian teks dengan tujuan mendapatkan representasi yang berbeda, lebih ketat dari data yang terkandung di dalamnya. Biasanya, representasi ini disebut token atau token. Untuk keperluan parsing JSON, kita harus menyoroti properti, nilainya, simbol awal dan akhir struktur - yaitu, token yang dapat direpresentasikan sebagai JsonToken dalam kode.

JsonToken adalah struktur yang berisi nilai (teks), serta jenis token. JSON adalah notasi yang ketat, sehingga semua jenis token dapat dikurangi ke enum berikutnya . Tentu saja, akan bagus untuk menambahkan token koordinatnya dalam data yang masuk (baris dan kolom), tetapi debugging berada di luar cakupan implementasi, yang berarti bahwa JsonToken tidak mengandung data ini.

Jadi, cara termudah untuk mem-parsing teks menjadi token adalah membaca setiap karakter secara berurutan dan membandingkannya dengan pola. Kita perlu memahami apa arti simbol ini atau itu. Ada kemungkinan bahwa kata kunci (true, false, null) dimulai dengan karakter ini, ada kemungkinan bahwa ini adalah awal dari garis (tanda kutip), atau mungkin karakter ini sendiri adalah token ([,], {,}). Ide umumnya terlihat seperti ini:

 var tokens = new List<JsonToken>(); for (int i = 0; i < json.Length; i++) { char ch = json[i]; switch (ch) { case '[': tokens.Add(new JsonToken(JsonTokenType.ArrayStart)); break; case ']': tokens.Add(new JsonToken(JsonTokenType.ArrayEnd)); break; case '"': string stringValue = ReadString(); tokens.Add(new JsonToken(JsonTokenType.String, stringValue); break; ... } } 

Melihat kode tersebut, tampaknya Anda dapat membaca dan segera melakukan sesuatu dengan data yang dibaca. Mereka tidak perlu disimpan, mereka harus segera dikirim ke konsumen. Jadi, seorang IEnumerator memohon, yang akan mem-parsing teks menjadi beberapa bagian. Pertama, ini akan mengurangi alokasi, karena kita tidak perlu menyimpan hasil antara (array token). Kedua, kami akan meningkatkan kecepatan kerja - ya, dalam contoh kami inputnya berupa string, tetapi dalam situasi nyata, input akan diganti oleh Stream (dari file atau jaringan), yang kami baca secara berurutan.

Saya sudah menyiapkan kode JsonTokenizer , yang dapat ditemukan di sini . Idenya adalah sama - tokenizer secara berurutan berjalan sepanjang garis, mencoba menentukan apa simbol atau urutannya merujuk. Jika kami berhasil memahami, maka kami membuat token dan mentransfer kendali ke konsumen. Jika belum jelas, baca terus.

Bersiap untuk Deserialize Objek


Paling sering, permintaan untuk mengonversi data dari JSON adalah panggilan ke metode generik Deserialize, di mana TOut adalah tipe data yang harus dipetakan dengan token JSON. Di mana Type adalah : saatnya menerapkan Reflection dan ExpressionTrees . Dasar-dasar bekerja dengan ExpressionTrees, serta mengapa ekspresi yang dikompilasi lebih baik daripada Refleksi "telanjang", saya jelaskan dalam artikel sebelumnya tentang cara membuat AutoMapper Anda . Jika Anda tidak tahu apa-apa tentang Expression.Labmda.Compile () - Saya sarankan membacanya. Sepertinya saya bahwa contoh mapper ternyata cukup dimengerti.

Jadi, rencana untuk membuat deserializer objek didasarkan pada pengetahuan bahwa kita bisa mendapatkan tipe properti dari tipe TOut kapan saja, yaitu koleksi PropertyInfo . Pada saat yang sama, tipe properti dibatasi oleh notasi JSON: angka, string, array dan objek. Bahkan jika kita tidak melupakan null, ini tidak sebanyak yang terlihat pada pandangan pertama. Dan jika untuk setiap tipe primitif kita akan dipaksa untuk membuat deserializer terpisah, maka untuk array dan objek kita dapat membuat kelas generik. Jika Anda berpikir sedikit, semua serializer-deserializers (atau konverter ) dapat dikurangi menjadi antarmuka berikut:

 public interface IJsonConverter<T> { T Deserialize(JsonTokenizer tokenizer); void Serialize(T value, StringBuilder builder); } 

Kode konverter tipe primitif sangat sederhana: kita mengekstrak JsonToken saat ini dari tokenizer dan mengubahnya menjadi nilai dengan menguraikan. Misalnya, float.Parse (currentToken.Value). Lihatlah BoolConverter atau FloatConverter - tidak ada yang rumit. Selanjutnya, jika Anda membutuhkan deserializer untuk bool? atau float ?, bisa juga ditambahkan.

Deserialisasi array


Kode kelas umum untuk mengkonversi array dari JSON juga relatif sederhana. Itu diparameterisasi oleh tipe elemen yang bisa kita ekstrak Type.GetElementType () . Menentukan bahwa suatu tipe adalah array juga sederhana: Type.IsArray . Deserialisasi array datang ke tokenizer.MoveNext () hingga token jenis ArrayEnd tercapai. Deserialisasi elemen array adalah deserialisasi tipe elemen array, oleh karena itu, ketika membuat ArrayConverter, elemen deserializer diteruskan ke sana.

Terkadang ada kesulitan dengan Instansiasi implementasi generik, jadi saya akan segera memberitahu Anda bagaimana melakukannya. Refleksi memungkinkan Anda untuk membuat tipe generik secara realtime, yang berarti kita dapat menggunakan tipe yang dibuat sebagai argumen untuk Activator.CreateInstance. Manfaatkan ini:

 Type elementType = arrayType.GetElementType(); Type converterType = typeof(ArrayConverter<>).MakeGenericType(elementType); var converterInstance = Activator.CreateInstance(converterType, object[] args); 

Menyelesaikan persiapan untuk membuat deserializer objek, Anda dapat menempatkan semua kode infrastruktur yang terkait dengan pembuatan dan penyimpanan deserializer dalam fasad JConverter . Dia akan bertanggung jawab atas semua operasi serialisasi dan deserialisasiisasi JSON dan tersedia untuk konsumen sebagai layanan.

Deserialisasi objek


Biarkan saya mengingatkan Anda bahwa Anda bisa mendapatkan semua properti bertipe T seperti ini: typeof (T) .GetProperties (). Untuk setiap properti, Anda dapat mengekstrak PropertyInfo.PropertyType , yang akan memberi kami kesempatan untuk membuat IJsonConverter yang diketik untuk membuat serial dan deserialisasi data dari tipe tertentu. Jika jenis properti adalah array, maka kami instantiate ArrayConverter atau menemukan yang cocok di antara yang ada. Jika tipe properti adalah tipe primitif, maka deserializers (konverter) sudah dibuat untuk mereka di konstruktor JConverter.

Kode yang dihasilkan dapat dilihat di ObjectConverter kelas generik. Aktivator dibuat dalam konstruktornya, properti diekstraksi dari kamus yang disiapkan khusus, dan untuk masing-masingnya dibuat metode deserialisasi - Action <TObject, JsonTokenizer>. Diperlukan, pertama, untuk segera menghubungkan IJsonConverter dengan properti yang diinginkan, dan kedua, untuk menghindari tinju saat mengekstraksi dan menulis tipe primitif. Setiap metode deserialisasi tahu properti mana dari objek yang keluar yang akan dicatat, deserializer nilai diketik secara ketat dan mengembalikan nilai persis dalam bentuk yang dibutuhkan.

Ikatan IJsonConverter ke properti adalah sebagai berikut:

 Type converterType = propertyValueConverter.GetType(); ConstantExpression Expression.Constant(propertyValueConverter, converterType); MethodInfo deserializeMethod = converterType.GetMethod("Deserialize"); var value = Expression.Call(converter, deserializeMethod, tokenizer); 

Konstanta Expression.Constant dibuat langsung dalam ekspresi, yang menyimpan referensi ke instance deserializer untuk nilai properti. Ini bukan konstanta yang kita tulis dalam โ€œregular C #โ€, karena ia dapat menyimpan tipe referensi. Selanjutnya, metode Deserialize diambil dari jenis deserializer, yang mengembalikan nilai dari tipe yang diinginkan, dan kemudian disebut - Expression.Call . Jadi, kita mendapatkan metode yang tahu persis di mana dan apa yang harus ditulis. Tetap memasukkannya ke dalam kamus dan menyebutnya ketika token dari tipe Properti dengan nama yang diinginkan "berasal" dari tokenizer. Kelebihan lainnya adalah semuanya bekerja dengan sangat cepat.

Seberapa cepat


Sepeda, seperti disebutkan di awal, masuk akal untuk menulis dalam beberapa kasus: jika ini adalah upaya untuk memahami cara kerja teknologi, atau Anda perlu mencapai beberapa hasil khusus. Misalnya, kecepatan. Anda dapat memastikan bahwa deserializer benar-benar deserialize dengan tes yang disiapkan (saya menggunakan AutoFixture untuk mendapatkan data uji). Ngomong-ngomong, Anda mungkin memperhatikan bahwa saya juga menulis serialisasi objek. Tapi karena artikelnya ternyata cukup besar, saya tidak akan menjelaskannya, tetapi hanya memberikan tolok ukur. Ya, sama seperti dengan artikel sebelumnya, saya menulis benchmark menggunakan perpustakaan BenchmarkDotNet .

Tentu saja, saya membandingkan kecepatan deserialisasi dengan Newtonsoft (Json.NET), sebagai solusi yang paling umum dan direkomendasikan untuk bekerja dengan JSON. Selain itu, tepat di situs web mereka tertulis: 50% lebih cepat dari DataContractJsonSerializer, dan 250% lebih cepat dari JavaScriptSerializer. Singkatnya, saya ingin tahu berapa banyak kode saya akan hilang. Hasilnya mengejutkan saya: perhatikan bahwa alokasi data hampir tiga kali lebih sedikit, dan laju deserialisasi sekitar dua kali lebih cepat.
MetodeBerartiKesalahanStddevRasioDialokasikan
Newtonsoft75,39 ms0,3027 ms0,2364 ms1,0035,47 MB
Velo31,78 ms0,1135 ms0,1062 ms0,4212,36 MB

Perbandingan kecepatan dan alokasi selama serialisasi data menghasilkan hasil yang lebih menarik. Ternyata serializer sepeda mengalokasikan hampir lima kali lebih sedikit dan bekerja hampir tiga kali lebih cepat. Jika kecepatan benar-benar mengganggu saya (sangat banyak), itu akan menjadi keberhasilan yang jelas.
MetodeBerartiKesalahanStddevRasioDialokasikan
Newtonsoft54,83 ms0,5582 ms0,5222 ms1,0025,44 MB
Velo20,66 ms0,0484 ms0,0429 ms0,385,93 MB

Ya, ketika mengukur kecepatan, saya tidak menggunakan tips untuk meningkatkan produktivitas yang diposting di situs web Json.NET. Saya mengambil pengukuran dari kotak, yaitu, sesuai dengan skenario yang paling umum digunakan: JsonConvert.DeserializeObject. Mungkin ada cara lain untuk meningkatkan kinerja, tetapi saya tidak tahu tentang mereka.

Kesimpulan


Meskipun kecepatan serialisasi dan deserialisasi yang relatif tinggi, saya tidak akan merekomendasikan meninggalkan Json.NET mendukung solusi saya sendiri. Keuntungan dalam kecepatan dihitung dalam milidetik, dan mereka dengan mudah "tenggelam" dalam penundaan jaringan, disk atau kode, yang secara hierarkis terletak di atas tempat serialisasi diterapkan. Untuk mendukung solusi eksklusif semacam itu adalah neraka, di mana hanya pengembang yang fasih dalam bidang ini yang diizinkan.

Ruang lingkup sepeda tersebut adalah aplikasi yang dirancang sepenuhnya dengan tujuan untuk kinerja tinggi, atau proyek kesayangan di mana Anda memahami bagaimana teknologi ini atau itu bekerja. Saya harap saya sedikit membantu Anda dalam semua ini.

Source: https://habr.com/ru/post/id464525/


All Articles