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




constructor warnings resharper virtual-functions (15)

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

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


Answers

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


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


هناك إجابات مكتوبة جيدًا عن سبب عدم رغبتك في القيام بذلك. هنا مثال مضاد حيث قد ترغب في القيام بذلك (ترجمة إلى C # من التصميم العملي للكائن في Ruby بواسطة Sandi Metz ، صفحة 126).

لاحظ أن GetDependency() لا تلمس أي متغيرات حالة. سيكون ثابتًا إذا كانت الطرق الثابتة ظاهرية.

(لكي نكون منصفين ، ربما تكون هناك طرق أكثر ذكاءً للقيام بذلك من خلال حاويات حقن التبعية أو مُبدِلات الكائنات ...)

public class MyClass
{
    private IDependency _myDependency;

    public MyClass(IDependency someValue = null)
    {
        _myDependency = someValue ?? GetDependency();
    }

    // If this were static, it could not be overridden
    // as static methods cannot be virtual in C#.
    protected virtual IDependency GetDependency() 
    {
        return new SomeDependency();
    }
}

public class MySubClass : MyClass
{
    protected override IDependency GetDependency()
    {
        return new SomeOtherDependency();
    }
}

public interface IDependency  { }
public class SomeDependency : IDependency { }
public class SomeOtherDependency : IDependency { }

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

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

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

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


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

  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 # بشكل كبير عن قواعد 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. قد لا يعرف المبرمجون ما يمكن توقعه!


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

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


احذر من متابعة نصيحة Resharper وجعل الصف مختومًا! إذا كان نموذجًا في EF Code First ، فسيتم إزالة الكلمة الرئيسية الافتراضية وسيؤدي ذلك إلى تعطيل التحميل البطيء لعلاقاتها.

    public **virtual** User User{ get; set; }

شيء آخر مثير للاهتمام وجدته هو أن خطأ ReSharper يمكن أن يكون "راضيًا" عن طريق القيام بشيء مثل أدناه والذي هو البكم بالنسبة لي (ومع ذلك ، كما ذكر من قبل العديد من قبل ، فإنه لا يزال من غير المستحسن استدعاء دعم / طرق افتراضية في ctor.

public class ConfigManager
{

   public virtual int MyPropOne { get; private set; }
   public virtual string MyPropTwo { get; private set; }

   public ConfigManager()
   {
    Setup();
   }

   private void Setup()
   {
    MyPropOne = 1;
    MyPropTwo = "test";
   }

}


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


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

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 ++ / CLI ، يتم التفاف المنشئات في التعليمات البرمجية التي سيتم استدعاء Dispose على كائن تم إنشاؤه جزئياً في حالة فشل البناء. الاتصال غالباً ما يكون Dispose في مثل هذه الحالات ضروريًا لمنع تسرب الموارد ، ولكن يجب إعداد طرق Dispose لإمكانية أن الكائن الذي يتم تشغيله قد لا يكون قد تم بناؤه بشكل كامل.


في C # ، يتم تشغيل منشئ أساس من فئة 'مُنشئ قبل مُنشئ class المُنشأ ، لذلك لم تتم تهيئة أي من حقول المثيل التي قد تستخدمها فئة مشتقة في العضو الظاهري المحتمل ربما.

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


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

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

يوضح التعليمة البرمجية التالية مأخوذ من إرشادات تصميم منشئ 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);
    }
}

تحذير: لا يمكن تعديل معلومات العنوان - تم إرسال الرؤوس بالفعل

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

هذا هو E_WARNING ولن يتوقف البرنامج النصي.

مثال نموذجي سيكون ملف قالب مثل هذا:

<html>
    <?php session_start(); ?>
    <head><title>My Page</title>
</html>
...

ستحاول الدالة session_start() إرسال رؤوس مع ملف تعريف ارتباط جلسة العمل إلى العميل. ولكن PHP أرسلت بالفعل رؤوسًا عندما كتبت عنصر <html> إلى تدفق الإخراج. سيكون عليك نقل session_start() إلى الأعلى.

يمكنك حل هذه المشكلة عن طريق المرور بالخطوط قبل التعليمة البرمجية التي تعمل على التحذير والتحقق من مكان مخرجاتها. نقل أي رأس إرسال رمز قبل هذا الرمز.

غالبًا ما يتم إغفال إخراج خطوط جديدة بعد إغلاق PHP ?> يعتبر ممارسة قياسية ليتم حذفها ?> عندما يكون هذا هو آخر شيء في الملف. وبالمثل ، هناك سبب آخر شائع لهذا التحذير وهو عندما يكون <?php الافتتاحي يحتوي على مساحة فارغة ، أو سطر ، أو حرف غير مرئي قبله ، مما يؤدي إلى قيام خادم الويب بإرسال الرؤوس والمساحة البيضاء / السطر الجديد وبالتالي عند تعذر بدء PHP في التحليل. لتقديم أي رأس.

إذا كان ملفك يحتوي على أكثر من شفرة <?php ... ?> في ذلك ، فلا يجب أن يكون لديك أي مسافات بينهما. (ملاحظة: قد يكون لديك عدة كتل إذا كان لديك رمز تم إنشاؤه تلقائيًا)

تأكد أيضًا من عدم وجود أية علامات ترتيب بايت في التعليمة البرمجية الخاصة بك ، على سبيل المثال عندما يكون ترميز البرنامج النصي هو UTF-8 مع BOM.

أسئلة ذات صلة:







c# constructor warnings resharper virtual-functions