لماذا تستخدم مجموعات BCL التعدادات الهيكلية ، وليس الفصول؟

StackOverflow https://stackoverflow.com/questions/3168311

سؤال

نعلم جميعا الهياكل القابلة للتغيير شريرة على العموم. أنا متأكد أيضًا من ذلك لأن IEnumerable<T>.GetEnumerator() إرجاع النوع IEnumerator<T>, ، يتم تعبئة الهياكل على الفور في نوع مرجعي ، تكلف أكثر مما لو كانت مجرد أنواع مرجعية لتبدأ.

فلماذا ، في مجموعات BCL العامة ، هل جميع العدادات قابلة للتغيير؟ بالتأكيد كان هناك سبب وجيه. الشيء الوحيد الذي يحدث لي هو أنه يمكن نسخ الهياكل بسهولة ، وبالتالي الحفاظ على حالة التعداد في نقطة تعسفية. لكن إضافة أ Copy() طريقة ل IEnumerator كانت الواجهة أقل إزعاجًا ، لذلك لا أرى أن هذا مبرر منطقي من تلقاء نفسه.

حتى لو كنت لا أتفق مع قرار التصميم ، أود أن أكون قادرًا على فهم المنطق وراءه.

هل كانت مفيدة؟

المحلول

في الواقع ، هو لأسباب الأداء. قام فريق BCL كثير من الأبحاث حول هذه النقطة قبل أن تقرر الذهاب مع ما تسميه بحق كممارسة مشبوهة وخطيرة: استخدام نوع القيمة القابلة للتغيير.

أنت تسأل لماذا هذا لا يسبب الملاكمة. ذلك لأن برنامج التحويل البرمجي C# لا ينشئ رمزًا لربط الأشياء إلى ienumerable أو ienumerator في حلقة foreach إذا كان بإمكانه تجنب ذلك!

عندما نرى

foreach(X x in c)

أول شيء نفعله هو التحقق لمعرفة ما إذا كان C لديه طريقة تسمى GetEnumerator. إذا كان الأمر كذلك ، فإننا نتحقق لمعرفة ما إذا كان النوع الذي يعود لديه طريقة MovenExt و Property Current. إذا كان الأمر كذلك ، يتم إنشاء حلقة foreach بالكامل باستخدام المكالمات المباشرة لتلك الأساليب والخصائص. فقط إذا كان "النمط" لا يمكن مطابقته ، فهل نعود للبحث عن الواجهات.

هذا له تأثيران مرغوبان.

أولاً ، إذا كانت المجموعة ، على سبيل المثال ، مجموعة من ints ، ولكن تم كتابتها قبل اختراع الأنواع العامة ، فهي لا تأخذ عقوبة الملاكمة المتمثلة في ملاكمة قيمة التيار إلى الكائن ثم إلغاء ربطها إلى int. إذا كان التيار خاصية تُرجع int ، فنحن نستخدمها فقط.

ثانياً ، إذا كان العداد هو نوع قيمة ، فهو لا يربح العداد إلى ienumerator.

كما قلت ، قام فريق BCL بالكثير من الأبحاث حول هذا الأمر واكتشف أن الغالبية العظمى من الوقت ، عقوبة التخصيص والتعامل كان العداد كبيرًا بدرجة كافية بحيث يستحق أن يجعله نوعًا من القيمة ، على الرغم من أن القيام بذلك يمكن أن يسبب بعض الأخطاء المجنونة.

على سبيل المثال ، فكر في هذا:

struct MyHandle : IDisposable { ... }
...
using (MyHandle h = whatever)
{
    h = somethingElse;
}

كنت تتوقع بحق أن تفشل محاولة تحوير H ، وبالفعل. يكتشف المترجم أنك تحاول تغيير قيمة شيء له تصرف معلق ، وأن القيام بذلك قد يتسبب في عدم التخلص من الكائن الذي يجب التخلص منه فعليًا.

لنفترض الآن أن لديك:

struct MyHandle : IDisposable { ... }
...
using (MyHandle h = whatever)
{
    h.Mutate();
}

ماذا يحدث هنا؟ قد تتوقع بشكل معقول أن يفعل المترجم ما يفعله إذا كان H حقلًا قراءًا: قم بعمل نسخة ، وتحول النسخة من أجل التأكد من أن الطريقة لا تتخلص من الأشياء في القيمة التي يجب التخلص منها.

ومع ذلك ، فإن ذلك يتعارض مع حدسنا حول ما يجب أن يحدث هنا:

using (Enumerator enumtor = whatever)
{
    ...
    enumtor.MoveNext();
    ...
}

نتوقع أن يقوم movenext داخل كتلة باستخدام إرادة انقل العداد إلى واحد التالي بغض النظر عما إذا كان هيكلًا أو نوعًا مرجعًا.

لسوء الحظ ، فإن برنامج التحويل البرمجي C# لديه خطأ. إذا كنت في هذا الموقف نختار الإستراتيجية التي يجب اتباعها بشكل غير متسق. السلوك اليوم هو:

  • إذا كان المتغير المغطى بالقيمة يتم تحوره عبر الطريقة هو محلي طبيعي ، فسيتم تحوره بشكل طبيعي

  • ولكن إذا كان محليًا مرفوفًا (لأنه متغير مغلق لوظيفة مجهولة أو في كتلة التكرار) ثم المحلي هو تم إنشاؤه في الواقع كحقل للقراءة فقط ، والعتاد الذي يضمن حدوث طفرات على نسخة يتولى.

لسوء الحظ ، توفر المواصفات إرشادات قليلة في هذه المسألة. من الواضح أن هناك شيئًا ما مكسورًا لأننا نفعل ذلك بشكل غير متسق ، ولكن ماذا حقا الشيء الذي يجب فعله غير واضح على الإطلاق.

نصائح أخرى

يتم توضيح طرق الهيكل عندما يكون نوع الهيكل معروفًا في وقت الترجمة ، وتكون طريقة الاتصال عبر الواجهة بطيئة ، لذلك الإجابة هي: بسبب سبب الأداء.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top