سؤال

يبدو أنني أتذكر قراءة شيء ما حول مدى سوء تنفيذ البنيات للواجهات في CLR عبر C#، لكن يبدو أنني لا أستطيع العثور على أي شيء حيال ذلك.هل هذا سيء؟هل هناك عواقب غير مقصودة للقيام بذلك؟

public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }
هل كانت مفيدة؟

المحلول

هناك عدة أمور تحدث في هذا السؤال..

من الممكن أن تقوم البنية بتنفيذ واجهة، ولكن هناك مخاوف تتعلق بالإرسال والتغيير والأداء.انظر في هذا المنصب لمزيد من التفاصيل: http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

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

نصائح أخرى

نظرًا لعدم تقديم أي شخص آخر هذه الإجابة بشكل صريح فسأضيف ما يلي:

تنفيذ الواجهة الموجودة على البنية ليس لها أي عواقب سلبية على الإطلاق.

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

  • استخدام الكائن الناتج لأغراض القفل (فكرة سيئة للغاية بأي شكل من الأشكال)
  • استخدام دلالات المساواة المرجعية وتوقع أن تعمل مع قيمتين محاصرتين من نفس البنية.

كلا الأمرين غير مرجح، بدلاً من ذلك من المحتمل أن تقوم بأحد الإجراءات التالية:

الأدوية العامة

ربما تكون الأسباب المعقولة للبنيات التي تنفذ الواجهات هي أنه يمكن استخدامها داخل ملف نوعي السياق مع قيود.عند استخدامه بهذه الطريقة يكون المتغير كالتالي:

class Foo<T> : IEquatable<Foo<T>> where T : IEquatable<T>
{
    private readonly T a;

    public bool Equals(Foo<T> other)
    {
         return this.a.Equals(other.a);
    }
}
  1. تمكين استخدام البنية كمعلمة نوع
    • طالما لا يوجد عائق آخر مثل new() أو class يستخدم.
  2. السماح بتجنب الملاكمة على الهياكل المستخدمة بهذه الطريقة.

إذن this.a ليس مرجعًا للواجهة وبالتالي فهو لا يتسبب في وضع مربع لأي شيء فيه.علاوة على ذلك، عندما يقوم مترجم c# بتجميع الفئات العامة ويحتاج إلى إدراج استدعاءات لطرق المثيل المحددة في مثيلات معلمة النوع T، فيمكنه استخدام مقيدة رمز التشغيل:

إذا كان thisType هو نوع قيمة وكان thisType يطبق الطريقة، فسيتم تمرير ptr بدون تعديل كمؤشر "هذا" لتعليمات طريقة الاستدعاء، لتنفيذ الطريقة بواسطة thisType.

يؤدي هذا إلى تجنب الملاكمة وبما أن نوع القيمة هو الذي يقوم بتنفيذ الواجهة يجب نفذ الطريقة وبالتالي لن تحدث ملاكمة.في المثال أعلاه Equals() يتم الاستدعاء بدون وجود مربع على هذا1.

واجهات برمجة التطبيقات منخفضة الاحتكاك

يجب أن تحتوي معظم البنيات على دلالات بدائية حيث تعتبر القيم المتطابقة في اتجاه البت متساوية2.سيوفر وقت التشغيل مثل هذا السلوك ضمنيًا Equals() ولكن هذا يمكن أن يكون بطيئا.أيضا هذه المساواة الضمنية هي لا يتعرض باعتباره تنفيذا ل IEquatable<T> وبالتالي يمنع استخدام البنيات بسهولة كمفاتيح للقواميس ما لم تنفذها بشكل صريح بنفسها.لذلك من الشائع أن تعلن العديد من أنواع البنية العامة أنها تنفذها IEquatable<T> (أين T هل هم أنفسهم) لجعل هذا الأمر أسهل وأفضل أداءً بالإضافة إلى توافقه مع سلوك العديد من أنواع القيم الموجودة داخل CLR BCL.

يتم تنفيذ جميع البدائيات في BCL كحد أدنى:

  • IComparable
  • IConvertible
  • IComparable<T>
  • IEquatable<T> (وبالتالي IEquatable)

العديد أيضا تنفيذ IFormattable, بالإضافة إلى ذلك، تقوم العديد من أنواع القيم المحددة في النظام مثل DateTime وTimeSpan وGuid بتنفيذ العديد من هذه الأنواع أو جميعها أيضًا.إذا كنت تنفذ نوعًا مشابهًا "مفيدًا على نطاق واسع" مثل بنية أرقام معقدة أو بعض القيم النصية ذات العرض الثابت، فإن تنفيذ العديد من هذه الواجهات الشائعة (بشكل صحيح) سيجعل بنيتك أكثر فائدة وقابلة للاستخدام.

الاستثناءات

