c# - دعوة عضو الظاهري في منشئ




constructor warnings (12)

أحد الجوانب المهمة في هذا السؤال والتي لم تتناولها إجابات أخرى حتى الآن هو أنه من الآمن أن تدعو الطبقة الأساسية الأعضاء الظاهرية من داخل منشئها إذا كان هذا ما تتوقعه الفئات المشتقة . في مثل هذه الحالات ، يكون مصمم الفئة المشتقة مسؤولاً عن ضمان أن أي أساليب يتم تنفيذها قبل اكتمال البناء سوف تتصرف بطريقة معقولة بقدر الإمكان في ظل الظروف. على سبيل المثال ، في C ++ / CLI ، يتم التفاف المنشئات في التعليمات البرمجية التي سيتم استدعاء Dispose على كائن تم إنشاؤه جزئياً في حالة فشل البناء. الاتصال غالباً ما يكون Dispose في مثل هذه الحالات ضروريًا لمنع تسرب الموارد ، ولكن يجب إعداد طرق Dispose لإمكانية أن الكائن الذي يتم تشغيله قد لا يكون قد تم بناؤه بشكل كامل.

أتلقى تحذيرًا من ReSharper حول مكالمة إلى عضو افتراضي من مُنشئ الكائنات الخاصة بي.

لماذا يكون هذا شيء لا تفعل؟


أود فقط إضافة أسلوب Initialize () إلى الفئة الأساسية ثم استدعاء ذلك من المنشئات المشتقة. ستستدعي هذه الطريقة أي طرق / خواص افتراضية / مجردة بعد تنفيذ جميع الصانعين :)


تختلف قواعد C # بشكل كبير عن قواعد Java و C ++.

عندما تكون في المُنشئ لكائن ما في C # ، يوجد ذلك الكائن في نموذج مُهيأ بشكل كامل (ليس فقط "تم إنشاؤه") ، كنوع مشتق بالكامل.

namespace Demo
{
    class A 
    {
      public A()
      {
        System.Console.WriteLine("This is a {0},", this.GetType());
      }
    }

    class B : A
    {      
    }

    // . . .

    B b = new B(); // Output: "This is a Demo.B"
}

هذا يعني أنه إذا قمت بالاتصال بوظيفة ظاهرية من مُنشئ A ، فسيتم حلها إلى أي تجاوز في B ، إذا تم توفيرها.

حتى إذا قمت بإعداد A و B عمداً مثل هذا ، وفهم كامل لسلوك النظام ، فقد تكون في حالة صدمة لاحقًا. لنفترض أنك سمّيت الوظائف الافتراضية في منشئ B ، "مع العلم" أنها ستتعامل مع B أو A حسب الاقتضاء. ثم يمر الوقت ، ويقرر شخص آخر أنهم بحاجة إلى تحديد C ، وتجاوز بعض الوظائف الافتراضية هناك. وينتهي كل من مُنشئ B المفاجئ بالشفرة البرمجية في C ، مما قد يؤدي إلى سلوك مثير للدهشة.

ربما تكون فكرة جيدة لتجنب الوظائف الافتراضية في الصانعين على أي حال ، لأن القواعد مختلفة جدا بين C # و C ++ و Java. قد لا يعرف المبرمجون ما يمكن توقعه!


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

  class B
  {
    protected virtual void Foo() { }
  }

  class A : B
  {
    public A()
    {
      Foo(); // warning here
    }
  }

يمكنك ختم الفئة أ:

  sealed class A : B
  {
    public A()
    {
      Foo(); // no warning
    }
  }

أو يمكنك ختم طريقة Foo:

  class A : B
  {
    public A()
    {
      Foo(); // no warning
    }

    protected sealed override void Foo()
    {
      base.Foo();
    }
  }

عندما يتم إنشاء كائن مكتوب في C # ، ما يحدث هو أن المبدئين يعملان بالترتيب من الفئة الأكثر اشتقاقًا إلى الفئة الأساسية ، ومن ثم يتم تشغيل المنشئات بالترتيب من الفئة الأساسية إلى الفئة الأكثر اشتقاقًا ( راجع مدوّنة Eric Lippert للتفاصيل لماذا هذا هو ).

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

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

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


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

class Parent
{
    public Parent()
    {
        DoSomething();
    }
    protected virtual void DoSomething()
    {
    }
}

