واجهات C#. التنفيذ الضمني مقابل التطبيق الصريح




sealed classes c# (9)

ما الاختلافات في تنفيذ الواجهات بشكل ضمني وصريح في C #؟

متى يجب عليك استخدام الضمني ومتى يجب عليك استخدام صريح؟

هل هناك أي إيجابيات و / أو سلبيات لأحد أو لآخر؟

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

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

يمكن لأي شخص أن يتطرق إلى هذا الجانب أيضا؟


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

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

بخلاف ذلك ، يؤدي تنفيذ الواجهات بوضوح إلى عدم استخدام الإشارات إلى فئة التنفيذ الفعلية ، مما يسمح لك بتغيير ذلك التنفيذ في وقت لاحق. "أتأكد" ، على ما أظن ، هو "الميزة". يمكن للتنفيذ الجيد التنفيذ تحقيق ذلك دون تنفيذ واضح.

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

مثل الكثير من الأشياء ، فإن الميزة هي الحرمان (والعكس بالعكس). سيضمن تنفيذ الواجهات بشكل صريح عدم الكشف عن شفرة تنفيذ الفئة الملموسة.


بالإضافة إلى الإجابات الممتازة التي تم تقديمها بالفعل ، هناك بعض الحالات التي يتطلب فيها تنفيذًا صريحًا حتى يتمكن المترجم من معرفة ما هو مطلوب. إلقاء نظرة على IEnumerable<T> IE كمثال رئيسي من المرجح أن تأتي في كثير من الأحيان إلى حد ما.

إليك مثال على ذلك:

public abstract class StringList : IEnumerable<string>
{
    private string[] _list = new string[] {"foo", "bar", "baz"};

    // ...

    #region IEnumerable<string> Members
    public IEnumerator<string> GetEnumerator()
    {
        foreach (string s in _list)
        { yield return s; }
    }
    #endregion

    #region IEnumerable Members
    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
    #endregion
}

