
التسلسل وإلغاء التسلسل هي عمليات نموذجية يعاملها المطور الحديث على أنها تافهة. نتواصل مع قواعد البيانات ، وننشئ طلبات HTTP ، ونستقبل البيانات من خلال واجهة برمجة تطبيقات REST ، وغالباً ما لا نفكر في كيفية عملها. أقترح اليوم كتابة المسلسل و deserializer لـ JSON لمعرفة ما يوجد تحت الغطاء.
تنصل
مثل
آخر مرة ، سألاحظ: سوف نكتب مسلسل بدائي ، قد يقول أحدهم ، دراجة. إذا كنت بحاجة إلى حل جاهز ، فاستخدم
Json.NET . أصدر هؤلاء الأشخاص منتجًا رائعًا قابلاً للتخصيص بدرجة كبيرة ويمكنه فعل الكثير ويقوم
بالفعل بحل المشكلات التي تنشأ عند العمل مع JSON. استخدام الحل الخاص بك رائع حقًا ، ولكن فقط إذا كنت بحاجة إلى أقصى أداء أو تخصيص خاص أو إذا كنت تحب الدراجات بالطريقة التي أحبها.
مجال الموضوع
تتكون خدمة التحويل من JSON إلى تمثيل كائن من نظامين فرعيين على الأقل. Deserializer هو نظام فرعي يقوم بتحويل
JSON (نص) صالح إلى تمثيل كائن داخل برنامجنا. تتضمن عملية إلغاء التسلسل رمزية ، أي تحليل JSON إلى عناصر منطقية. Serializer هو نظام فرعي يؤدي المهمة العكسية: يحول تمثيل الكائن للبيانات إلى JSON.
غالبًا ما يرى المستهلك الواجهة التالية. لقد قمت بتبسيطه عمدا لتسليط الضوء على الأساليب الرئيسية التي تستخدم في معظم الأحيان.
public interface IJsonConverter { T Deserialize<T>(string json); string Serialize(object source); }
"تحت الغطاء" ، تتضمن إلغاء التسلسل رمزية (تحليل نص JSON) وبناء بعض البدائل التي تجعل من السهل إنشاء تمثيل كائن في وقت لاحق. لأغراض التدريب ، سنقوم بتخطي بناء المواد الأولية الوسيطة (على سبيل المثال ، JObject ، JProperty من Json.NET) وسنقوم على الفور بكتابة البيانات إلى الكائن. هذا ناقص ، لأنه يقلل من خيارات التخصيص ، لكن من المستحيل إنشاء مكتبة بأكملها في إطار مقال واحد.
tokenization
اسمحوا لي أن أذكرك بأن عملية الرمز المميز أو
التحليل المعجمي هي تحليل للنص بهدف الحصول على تمثيل مختلف وأكثر دقة للبيانات الموجودة فيه. عادةً ما يسمى هذا التمثيل
الرموز المميزة أو الرموز المميزة. لأغراض تحليل JSON ، يجب علينا إبراز الخصائص وقيمها ورموز بداية ونهاية البنى - أي الرموز المميزة التي يمكن تمثيلها على أنها JsonToken في الكود.
JsonToken هو هيكل يحتوي على قيمة (نص) ، وكذلك نوع من الرمز المميز. JSON تدوين صارم ، لذلك يمكن اختزال جميع أنواع الرموز إلى
التعداد التالي . بالطبع ، سيكون من الجيد أن تضيف إلى الرمز المميز إحداثياتها في البيانات الواردة (الصف والعمود) ، ولكن التصحيح خارج نطاق التنفيذ ، مما يعني أن JsonToken لا يحتوي على هذه البيانات.
لذلك ، فإن أسهل طريقة لتحليل النص إلى الرموز هي قراءة كل حرف بالتتابع ومقارنته بالأنماط. نحن بحاجة إلى فهم معنى هذا الرمز أو ذاك. من المحتمل أن تبدأ الكلمة الأساسية (صواب ، خطأ ، خالية) مع هذا الحرف ، من الممكن أن تكون هذه هي بداية السطر (علامة اقتباس) ، أو ربما تكون هذه الشخصية بحد ذاتها رمزية ([،] ، {،}). الفكرة العامة تبدو هكذا:
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; ... } }
بالنظر إلى الكود ، يبدو أنه يمكنك قراءة وفعل شيء على الفور مع بيانات القراءة. لا تحتاج إلى تخزين ، يجب إرسالها على الفور إلى المستهلك. وبالتالي ، يطالب IEnumerator معين ، والذي سيتم تحليل النص إلى أجزاء. أولاً ، سيؤدي هذا إلى تقليل التخصيص ، نظرًا لأننا لسنا بحاجة إلى تخزين النتائج الوسيطة (مجموعة من الرموز المميزة). ثانياً ، سنزيد من سرعة العمل - نعم ، في المثال الخاص بنا ، يمثل الإدخال سلسلة ، ولكن في الحالة الحقيقية ، سيتم
استبداله بـ Stream (من ملف أو شبكة) ، نقرأها بالتتابع.
أعددت رمز
JsonTokenizer ، والذي يمكن
العثور عليه هنا . الفكرة هي نفسها - الرمز المميز يمضي بالتتابع على طول الخط ، في محاولة لتحديد ما يشير إليه الرمز أو تسلسله. إذا تبين أن الأمر مفهوم ، فسننشئ عنصر تحكم مميز ونقله إلى المستهلك. إذا لم يكن الأمر واضحًا بعد ، فاقرأ.
التحضير لإلغاء تسلسل الكائنات
غالبًا ما يكون طلب تحويل البيانات من JSON عبارة عن استدعاء لطريقة Deserialize العامة ، حيث
TOUT هو نوع البيانات الذي يجب تعيين الرموز المميزة JSON به. حيث
Type : حان الوقت لتطبيق
Reflection و
ExpressionTrees . أساسيات العمل مع ExpressionTrees ، وكذلك لماذا تعد التعبيرات المترجمة أفضل من الانعكاس "العاري" ، لقد وصفت في مقالة سابقة حول
كيفية عمل AutoMapper . إذا كنت لا تعرف أي شيء عن Expression.Labmda.Compile () - أوصي بقراءته. يبدو لي ، على سبيل المثال من مخطط الخرائط ، أنه تبين بشكل مفهوم.
لذلك ، تستند خطة إنشاء deserializer كائن إلى معرفة أنه يمكننا الحصول على أنواع الخصائص من نوع TOut في أي وقت ، أي مجموعة
PropertyInfo . في الوقت نفسه ، يتم تحديد أنواع الممتلكات بترميز JSON: الأرقام والسلاسل والمصفوفات والكائنات. حتى لو لم ننسَ لاغية ، فليس هذا كثيرًا كما يبدو للوهلة الأولى. وإذا كان لكل نوع بدائي ، فسنضطر إلى إنشاء مصمم منفصل ، ثم بالنسبة للصفائف والأشياء ، يمكننا تكوين فصول عامة. إذا كنت تعتقد قليلاً ، فيمكن تخفيض كل المتسللين (أو
المحولات ) إلى الواجهة التالية:
public interface IJsonConverter<T> { T Deserialize(JsonTokenizer tokenizer); void Serialize(T value, StringBuilder builder); }
تعتبر شفرة المحول المكتوب بقوة للأنواع البدائية بسيطة قدر الإمكان: نقوم باستخراج JsonToken الحالي من الرمز المميز ونحوله إلى قيمة عن طريق التحليل. على سبيل المثال ، float.Parse (currentToken.Value). نلقي نظرة على
BoolConverter أو
FloatConverter - لا شيء معقد. بعد ذلك ، إذا كنت بحاجة إلى deserializer عن منطقي؟ أو تطفو؟ ، ويمكن أيضا أن تضاف.
صف التسلسل
شفرة الفئة العامة لتحويل صفيف من JSON هي أيضًا بسيطة نسبيًا. يتم
تحديدها حسب نوع العنصر الذي يمكننا استخراج
Type.GetElementType () . تحديد أن نوعًا ما هو صفيف أمر بسيط أيضًا:
Type.IsArray . يأتي إلغاء تسلسل الصفيف إلى رمز tokenizer.MoveNext () حتى يتم الوصول إلى رمز نوع ArrayEnd. إن إلغاء تسلسل عناصر الصفيف هو إلغاء تسلسل نوع عنصر الصفيف ، وبالتالي ، عند إنشاء ArrayConverter ، يتم تمرير عنصر إلغاء تسلسل العنصر إليه.
في بعض الأحيان توجد صعوبات في إنشاء مثيل للتطبيقات العامة ، لذلك سوف أخبرك على الفور بكيفية القيام بذلك. يسمح لك الانعكاس بإنشاء أنواع عامة في الوقت الفعلي ، مما يعني أنه يمكننا استخدام النوع الذي تم إنشاؤه كوسيطة لـ Activator.CreateInstance. استفد من هذا:
Type elementType = arrayType.GetElementType(); Type converterType = typeof(ArrayConverter<>).MakeGenericType(elementType); var converterInstance = Activator.CreateInstance(converterType, object[] args);
عند الانتهاء من الاستعدادات لإنشاء
مصمم إزالة الكائنات ، يمكنك وضع كل رمز البنية التحتية المرتبطة بإنشاء وتخزين مواد
إزالة الملوثات في واجهة
JConverter . سيكون مسؤولاً عن جميع عمليات التسلسل وإلغاء التسلسل JSON وهو متاح للمستهلكين كخدمة.
إلغاء تسلسل الأشياء
اسمحوا لي أن أذكرك أنه يمكنك الحصول على جميع خصائص النوع T مثل هذا: typeof (T) .GetProperties (). لكل خاصية ، يمكنك استخراج
PropertyInfo.PropertyType ، مما سيتيح لنا الفرصة لإنشاء IJsonConverter مكتوب لتسلسل البيانات وإلغاء تسلسلها من نوع معين. إذا كان نوع العقار عبارة عن صفيف ، فإننا نقوم بإنشاء مثيل لـ ArrayConverter أو العثور على واحد مناسب من بين الموجودات الحالية. إذا كان نوع الخاصية نوعًا بدائيًا ، فسيتم بالفعل إنشاء مُزيلات (محولات) لهم في مُنشئ JConverter.
يمكن عرض الكود الناتج في
ObjectConverter من الفئة العامة. يتم إنشاء المنشط في المنشئ الخاص به ، ويتم استخراج الخصائص من قاموس مُعد خصيصًا ، ويتم إنشاء طريقة لإزالة التسلسل لكل منها - الإجراء <TObject ، JsonTokenizer>. هناك حاجة ، أولاً ، من أجل ربط IJsonConverter على الفور بالخاصية المطلوبة ، وثانياً ، لتجنب الملاكمة عند استخراج الأنواع البدائية وكتابتها. تعرف كل طريقة لإلغاء التسلسل عن خاصية الكائن الصادر التي سيتم تسجيلها ، ويتم إلغاء كتابة القيمة بدقة وإرجاع القيمة بالضبط في النموذج المطلوب.
الربط من IJsonConverter إلى خاصية كما يلي:
Type converterType = propertyValueConverter.GetType(); ConstantExpression Expression.Constant(propertyValueConverter, converterType); MethodInfo deserializeMethod = converterType.GetMethod("Deserialize"); var value = Expression.Call(converter, deserializeMethod, tokenizer);
يتم إنشاء ثابت
Expression.Constant مباشرة في التعبير ، الذي يخزن مرجعًا إلى مثيل deserializer لقيمة الخاصية. هذا ليس بالضبط الثابت الذي نكتبه في "C # العادية" ، لأنه يمكن تخزين نوع مرجعي. بعد ذلك ، يتم استرداد أسلوب Deserialize من نوع deserializer ، والذي يُرجع قيمة النوع المطلوب ، ثم يطلق عليه -
Expression.Call . وبالتالي ، نحصل على طريقة تعرف بالضبط أين وماذا نكتب. يبقى وضعه في القاموس والاتصال به عندما يظهر رمز مميز من نوع الخاصية بالاسم المطلوب من الرمز المميز. ميزة أخرى هي أن كل شيء يعمل بسرعة كبيرة.
كيف سريع
الدراجات ، كما لوحظ في البداية ، من المنطقي أن تكتب في عدة حالات: إذا كانت هذه محاولة لفهم كيفية عمل التكنولوجيا ، أو تحتاج إلى تحقيق بعض النتائج الخاصة. على سبيل المثال ، السرعة. يمكنك التأكد من أن deserializer
يلغي حقًا
الاختبارات المجهزة (استخدم
AutoFixture للحصول على بيانات الاختبار). بالمناسبة ، ربما لاحظت أنني كتبت أيضًا تسلسل الكائنات. لكن بما أن المقالة اتضح أنها كبيرة جدًا ، فلن أصفها ، بل سأعطيها مقاييس. نعم ، تمامًا كما في المقالة السابقة ، كتبت مقاييس باستخدام مكتبة
BenchmarkDotNet .
بالطبع ،
قارنت سرعة إلغاء التسلسل مع Newtonsoft (Json.NET) ، باعتباره الحل الأكثر شيوعًا والموصى به للعمل مع JSON. علاوة على ذلك ، مباشرة على موقعه على شبكة الإنترنت: هو مكتوب: 50 ٪ أسرع من DataContractJsonSerializer ، و 250 ٪ أسرع من JavaScriptSerializer. باختصار ، أردت أن أعرف كم ستفقد شفرتي. لقد فاجأتني النتائج: لاحظ أن تخصيص البيانات أقل بثلاث مرات تقريبًا ، ومعدل إلغاء التسلسل أسرع مرتين تقريبًا.
أسفرت مقارنة السرعة والتخصيص
أثناء تسلسل البيانات عن نتائج أكثر إثارة للاهتمام. اتضح أن مسلسل الدراجة خصص خمس مرات تقريبًا وعمل بشكل أسرع ثلاث مرات تقريبًا. إذا كانت السرعة تضايقني حقًا (كثيرًا) ، فسيكون ذلك نجاحًا واضحًا.
نعم ، عند قياس السرعة ، لم أستخدم
التلميحات لزيادة الإنتاجية المنشورة على موقع Json.NET الإلكتروني. أخذت قياسات من خارج الصندوق ، أي وفقًا للسيناريو الأكثر شيوعًا: JsonConvert.DeserializeObject. قد تكون هناك طرق أخرى لتحسين الأداء ، لكنني لا أعرف عنها.
النتائج
على الرغم من السرعة العالية نسبيًا للتسلسل وإلغاء التسلسل ، لا أوصي بالتخلي عن Json.NET لصالح الحل الخاص بي. يتم احتساب الزيادة في السرعة بالميلي ثانية ، وهي "تغرق" بسهولة في تأخيرات الشبكة أو القرص أو الكود ، والتي توجد بشكل هرمي أعلى المكان الذي يتم فيه تطبيق التسلسل. دعم مثل هذه الحلول الاحتكارية هو الجحيم ، حيث لا يُسمح إلا للمطورين الذين لديهم معرفة جيدة بالموضوع.
نطاق هذه الدراجات هو التطبيقات المصممة بالكامل بهدف الحصول على أداء عالي ، أو مشاريع الحيوانات الأليفة حيث تفهم كيف تعمل هذه التقنية أو تلك. آمل أن أكون قد ساعدتكم قليلاً في كل هذا.