من الواضح إذا كانت الواجهة تعني بقوة التحولية (مثل ICollection) فإن تنفيذها يعد فكرة سيئة لأنه يعني أنك إما جعلت البنية قابلة للتغيير (مما يؤدي إلى أنواع الأخطاء الموضحة بالفعل حيث تحدث التعديلات على القيمة المعبأة بدلاً من القيمة الأصلية) أو أنك تربك المستخدمين بتجاهل الآثار المترتبة على الأساليب مثل Add() أو طرح الاستثناءات.

لا تتضمن العديد من الواجهات إمكانية التغيير (مثل IFormattable) وتكون بمثابة الطريقة الاصطلاحية لعرض وظائف معينة بطريقة متسقة.في كثير من الأحيان، لن يهتم مستخدم البنية بأي حمل ملاكمة لمثل هذا السلوك.

ملخص

عند القيام بذلك بشكل معقول، على أنواع القيمة غير القابلة للتغيير، يعد تنفيذ واجهات مفيدة فكرة جيدة


ملحوظات:

1:لاحظ أن المترجم قد يستخدم هذا عند استدعاء الأساليب الافتراضية على المتغيرات معروف أن تكون من نوع بنية محدد ولكن يلزم فيها استدعاء طريقة افتراضية.على سبيل المثال:

List<int> l = new List<int>();
foreach(var x in l)
    ;//no-op

العداد الذي يتم إرجاعه بواسطة القائمة هو عبارة عن بنية، وتحسين لتجنب التخصيص عند تعداد القائمة (مع بعض الأشياء المثيرة للاهتمام عواقب).ومع ذلك، تحدد دلالات foreach أنه إذا قام العداد بتنفيذ ذلك IDisposable ثم Dispose() سيتم استدعاؤه بمجرد اكتمال التكرار.من الواضح أن حدوث ذلك من خلال مكالمة معلبة من شأنه أن يلغي أي فائدة من كون العداد عبارة عن بنية (في الواقع سيكون الأمر أسوأ).والأسوأ من ذلك، إذا كان استدعاء التخلص يعدل حالة العداد بطريقة ما، فسيحدث هذا في المثيل المعبأ وقد يتم تقديم العديد من الأخطاء الدقيقة في الحالات المعقدة.وبالتالي فإن IL المنبعث في هذا النوع من المواقف هو:

IL_0001:  newobj      System.Collections.Generic.List..ctor
IL_0006:  stloc.0     
IL_0007:  nop         
IL_0008:  ldloc.0     
IL_0009:  callvirt    System.Collections.Generic.List.GetEnumerator
IL_000E:  stloc.2     
IL_000F:  br.s        IL_0019
IL_0011:  ldloca.s    02 
IL_0013:  call        System.Collections.Generic.List.get_Current
IL_0018:  stloc.1     
IL_0019:  ldloca.s    02 
IL_001B:  call        System.Collections.Generic.List.MoveNext
IL_0020:  stloc.3     
IL_0021:  ldloc.3     
IL_0022:  brtrue.s    IL_0011
IL_0024:  leave.s     IL_0035
IL_0026:  ldloca.s    02 
IL_0028:  constrained. System.Collections.Generic.List.Enumerator
IL_002E:  callvirt    System.IDisposable.Dispose
IL_0033:  nop         
IL_0034:  endfinally  

وبالتالي فإن تنفيذ IDisposable لا يسبب أي مشكلات في الأداء ويتم الحفاظ على الجانب القابل للتغيير (المؤسف) للعداد في حالة قيام طريقة التخلص بأي شيء بالفعل!

2:يعتبر double وfloat استثناءات لهذه القاعدة حيث لا تعتبر قيم NaN متساوية.

في بعض الحالات، قد يكون من الجيد للبنية تنفيذ واجهة (إذا لم تكن مفيدة على الإطلاق، فمن المشكوك فيه أن منشئي .net سيوفرونها).إذا كان الهيكل ينفذ واجهة للقراءة فقط مثل IEquatable<T>, وتخزين البنية في موقع تخزين (متغير، معلمة، عنصر صفيف، إلخ) من النوع IEquatable<T> سيتطلب أن يكون محاصرًا (يحدد كل نوع بنية في الواقع نوعين من الأشياء:نوع موقع تخزين يعمل كنوع قيمة ونوع كائن كومة يعمل كنوع فئة؛الأول قابل للتحويل ضمنيًا إلى الثاني - "الملاكمة" - والثاني يمكن تحويله إلى الأول عبر طاقم تمثيل صريح - "فتح العلبة").من الممكن استغلال تنفيذ البنية للواجهة بدون الملاكمة، ولكن باستخدام ما يسمى بالأدوية العامة المقيدة.

