مرحبا زملائي!
نذكر الجميع بأن لدينا كتابًا رائعًا لـ Mark Price ، "
C # 7 و .NET Core. تطوير عبر الأنظمة الأساسية للمحترفين ". يرجى ملاحظة ما يلي: هذه هي الطبعة الثالثة ، وقد تمت كتابة الإصدار الأول في الإصدار 6.0 ولم تظهر باللغة الروسية ، وتم إصدار الإصدار الثالث في الأصل في نوفمبر 2017 ويغطي الإصدار 7.1.
بعد إصدار هذه الخلاصة ، التي خضعت لتحرير علمي منفصل للتحقق من التوافق العكسي والصحة الأخرى للمادة المقدمة ، قررنا ترجمة مقالة مثيرة للاهتمام كتبها John Skeet حول الصعوبات المعروفة وغير المعروفة في التوافق العكسي التي يمكن أن تنشأ في C #. قراءة جيدة.
في يوليو 2017 ، بدأت كتابة مقال عن الإصدار. تخلت عنه قريبًا ، لأن الموضوع كان واسعًا جدًا بحيث لا يغطيه في منشور واحد فقط. في مثل هذا الموضوع ، من المنطقي إبراز موقع / ويكي / مستودع بالكامل. آمل أن أعود إلى هذا الموضوع يومًا ما ، لأنني أعتبره مهمًا للغاية وأعتقد أنه يحظى باهتمام أقل بكثير مما يستحقه.
لذلك ، في النظام البيئي .NET ، عادة ما يتم الترحيب
بالإصدار الدلالي - يبدو الأمر رائعًا ، ولكنه يتطلب من الجميع أن يفهموا على قدم المساواة ما يعتبر "تغييرًا أساسيًا". هذا ما كنت أفكر فيه لفترة طويلة. أحد الجوانب التي أدهشتني مؤخرًا هو مدى صعوبة تجنب التغييرات الأساسية عند زيادة التحميل على الطرق. حول هذا (بشكل رئيسي) سنناقش المنشور الذي تقرأه ؛ بعد كل شيء ، هذا الموضوع مثير جدا للاهتمام.
للبدء - تعريف موجز ...
المصادر والتوافق الثنائيإذا تمكنت من إعادة ترجمة كود العميل الخاص بي مع الإصدار الجديد من المكتبة ، وكل شيء يعمل بشكل جيد ، فهذا هو التوافق على مستوى رمز المصدر. إذا تمكنت من إعادة نشر عميلي الثنائي مع الإصدار الجديد من المكتبة دون إعادة الترجمة ، فهو متوافق مع الثنائي. لا شيء من هذا هو مجموعة شاملة للأخرى:
- قد تكون بعض التغييرات غير متوافقة مع كل من شفرة المصدر والرمز الثنائي في نفس الوقت - على سبيل المثال ، لا يمكنك حذف نوع عام كامل تعتمد عليه تمامًا.
- تتوافق بعض التغييرات مع شفرة المصدر ، ولكنها غير متوافقة مع الشفرة الثنائية - على سبيل المثال ، إذا قمت بتحويل حقل ثابت عام للقراءة فقط إلى خاصية.
- تتوافق بعض التغييرات مع الثنائية ، ولكنها غير متوافقة مع المصدر - على سبيل المثال ، إضافة حمولة زائدة يمكن أن تسبب الغموض أثناء الترجمة.
- تتوافق بعض التغييرات مع كل من المصدر والشفرة الثنائية - على سبيل المثال ، تنفيذ جديد لنص الطريقة.
إذن ما الذي نتحدث عنه؟لنفترض أن لدينا مكتبة عامة من الإصدار 1.0 ، ونريد إضافة العديد من الأحمال الزائدة إليها من أجل الانتهاء من الإصدار 1.1. نحن نتمسك بالإصدار الدلالي ، لذلك نحتاج إلى توافق عكسي. ما الذي يعنيه هذا أننا لا نستطيع ولا يمكننا القيام به ، وهل يمكن الإجابة على جميع الأسئلة هنا بنعم أو لا؟
في أمثلة مختلفة ، سأعرض الرمز في الإصدارين 1.0 و 1.1 ، ثم رمز "العميل" (أي الرمز الذي يستخدم المكتبة) ، والذي قد ينكسر نتيجة للتغييرات. لن تكون هناك هيئات منهجية ، ولا إعلانات طبقية ، لأنها في الأساس ليست مهمة - نحن نولي الاهتمام الرئيسي للتوقيعات. ومع ذلك ، إذا كنت مهتمًا ، فيمكن إعادة إنتاج كل هذه الفئات والطرق بسهولة. افترض أن جميع الطرق الموضحة هنا في فئة
Library
.
أبسط تغيير يمكن تصوره ، مزين بتحويل مجموعة من الأساليب إلى مندوب
أبسط مثال يتبادر إلى ذهني هو إضافة طريقة ذات معلمات حيث توجد بالفعل طريقة غير معلمة:
حتى هنا ، التوافق غير كامل. خذ بعين الاعتبار كود العميل التالي:
في الإصدار الأول من المكتبة ، كل شيء على ما يرام. يؤدي استدعاء أسلوب
HandleAction
تحويل مجموعة الأساليب إلى
library.Foo
، لذلك يتم إنشاء إجراء ، ونتيجة لذلك ، يتم إنشاء إجراء. في الإصدار 1.1 ، يصبح الموقف غامضًا: يمكن تحويل مجموعة من الأساليب إلى إجراء أو إجراء. بمعنى آخر ، هذا التغيير لا يتوافق مع شفرة المصدر.
في هذه المرحلة ، من المغري أن تستسلم وتعد نفسك ببساطة بعدم إضافة أي حمولة زائدة مرة أخرى. أو يمكننا القول أن مثل هذه الحالة من غير المرجح أن تكون خائفة من مثل هذا الفشل. دعونا ندعو تحولات مجموعة من الأساليب خارج النطاق في الوقت الحالي.
أنواع مرجعية غير ذات صلةضع في اعتبارك سياقًا آخر حيث يجب عليك استخدام الأحمال الزائدة بنفس عدد المعلمات. يمكن افتراض أن مثل هذا التغيير في المكتبة سيكون غير مدمر:
للوهلة الأولى ، كل شيء منطقي. نحافظ على الطريقة الأصلية ، لذلك لن نكسر التوافق الثنائي. إن أبسط طريقة لكسرها هي كتابة مكالمة تعمل في v1.0 ، ولكنها لا تعمل في v1.1 ، أو تعمل في كلا الإصدارين ، ولكن بطرق مختلفة.
ما هو عدم التوافق بين v1.0 و v1.1 الذي يمكن أن تعطيه هذه المكالمة؟ يجب أن يكون لدينا وسيطة متوافقة مع كل من
string
و
FileStream
. لكن هذه أنواع مرجعية لا تتعلق ببعضها البعض ...
الفشل الأول ممكن إذا قمنا بتحويل ضمني معرف من قبل المستخدم لكل من
string
و
FileStream
:
آمل أن تكون المشكلة واضحة: الشفرة التي كانت غامضة سابقًا وعملت مع
string
أصبحت الآن غامضة ، حيث يمكن تحويل النوع
OddlyConvertible
بشكل ضمني إلى كل من
string
و
FileStream
(كلا
OddlyConvertible
للتطبيق ، ولا أحد منهما أفضل من الآخر).
ربما في هذه الحالة ، من المعقول حظر التحويلات المعرفة من قبل المستخدم ... ولكن يمكن إسقاط هذا الرمز وأسهل بكثير:
يمكننا ضمنيًا تحويل قيمة حرفية فارغة إلى أي نوع مرجعي أو إلى أي نوع ذي أهمية لاغية ... وبالتالي ، مرة أخرى ، فإن الوضع في الإصدار 1.1 غامض. دعنا نحاول مرة أخرى ...
معلمات الأنواع المرجعية والأنواع الهامة غير القابلة للإلغاءلنفترض أننا لا نهتم بالتحويلات التي يحددها المستخدم ، لكننا لا نرغب في استخدام القيم الخالية من المشاكل. كيف في هذه الحالة لإضافة الزائد مع نوع مهم غير قابل للإلغاء؟
للوهلة الأولى ، إنه جيد -
library.Foo(null)
.
library.Foo(null)
ستعمل بشكل جيد في v1.1. فهل هو آمن؟ لا ، ليس فقط في C # 7.1 ...
القيمة الحرفية الافتراضية فارغة تمامًا ، ولكنها تنطبق على أي نوع. هذا مريح للغاية - وصداع حقيقي عندما يتعلق الأمر بالحمل الزائد والتوافق :(
معلمات اختياريةالمعلمات الاختيارية مشكلة أخرى. لنفترض أن لدينا معلمة اختيارية واحدة ، ونريد إضافة معلمة ثانية. لدينا ثلاثة خيارات ، تم تحديدها أدناه مثل 1.1a و 1.1 b و 1.1 c.
ولكن ماذا لو قام العميل بإجراء مكالمتين:
تحافظ Library 1.1a على التوافق على المستوى الثنائي ، ولكنها تنتهك على مستوى التعليمات البرمجية المصدر: الآن
library.Foo()
غامضة. وفقًا لقواعد التحميل الزائد في C # ، يفضل الطرق التي لا تتطلب من المترجم "ملء" جميع المعلمات الاختيارية المتاحة ، ومع ذلك ، فإنه لا ينظم عدد المعلمات الاختيارية التي يمكن ملؤها.
تحتفظ Library 1.1b بالتوافق على مستوى المصدر ، ولكنها تنتهك التوافق الثنائي. تم تصميم التعليمات البرمجية المترجمة الموجودة لاستدعاء أسلوب بمعلمة واحدة - ولم تعد هذه الطريقة موجودة.
تحتفظ مكتبة 1.1c بالتوافق الثنائي ، ولكنها محفوفة بالمفاجآت المحتملة على مستوى التعليمات البرمجية المصدر. الآن يتم حل المكالمة
library.Foo()
في طريقة مع معلمتين ، بينما
library.Foo("xyz")
حل
library.Foo("xyz")
في طريقة بمعلمة واحدة (من وجهة نظر المترجم ، يفضل على طريقة ذات معلمتين ، ويرجع ذلك أساسًا إلى عدم وجود معلمات اختيارية لا ملء المطلوبة). قد يكون هذا مقبولًا إذا كان الإصدار الذي يحتوي على معلمة واحدة يقوم ببساطة بتفويض الإصدارات بمعلمتين ، وفي كلتا الحالتين يتم استخدام نفس القيمة الافتراضية. ومع ذلك ، يبدو من الغريب أن قيمة المكالمة الأولى ستتغير إذا كانت الطريقة التي تم حلها سابقًا لا تزال موجودة.
يصبح الموقف مع المعلمات الاختيارية أكثر إرباكًا إذا كنت تريد إضافة معلمة جديدة ليس في النهاية ، ولكن في المنتصف - على سبيل المثال ، حاول الالتزام بالاتفاقية والحفاظ على المعلمة الاختيارية CancellationToken في النهاية. لن أخوض في هذا ...
طرق معممةلم يكن الانتهاء من الأنواع في أفضل الأوقات مهمة سهلة. عندما يتعلق الأمر بحل الأحمال الزائدة ، يتحول هذا العمل إلى كابوس موحد.
افترض أن لدينا طريقة واحدة غير معممة في v1.0 ، وفي v1.1 نضيف طريقة معممة أخرى.
للوهلة الأولى ، الأمر ليس مخيفًا جدًا ... ولكن دعنا نرى ما يحدث في رمز العميل:
في المكتبة v1.0 ، يتم حل كل من المكالمات في
Foo(object)
- الطريقة الوحيدة المتاحة.
مكتبة v1.1 متوافقة مع الإصدارات السابقة: إذا أخذت ملف العميل القابل للتنفيذ الذي تم تجميعه لـ v1.1 ، فستستمر كلتا المكالمتين في استخدام
Foo(object)
. ولكن ، في حالة إعادة الترجمة ، سيتم تحويل المكالمة الثانية (والثانية فقط) للعمل مع الطريقة المعممة. تنطبق كلتا الطريقتين على كل من المكالمات.
في المكالمة الأولى ، سيظهر الاستدلال النوعي أن
T
هو
object
، لذلك سيتم تحويل الوسيط إلى نوع المعلمة في كلتا الحالتين إلى
object
في
object
. عظيم. سيطبق المترجم القاعدة القائلة بأن الطرق غير العامة هي الأفضل دائمًا على الطرق العامة.
في المكالمة الثانية ، سيظهر الاستدلال النوعي أن
T
ستكون دائمًا
string
، لذلك عند تحويل وسيطة إلى معلمة نوع ، نحصل على
string
object
للطريقة الأصلية أو
string
إلى
string
للطريقة المعممة. التحول الثاني هو "أفضل" ، لذلك يتم اختيار الطريقة الثانية.
إذا كانت الطريقتان تعملان بنفس الطريقة ، فلا بأس. إذا لم يكن الأمر كذلك ، فسوف تكسر التوافق بطريقة غير واضحة للغاية.
الميراث والكتابة الديناميكيةآسف ، لقد عانيت من التنفس. يمكن أن يظهر كل من الميراث والكتابة الديناميكية عند حل الأحمال الزائدة بأكثر الطرق "الغامضة" والغامضة.
إذا أضفنا مثل هذه الطريقة على مستوى واحد من التسلسل الهرمي للوراثة التي ستحمل طريقة الفئة الأساسية بشكل زائد ، فستتم معالجة الطريقة الجديدة أولاً وسيتم تفضيلها على طريقة الفئة الأساسية ، حتى إذا كانت طريقة الفئة الأساسية أكثر دقة عند تحويل وسيطة إلى معلمة نوع. هناك مساحة كافية لخلط كل شيء.
وينطبق الشيء نفسه على الكتابة الديناميكية (في رمز العميل) ؛ إلى حد ما ، يصبح الوضع غير متوقع. لقد قمت بالفعل بالتضحية بجدية بالأمن أثناء التجميع ... لذلك لا تتفاجأ إذا حدث أي شيء.
الملخصحاولت أن أجعل الأمثلة في هذه المقالة بسيطة بما فيه الكفاية. يصبح كل شيء معقدًا للغاية ، وبسرعة كبيرة ، عندما يكون لديك الكثير من المعلمات الاختيارية. تعيين الإصدار أمر معقد ؛ يتضخم رأسي منه.