class Child : Parent
{
    private string foo = "HELLO";
    public Child() { /*Originally foo initialized here. Removed.*/ }
    protected override void DoSomething()
    {
        Console.WriteLine(foo.ToLower());
    }
}

لأنه حتى اكتمال التنفيذ منشئ ، لم يتم إنشاء مثيل الكائن بالكامل. قد لا تتم تهيئة أي أعضاء يشار إليهم بواسطة الوظيفة الظاهرية. في C ++ ، عندما تكون في مُنشئ ، this يشير فقط إلى النوع الثابت للمُنشئ الذي توجد به ، وليس النوع الديناميكي الفعلي للكائن الذي يتم إنشاؤه. هذا يعني أن استدعاء الدالة الظاهرية قد لا يذهب إلى حيث تتوقعه.


نعم ، من السوء عمومًا استدعاء الأسلوب الظاهري في المُنشئ.

عند هذه النقطة ، قد لا يتم إنشاء objet بالكامل بعد ، و قد لا يحدث الثبات المتوقع بواسطة أساليب.


هناك فرق بين C ++ و C # في هذه الحالة المحددة. في C ++ لم يتم تهيئة الكائن ولذلك فهو غير آمن استدعاء دالة virutal داخل مُنشئ. في C # عندما يتم إنشاء كائن فئة يتم تهيئة كافة أعضائها صفر. من الممكن استدعاء وظيفة ظاهرية في المُنشئ ، لكن إذا كان بإمكانك الوصول إلى الأعضاء الذين لا يزالون في صف. إذا لم تكن بحاجة إلى الوصول إلى الأعضاء ، فمن الآمن تماماً استدعاء وظيفة ظاهرية في C #.


واحدة مفقودة مهمة هي ، ما هي الطريقة الصحيحة لحل هذه المشكلة؟

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

يوضح التعليمة البرمجية التالية مأخوذ من إرشادات تصميم منشئ MSDN هذه المشكلة.

public class BadBaseClass
{
    protected string state;

    public BadBaseClass()
    {
        this.state = "BadBaseClass";
        this.DisplayState();
    }

    public virtual void DisplayState()
    {
    }
}

public class DerivedFromBad : BadBaseClass
{
    public DerivedFromBad()
    {
        this.state = "DerivedFromBad";
    }

    public override void DisplayState()
    {   
        Console.WriteLine(this.state);
    }
}

عند إنشاء مثيل جديد من DerivedFromBad ، يستدعي منشئ الفئة الأساسية إلى DisplayState ويظهر BadBaseClass لأن الحقل لم يتم تحديثه بعد بواسطة المُنشئ المشتق.

public class Tester
{
    public static void Main()
    {
        var bad = new DerivedFromBad();
    }
}

يزيل تطبيق محسّن الأسلوب الظاهري من مُنشئ الفئة الأساسية ويستخدم أسلوب Initialize . يؤدي إنشاء نسخة جديدة من DerivedFromBetter عرض "DerivedFromBetter" المتوقع

public class BetterBaseClass
{
    protected string state;

    public BetterBaseClass()
    {
        this.state = "BetterBaseClass";
        this.Initialize();
    }

    public void Initialize()
    {
        this.DisplayState();
    }

    public virtual void DisplayState()
    {
    }
}

public class DerivedFromBetter : BetterBaseClass
{
    public DerivedFromBetter()
    {
        this.state = "DerivedFromBetter";
    }

    public override void DisplayState()
    {
        Console.WriteLine(this.state);
    }
}

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

ومع ذلك ، إذا كان التصميم الخاص بك يفي بمبدأ Liskov Substitution ، فلن يتم إلحاق الضرر. ربما هذا هو السبب في تحمله - تحذير ، وليس خطأ.


للإجابة على سؤالك ، ضع في اعتبارك السؤال التالي: ما الذي سيطبعه التعليمة البرمجية التالية عند إنشاء كائن تابع؟

class Parent
{
    public Parent()
    {
        DoSomething();
    }

    protected virtual void DoSomething() 
    {
    }
}

class Child : Parent
{
    private string foo;

    public Child() 
    { 
        foo = "HELLO"; 
    }

    protected override void DoSomething()
    {
        Console.WriteLine(foo.ToLower());
    }
}

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





virtual-functions