هنا ، تنفذ IEnumerable<string> IEnumerable ، وبالتالي نحن بحاجة أيضا. ولكن معلقة ، كلا الإصدارين العام والعادي على حد سواء تنفيذ وظائف بنفس توقيع الأسلوب (C # يتجاهل نوع الإرجاع لهذا). هذا قانوني تمامًا وغرامة. كيف يحل المجمع الذي يستخدم؟ إنه يجبرك على أن يكون لديك ، على الأكثر ، تعريفًا ضمنيًا واحدًا ، ثم يمكنه حل كل ما يحتاج إليه.

أي.

StringList sl = new StringList();

// uses the implicit definition.
IEnumerator<string> enumerableString = sl.GetEnumerator();
// same as above, only a little more explicit.
IEnumerator<string> enumerableString2 = ((IEnumerable<string>)sl).GetEnumerator();
// returns the same as above, but via the explicit definition
IEnumerator enumerableStuff = ((IEnumerable)sl).GetEnumerator();

ملاحظة: القطعة الصغيرة من indirection في التعريف الواضح لـ IEnumerable يعمل لأن داخل المحول يعرف المحول البرمجي أن النوع الفعلي للمتغير هو StringList ، وهذه هي الطريقة التي يحل بها استدعاء الدالة. حقيقة بسيطة قليلا لتطبيق بعض طبقات التجريد يبدو أن بعض واجهات NET الأساسية تراكمت.


يقوم كل عضو في الفئة التي تقوم بتطبيق واجهة بتصدير إعلان يشبه بطريقة معادلة الطريقة التي تتم بها كتابة إعلانات واجهة VB.NET ، على سبيل المثال

Public Overridable Function Foo() As Integer Implements IFoo.Foo

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

Protected Overridable Function IFoo_Foo() As Integer Implements IFoo.Foo

وفي هذه الحالة ، يُسمح للفصل ومشتقاته بالوصول إلى أحد أعضاء الصف باستخدام الاسم IFoo_Foo ، ولكن العالم الخارجي لن يتمكن من الوصول إلى ذلك العضو المعين إلا من خلال الإرسال إلى IFoo . يكون مثل هذا الأسلوب جيدًا في كثير من الأحيان في الحالات التي يكون فيها أسلوب الواجهة له سلوك محدد في جميع التطبيقات ، ولكن سلوكًا مفيدًا على بعض فقط [على سبيل المثال ، السلوك المحدد لمجموعة IList<T>.Add للقراءة فقط في الأسلوب IList<T>.Add NotSupportedException ]. لسوء الحظ ، فإن الطريقة الصحيحة الوحيدة لتنفيذ الواجهة في C # هي:

int IFoo.Foo() { return IFoo_Foo(); }
protected virtual int IFoo_Foo() { ... real code goes here ... }

ليس لطيفا


للإشارة إلى جيفري ريختر من CLR عبر C #
( EIMI يعني e xplicit I nterface M ethod I mplementation)

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

  • لا توجد أية وثائق تشرح كيفية تنفيذ أحد الأنواع بشكل خاص لطريقة EIMI ، ولا يوجد دعم Microsoft Visual Studio IntelliSense .
  • يتم كتابة مثيلات نوع القيمة عند الإرسال إلى واجهة.
  • لا يمكن استدعاء EIMI بواسطة نوع مشتق.

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

يمكن أيضًا استخدام EIMI لإخفاء أعضاء واجهة غير مطبوعة بشدة من تطبيقات واجهات Framework الأساسية مثل IEnumerable <T> حتى لا تعرض صفك بطريقة غير مطبوعة بشكل مباشر ، ولكنها صحيحة بشكل صحيح.


السبب رقم 1

أميل إلى استخدام واجهة تطبيق صريحة عندما أريد تثبيط "البرمجة إلى تطبيق" ( مبادئ التصميم من أنماط التصميم ).

على سبيل المثال ، في تطبيق ويب يستند إلى MVP :

public interface INavigator {
    void Redirect(string url);
}

public sealed class StandardNavigator : INavigator {
    void INavigator.Redirect(string url) {
        Response.Redirect(url);
    }
}

والآن ، من غير المرجح أن تعتمد فئة أخرى (مثل presenter ) على تطبيق StandardNavigator ومن الأرجح أن تعتمد على واجهة INAVigator (بما أن التنفيذ سيحتاج إلى إرساله إلى واجهة لاستخدام طريقة Redirect).

السبب رقم 2

سبب آخر قد أذهب مع تطبيق واجهة صريح هو الحفاظ على منظف واجهة "افتراضي" للطبقة. على سبيل المثال ، إذا كنت أقوم بتطوير عنصر تحكم خادم ASP.NET ، فقد أحتاج إلى واجهتين:

  1. الواجهة الأساسية للفصل ، والتي يستخدمها مطورو صفحات الويب ؛ و
  2. واجهة "مخفية" يستخدمها المقدم الذي أقوم بتطويره للتعامل مع منطق التحكم

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

public sealed class CustomerComboBox : ComboBox, ICustomerComboBox {
    private readonly CustomerComboBoxPresenter presenter;

    public CustomerComboBox() {
        presenter = new CustomerComboBoxPresenter(this);
    }

    protected override void OnLoad() {
        if (!Page.IsPostBack) presenter.HandleFirstLoad();
    }

    // Primary interface used by web page developers
    public Guid ClientId {
        get { return new Guid(SelectedItem.Value); }
        set { SelectedItem.Value = value.ToString(); }
    }

    // "Hidden" interface used by presenter
    IEnumerable<CustomerDto> ICustomerComboBox.DataSource { set; }
}

يقوم مقدم العرض بملء مصدر البيانات ، ولا يحتاج مطور صفحة الويب إلى إدراك وجوده.

لكنها ليست لعبة مدفع فضية

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


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

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

  1. من خلال العمل بشكل مباشر مع الواجهة ، فأنت لا تعترف بها ، وتربط رمزك بالتطبيق الأساسي.
  2. إذا كان لديك بالفعل ، على سبيل المثال ، خاصية عامة اسم في رمزك وتريد تنفيذ واجهة تحتوي على خاصية اسم ، فإن القيام بذلك بشكل صريح سيبقي الاثنين منفصلين. حتى لو كانوا يفعلون نفس الشيء فما زلت أقوم بتفويض الدعوة الصريحة إلى خاصية الاسم. أنت لا تعرف أبدًا ، قد ترغب في تغيير كيفية عمل الاسم للفئة العادية وكيفية عمل الاسم ، الواجهة في وقت لاحق.
  3. إذا قمت بتطبيق واجهة ضمنية ، فإن صفك الآن يكشف عن سلوكيات جديدة قد تكون ذات صلة بعميل الواجهة فقط ، وهذا يعني أنك لا تحافظ على دروسك كافية (رأيي).

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

/// <summary>
/// This is a Book
/// </summary>
interface IBook
{
    string Title { get; }
    string ISBN { get; }
}

/// <summary>
/// This is a Person
/// </summary>
interface IPerson
{
    string Title { get; }
    string Forename { get; }
    string Surname { get; }
}

/// <summary>
/// This is some freaky book-person.
/// </summary>
class Class1 : IBook, IPerson
{
    /// <summary>
    /// This method is shared by both Book and Person
    /// </summary>
    public string Title
    {
        get
        {
            string personTitle = "Mr";
            string bookTitle = "The Hitchhikers Guide to the Galaxy";

            // What do we do here?
            return null;
        }
    }

    #region IPerson Members

    public string Forename
    {
        get { return "Lee"; }
    }

    public string Surname
    {
        get { return "Oades"; }
    }

    #endregion

    #region IBook Members

    public string ISBN
    {
        get { return "1-904048-46-3"; }
    }

    #endregion
}

يجمع هذا الرمز ويدير "موافق" ، ولكن تتم مشاركة خاصية "العنوان".

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

string IBook.Title
{
    get
    {
        return "The Hitchhikers Guide to the Galaxy";
    }
}

string IPerson.Title
{
    get
    {
        return "Mr";
    }
}

public string Title
{
    get { return "Still shared"; }
}

لاحظ أنه يتم الاستدلال على تعريفات الواجهة الصريحة لتكون عامة - وبالتالي لا يمكنك أن تعلن أنها عامة (أو غير ذلك) بشكل صريح.

لاحظ أيضًا أنه لا يزال بإمكانك الحصول على إصدار "مشترك" (كما هو موضح أعلاه) ، ولكن في حين أن هذا ممكن ، فإن وجود مثل هذه الخاصية أمر مشكوك فيه. ربما يمكن استخدامه كتنفيذ افتراضي للعنوان - بحيث لا يلزم تعديل الكود الحالي لإدخال Class1 إلى IBook أو IPerson.

إذا لم تقم بتعريف العنوان "المشترك" (الضمني) ، فيجب على مستهلكي Class1 أن يعرضوا أولاً أمثلة من Class1 إلى IBook أو IPerson أولاً - وإلا لن يتم ترجمة التعليمات البرمجية.


أحد الاستخدامات المهمة لتنفيذ الواجهة الصريحة هو عند الحاجة إلى تنفيذ واجهات مع رؤية مختلطة .

يتم شرح المشكلة والحل بشكل جيد في المقالة C # Interface Interface .

على سبيل المثال ، إذا كنت تريد حماية تسرب الكائنات بين طبقات التطبيق ، تسمح لك هذه التقنية بتحديد رؤية مختلفة للأعضاء التي قد تسبب التسرب.


أستخدم واجهة واضحة في معظم الأوقات. وهنا الاسباب الرئيسية.

إعادة بيع ديون أكثر أمنا

عند تغيير الواجهة ، من الأفضل أن يتمكن المترجم من التحقق من ذلك. هذا هو أصعب مع تطبيقات ضمنية.

هناك حالتان شائعتان يتبادران إلى الذهن:

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

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

من المؤسف أن C # لا تحتوي على كلمة رئيسية تجبرنا على وضع علامة على طريقة كتطبيق ضمني ، لذلك يمكن للمجمّع إجراء الفحوص الإضافية. لا تحتوي الطرق الافتراضية على أي من المشاكل المذكورة أعلاه بسبب الاستخدام المطلوب لـ "تجاوز" و "جديد".

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

انها توثق ذاتي

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

حيث: "bool ITask.Execute ()" واضح ولا لبس فيه.

فصل واضح لتطبيق الواجهة

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

عندما أتطرق لرمز الصف ، عندما أواجه تطبيقات الواجهات الصريحة ، يتحول عقلي إلى وضع "تعاقد الكود". غالبًا ما تتقدم هذه التطبيقات ببساطة إلى طرق أخرى ، ولكن في بعض الأحيان ستقوم بفحص إضافي للمدارس / الحالة ، وتحويل المعلمات الواردة لتتوافق بشكل أفضل مع المتطلبات الداخلية ، أو حتى الترجمة لأغراض الإصدار (أي أجيال متعددة من الواجهات ، كل ذلك يؤدي إلى التنفيذ المشترك).

(أنا أدرك أن الجمهور هم أيضا من العقود المبرمة ، لكن الواجهات أقوى بكثير ، خاصة في قاعدة البرمجة التي تعتمد على الواجهة ، حيث يكون الاستخدام المباشر لأنواع الخرسانة عادة علامة على وجود كود داخلي فقط).

ذات الصلة: السبب 2 أعلاه من قبل جون .

وما إلى ذلك وهلم جرا

بالإضافة إلى المزايا المذكورة بالفعل في إجابات أخرى هنا:

  • عند اللزوم ، حسب disambiguation أو الحاجة إلى واجهة داخلية
  • لا يشجع "البرمجة على التنفيذ" ( السبب 1 بواسطة جون )

مشاكل

ليس كل المرح والسعادة. هناك بعض الحالات التي ألتصق فيها بمفارقات:

  • أنواع القيم ، لأن ذلك يتطلب الملاكمة وانخفاض الرفع. هذه ليست قاعدة صارمة ، وتعتمد على الواجهة وكيفية استخدامها. IComparable؟ ضمني. IFormattable؟ ربما صريح.
  • واجهات نظام تافهة لها طرق يتم تسميتها بشكل متكرر مباشرة (مثل IDisposable.Dispose).

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

  1. إضافة الجماهير وجعل طرق الواجهة إلى الأمام لهم للتنفيذ. يحدث عادة مع واجهات أبسط عند العمل داخليا.
  2. (طريقي المفضل) إضافة public IMyInterface I { get { return this; } } public IMyInterface I { get { return this; } } (التي يجب أن يتم تضمينها) واستدعاء foo.I.InterfaceMethod() . إذا كانت هناك واجهات متعددة تحتاج إلى هذه الإمكانية ، فقم بتوسيع الاسم إلى ما بعدها (في تجربتي ، من النادر أن أكون بحاجة إلى هذه الحاجة).




interface