على سبيل المثال، إذا كان لدى المرء طريقة CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>, ، يمكن استدعاء مثل هذه الطريقة thing1.Compare(thing2) دون الحاجة إلى المربع thing1 أو thing2.لو thing1 يحدث أن يكون، على سبيل المثال، Int32, ، سيعرف وقت التشغيل ذلك عندما يقوم بإنشاء الكود الخاص بـ CompareTwoThings<Int32>(Int32 thing1, Int32 thing2).نظرًا لأنه سيعرف النوع الدقيق لكل من الشيء الذي يستضيف الطريقة والشيء الذي يتم تمريره كمعلمة، فلن يحتاج إلى وضع أي منهما في مربع.

أكبر مشكلة في البنيات التي تنفذ الواجهات هي أن البنية التي يتم تخزينها في موقع من نوع الواجهة، Object, ، أو ValueType (على عكس الموقع من نوعه الخاص) سوف يتصرف ككائن فئة.بالنسبة لواجهات القراءة فقط، لا يمثل هذا مشكلة بشكل عام، ولكن بالنسبة للواجهات المتغيرة مثل IEnumerator<T> يمكن أن تسفر عن بعض الدلالات الغريبة.

خذ بعين الاعتبار، على سبيل المثال، الكود التالي:

List<String> myList = [list containing a bunch of strings]
var enumerator1 = myList.GetEnumerator();  // Struct of type List<String>.IEnumerator
enumerator1.MoveNext(); // 1
var enumerator2 = enumerator1;
enumerator2.MoveNext(); // 2
IEnumerator<string> enumerator3 = enumerator2;
enumerator3.MoveNext(); // 3
IEnumerator<string> enumerator4 = enumerator3;
enumerator4.MoveNext(); // 4

سيتم وضع علامة على البيان رقم 1 enumerator1 لقراءة العنصر الأول.سيتم نسخ حالة هذا العداد إلى enumerator2.البيان المحدد رقم 2 سيقدم تلك النسخة لقراءة العنصر الثاني، لكنه لن يؤثر enumerator1.سيتم بعد ذلك نسخ حالة هذا العداد الثاني enumerator3, ، والتي سيتم تقديمها بواسطة البيان رقم 3.ثم لأنه enumerator3 و enumerator4 كلا النوعين المرجعيين، أ مرجع ل enumerator3 سيتم بعد ذلك نسخها إلى enumerator4, ، سوف يتقدم البيان المميز بشكل فعال كلاهما enumerator3 و enumerator4.

يحاول بعض الأشخاص التظاهر بأن أنواع القيم وأنواع المراجع كلاهما نوعان Object, ، ولكن هذا ليس صحيحا حقا.أنواع القيمة الحقيقية قابلة للتحويل إلى Object, ، ولكنها ليست أمثلة عليه.مثال على List<String>.Enumerator الذي يتم تخزينه في موقع من هذا النوع هو نوع قيمة ويتصرف كنوع قيمة؛نسخه إلى موقع النوع IEnumerator<String> سيتم تحويله إلى نوع مرجعي، و سوف يتصرف كنوع مرجعي.هذا الأخير هو نوع من Object, ، ولكن السابق ليس كذلك.

راجع للشغل، بضع ملاحظات أخرى:(1) بشكل عام، يجب أن يكون لأنواع الفئات القابلة للتغيير خصائصها الخاصة Equals تختبر الطرق المساواة المرجعية، ولكن لا توجد طريقة مناسبة للبنية المعبأة للقيام بذلك؛(2) على الرغم من اسمها، ValueType هو نوع فئة، وليس نوع القيمة؛جميع الأنواع مشتقة من System.Enum هي أنواع القيمة، كما هو الحال مع جميع الأنواع التي تشتق منها ValueType فيما عدا System.Enum, ، ولكن الاثنين ValueType و System.Enum هي أنواع الطبقة.

يتم تنفيذ الهياكل كأنواع قيمة والفئات هي أنواع مرجعية.إذا كان لديك متغير من النوع Foo، وقمت بتخزين مثيل Fubar فيه، فسيتم "وضعه في صندوق" في نوع مرجعي، وبالتالي التغلب على ميزة استخدام البنية في المقام الأول.

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

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

لكن الحصول على مرجع واجهة للبنية سوف BOX هو - هي.لذلك عقوبة الأداء وما إلى ذلك.

السيناريو الوحيد الصحيح الذي يمكنني التفكير فيه الآن هو يتضح في رسالتي هنا.عندما تريد تعديل حالة البنية المخزنة في مجموعة، يجب عليك القيام بذلك عبر واجهة إضافية مكشوفة في البنية.

أعتقد أن المشكلة تكمن في أنها تسبب الملاكمة لأن البنيات هي أنواع قيمة لذا هناك عقوبة طفيفة على الأداء.

يشير هذا الرابط إلى احتمال وجود مشكلات أخرى به...

http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

لا توجد عواقب على البنية التي تنفذ الواجهة.على سبيل المثال، تقوم بنيات النظام المضمنة بتنفيذ واجهات مثل IComparable و IFormattable.

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

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

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

الهياكل تشبه تمامًا الفئات التي تعيش في المكدس.لا أرى أي سبب يجعلهم "غير آمنين".

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