لماذا تستخدم مجموعات BCL التعدادات الهيكلية ، وليس الفصول؟
-
02-10-2019 - |
سؤال
نعلم جميعا الهياكل القابلة للتغيير شريرة على العموم. أنا متأكد أيضًا من ذلك لأن 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# لديه خطأ. إذا كنت في هذا الموقف نختار الإستراتيجية التي يجب اتباعها بشكل غير متسق. السلوك اليوم هو:
إذا كان المتغير المغطى بالقيمة يتم تحوره عبر الطريقة هو محلي طبيعي ، فسيتم تحوره بشكل طبيعي
ولكن إذا كان محليًا مرفوفًا (لأنه متغير مغلق لوظيفة مجهولة أو في كتلة التكرار) ثم المحلي هو تم إنشاؤه في الواقع كحقل للقراءة فقط ، والعتاد الذي يضمن حدوث طفرات على نسخة يتولى.
لسوء الحظ ، توفر المواصفات إرشادات قليلة في هذه المسألة. من الواضح أن هناك شيئًا ما مكسورًا لأننا نفعل ذلك بشكل غير متسق ، ولكن ماذا حقا الشيء الذي يجب فعله غير واضح على الإطلاق.
نصائح أخرى
يتم توضيح طرق الهيكل عندما يكون نوع الهيكل معروفًا في وقت الترجمة ، وتكون طريقة الاتصال عبر الواجهة بطيئة ، لذلك الإجابة هي: بسبب سبب الأداء.