9 قواعد عن التصنيفات Classes لا غنى عن معرفتها

This article is available in English too, check it out here.

نظرة خاطفة

درسنا في هذا اليوم يتكلم عن 9 قواعد هامة جدا بالنسبة للتصنيفات Classes في لغات C#، C++/CLI، و ISO/ANSI C++.

تحديدا الدرس اليوم يتكلم عن القواعد المنطبقة على الـ Constructors، Destructors، و Finalizers خاصة في حالة التدرج Hierarchy للتصنيفات، بمعنى أن يكون هناك أب وهناك أبناء. وسنرمز إلى كل نوع من هذه الدوال باسم وهو دوال الإنشاء Constructors، دوال الإزالة Destructors، ودوال الإنهاء Finalizers.

حتى إن لم تكون متقنا للغة أخرى غير الـ C# أو العكس فيمكنك قراءة هذا الدرس ومتابعة ما يخص اللغة التي تعرفها فقط. أيضا لا يشترط الإتقان لأي من اللغات، بل فقط معرفة البدايات للـ Classes والدوال الخاصة مثل Constructors، Destructors، و Finalizers.

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

والقواعد التي سنتعرض لها هي 9 قواعد وهي كالتالي:

  • القاعدة 1: يتم النداء على دوال الإنشاء Constructors بشكل تنازلي
  • القاعدة 2: في معجم الـ C#، اللفظان Destructor و Finalizer متردفان
  • القاعدة 3: يتم النداء على دوال الإزالة Destructors بشكل تصاعدي
  • القاعدة 4: دوال الإنهاء Finalizers هي ميزة من مميزات GC في بيئة الدوت نت
  • القاعدة 5: لا يمكنك تقرير متى سيتم النداء على دوال الإنهاء Finalizers
  • القاعدة 6: في C++/CLI هناك فرق بين دوال الإزالة Destructors والإنهاء Finalizers
  • القاعدة 7: في Classic C++ و C++/CLI يمكنك تحديد متى يتم النداء على دوال الإزالة Destructors
  • القاعدة 8: في C++/CLI لا يتم النداء على دوال الإزالة Destructors والإنهاء Finalizers معا
  • القاعدة 9: يجب الحذر من الدوال الظاهرية Virtual Functions في دوال الإنشاء Constructors

القاعدة 1: يتم النداء على دوال الإنشاء بشكل تنازلي

عندما يكون هناك تدرج في التصنيفات أي أن يكون هناك أب وابن فإن دوال الإنشاء Constructors يتم النداء عليها بشكل تنازلي من أبعد أب وحتى آخر ابن المطلوب إنشاء نسخة منه. تنطبق على C#، C++/CLI، و ANSI C++.

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

    class BaseClass
    {
        public BaseClass()
        {
            Console.WriteLine("ctor of BaseClass");
        }
    }
    class DerivedClass : BaseClass
    {
        public DerivedClass()
        {
            Console.WriteLine("ctor of DerivedClass");
        }
    }
    class ChildClass : DerivedClass
    {
        public ChildClass()
        {
            Console.WriteLine("ctor of ChildClass");
        }
    }

هنا قمنا بإنشاء تصنيف أب وهو BaseClass وهو بالطبع ينحدر بشكل ضمني من التصنيف System.Object. ثم قمنا بعد ذلك بإنشاء تفرع من هذا الأب وهو DerivedClass. وأخيرا، أنشأنا تفرع من التفرع الأول وهو ChildClass. ليصبح لدينا أب وابن وابن آخر لهذا الابن (:D). وقمنا أيضا بإضافة دوال الإنشاء Constructors في الثلاثة تصنيفات.

والآن قم بتشغيل الكود.

    static void Main()
    {
        ChildClass cls = new ChildClass();
    }

فتكون النتيجة هي التالية:

ctor of BaseClass
ctor of DerivedClass
ctor of ChildClass

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

القاعدة 2: في معجم الـ C#، اللفظان Destructor و Finalizer متردفان

في الـ C# يعتبر اللفظان دالة الإزالة Destructor ودالة الإنهاء Finalizer مترادفان يرمزان لنفس الدالة وهي هذه التي يتم النداء عليها قبل إزالة هذا العنصر من الذاكرة تماما. ينطبق على C# فقط.

باستخدام نفس التدرج نقوم بتغيير في الدوال لتصبح كالتالي:

    class BaseClass
    {
        public ~BaseClass()
        {
            Console.WriteLine("dtor of BaseClass");
        }
    }
    class DerivedClass : BaseClass
    {
        public ~DerivedClass()
        {
            Console.WriteLine("dtor of DerivedClass");
        }
    }
    class ChildClass : DerivedClass
    {
        public ~ChildClass()
        {
            Console.WriteLine("dtor of ChildClass");
        }
    }

عندما تقوم بتعريف الدوال بهذا الشكل (عدم تحديد فيمة مرتجعة Return Value وإضافة العلامة ~ إلى اسم الدالة) فإننا نقوم بإنشاء دالة من نوع خاص وهي دالة الإزالة Destructor. وهذه الدالة يتم نداؤها مباشرة قبل إزالة هذا العنصر من الذاكرة. وبالتالي يمكنك إضافة أي كود مسؤول عن تحرير الذاكرة أو إنهاء أي عمليات موجودة مثل ملفات مفتوحة أو قواعد بيانات ونحوها.

هذا والجدير بالذكر أن المترجم Compiler يقوم بتحويل هذه الدالة إلى دالة أخرى باسم آخر وهي دالة Finalize() وهي عبارة عن تبديل Override للدالة الظاهرية Virtual الموجودة في System.Object.Finalize(). فلو تكمننا من عملية فك الكود ومشاهدته بعد عملية الترجمة Compiling لوجدنا أن دالتنا تحولت للدالة التالية:

        protected virtual void Finalize()
        {
            try
            {
                Console.WriteLine("dtor of ChildClass");
            }
            finally
            {
                base.Finalize();
            }
        }

إذا فلفظي Destructor و Finalizer في الـ C# يرمزان لنفس الشيئ وهي هذه الدالة التي يتم النداء عليها قبل إزالة العنصر من الذاكرة مباشرة. أما عن طريقة النداء عليها (تصاعدية/تنازلية) ففي القاعدة القادمة.

القاعدة 3: يتم النداء على دوال الإزالة Destructors بشكل تصاعدي

دوال الإزالة Destructors يتم النداء عليها تصاعديا. بمعنى أنها تبدأ من العنصر نفسه وتستمر بالاتجاه الأعلى ناحية الأب ثم أب الأب وهكذا حتى تصل إلى آخر تصنيف من هذا التدرج. تنطبق على C#، C++/CLI، و ANSI C++.

باستخدام آخر كود كتبناه قم بتجربته.

    static void Main()
    {
        ChildClass cls = new ChildClass();
        // 'cls' is removed from memory here
    }

فتكون هذه النتائج:

dtor of ChildClass
dtor of DerivedClass
dtor of BaseClass

نلاحظ أنه تم النداء على الدالة بشكل تصاعدي من أول آخر فرع وحتى أبعد أب.

القاعدة 4: دوال الإنهاء Finalizers هي ميزة من مميزات GC في بيئة الدوت نت

دوال الإنهاء Finalizers هي ميزة من مميزات الـ Garbage Collector أو GC وهي أحد خدمات الدوت نت المسؤولة عن إدارة موارد البرنامج من الذاكرة. ولهذا فإن الـ Structures لا يمكنك فيها إنشاء Finalizers أو حتى Constructors (كما قلنا هما لفظان مترادفان.)

فمثلا جرب إضافة الـ Destructor إلى أحد التركيبات Structures الخاصة بك في C#، مثلا كالتالي:

    struct MyStruct
    {
        ~MyStruct()
        {
            Console.WriteLine("dtor of MyStruct");
        }
    }

لن تستطيع عمل ترجمة Compile للكود بسبب أن هذه الجملة خطأ فالتركيبات Structures لا تخضع للـ GC فقط الـ Structures. فكما تعلم يقوم الـ GC بإدارة العناصر المرجعية Reference Types بينما الـ Structures هي عناصر قيمة Value Types.

إذا فالنتيجة أن الـ Finalizers خاصة بـ .NET Classes فقط حتى ليس الـ Structures. فلا ينطبق هذا على ANSI C++.

القاعدة 5: لا يمكنك تقرير متى سيتم النداء على دوال الإنهاء Finalizers

لأنك لا تعرف متى تقوم GC بإزالة العنصر بالكامل من الذاكرة، فلا يمكنك تقرير متى سيتم النداء على دالة Finalize(). تنطبق على C# و C++/CLI.

حتى لو قمت باستخدام دوال مثل System.GC.Collect() فأنت لا تعرف بالضبط متى يتم إزالة العنصر والنداء على هذه الدالة وهذا بسبب أن الـ GC دائما ما يقوم بتأخير إزالة هذه العناصر التي تحوي الدالة Finalize() إلى وقت غير معروف بالنسبة للمبرمج.

القاعدة 6: في C++/CLI هناك فرق بين دوال الإزالة Destructors والإنهاء Finalizers

على عكس لغات الدوت نت الأخرى، ففي C++/CLI هناك فرق بين دوال الإزالة Destructors ودوال الإنهاء Finalizers. فالأخيرة Finalizers كما تعلم هي الدالة التي تقوم GC بالنداء عليها متى يتم إزالة العنصر من الذاكرة. أما إذا قمت بإزالة العنصر يدويا (وهذه ميزة من مميزات C++/CLI) فسوف يتم النداء على دالة الإزالة Destructors وليست دالة الإنهاء Finalizers.

دعنا نتابع على نفس المثال ونفس السياق الخاص بنا ولكن بلغة أخرى وهي C++/CLI. لاحظ الكود التالي:

ref class BaseClass
{
public:
	BaseClass()
	{
		Console::WriteLine("ctor of BaseClass");
	}
	~BaseClass()
	{
		Console::WriteLine("dtor of BaseClass");
		GC::ReRegisterForFinalize(this);
	}
};
ref class DerivedClass : BaseClass
{
public:
	DerivedClass()
	{
		Console::WriteLine("ctor of DerivedClass");
	}
	~DerivedClass()
	{
		Console::WriteLine("dtor of DerivedClass");
		GC::ReRegisterForFinalize(this);
	}
};
ref class ChildClass : DerivedClass
{
public:
	ChildClass()
	{
		Console::WriteLine("ctor of ChildClass");
	}
	~ChildClass()
	{
		Console::WriteLine("dtor of ChildClass");
		GC::ReRegisterForFinalize(this);
	}
};

فعند محاولتك تشغيل الكود بالطريقة المعتادة:

int main()
{
	ChildClass^ cls = gcnew ChildClass();
}

تكون النتائج مماثلة للتالي:

ctor of BaseClass
ctor of DerivedClass
ctor of ChildClass

بالطريقة المعروفة تم النداء على الـ Constructors بشكل تنازلي. ولكن أين المشكلة هنا؟ المشكلة هنا أن كود الـ Destructor بعكس الـ C# لم يتم نداؤه!

والسبب هنا، أن لغة C++/CLI لها وضع خاص بعكس بقية لغات الدوت نت. فهذه اللغة تمكنك من وضع تعريفان مختلفان للـ Destructors والـ Finalizers وهنا ليس اللفظان مترادفان كما في C# أو بقية اللغات الأخرى.

والفرق، هو أن الـ Destructors يتم النداء عليها عند تنفيذ أمر الإزالة delete على هذا العنصر. أما الـ Finalizer فإنها يتم النداء عليها تلقائيا بواسطة الـ GC عند إزالة العنصر من الذاكرة. ولأننا في الكود السابق تركنا العنصر للـ GC بينما لم نعط تعريفا للـ Finalizer فلم نجد سوى نتائج دوال الإنشاء Constructors فقط.

الآن، فلنضف أمر الإزالة delete إلى الكود ونلاحظ ماذا يحدث:

int main()
{
	ChildClass^ cls = gcnew ChildClass();
	delete cls;
}

الآن شغل الكود، تجد أن النتائج من الـ Destructors تمت طباعتها.

ثم إلى النقطة الأكثر إثارة، سوف نقوم بتعريف الـ Constructors والـ Finalizers في التصنيفات (لاحظ أن هذا ممنوع في جميع لغات الدوت نت الأخرى. لاحظ أيضا أن الـ ANSI C++ لا تدعم الـ Finalizers أبدا كما قلنا في القاعدة الرابعة.)

ref class BaseClass
{
public:
	BaseClass()
	{
		Console::WriteLine("ctor of BaseClass");
	}
	~BaseClass()
	{
		Console::WriteLine("dtor of BaseClass");
		GC::ReRegisterForFinalize(this);
	}
	!BaseClass()
	{
		Console::WriteLine("finz of BaseClass");
	}
};
ref class DerivedClass : BaseClass
{
public:
	DerivedClass()
	{
		Console::WriteLine("ctor of DerivedClass");
	}
	~DerivedClass()
	{
		Console::WriteLine("dtor of DerivedClass");
		GC::ReRegisterForFinalize(this);
	}
	!DerivedClass()
	{
		Console::WriteLine("finz of DerivedClass");
	}
};
ref class ChildClass : DerivedClass
{
public:
	ChildClass()
	{
		Console::WriteLine("ctor of ChildClass");
	}
	~ChildClass()
	{
		Console::WriteLine("dtor of ChildClass");
		GC::ReRegisterForFinalize(this);
	}
	!ChildClass()
	{
		Console::WriteLine("finz of ChildClass");
	}
};

لاحظ التقارب بين تعريفات الـ Constructors، الـ Destructors، والـ Finalizers. الأولى بدون أي إضافات فقط بإزالة القيمة المرتجعة. والثانية مثل الأولى ولكن مع إضافة العلامة ~ إلى الاسم. أما الأخيرة فهي أيضا مثل الأولى ولكن مع إضافة العلامة ! إلى الاسم.

والآن لنقم بتجربة الكود:

int main()
{
	ChildClass^ cls = gcnew ChildClass();
}

ثم قم بتشغيله لترى هذه النتائج:

ctor of BaseClass
ctor of DerivedClass
ctor of ChildClass
finz of ChildClass
finz of DerivedClass
finz of BaseClass

الآن، عملت الـ Finalizers ولم تعمل الـ Destructors!

القاعدة 7: في Classic C++ و C++/CLI يمكنك تحديد متى يتم النداء على دوال الإزالة Destructors

وذلك لأنك تملك الأمر delete والذي يقوم بإزالة العنصر مباشرة والنداء على هذه الدالة.

قم بتجربة هذا الكود:

int main()
{
	ChildClass^ cls = gcnew ChildClass();
	delete cls;
}

هذا يعمل بشكل صحيح ويقوم بالنداء على الـ Destructor.

والآن جرب هذا أيضا:

int main()
{
	ChildClass cls;
}

بانتهاء المدى الذي قمنا بتعريف هذا العنصر فيه وهو نهاية الدالة يقوم الكود بإزالته تلقائيا والنداء على نفس الدالة.

القاعدة 8: في C++/CLI لا يتم النداء على دوال الإزالة Destructors والإنهاء Finalizers معا

لا يتم النداء على دالتين للإزالة مع بعض. إما الـ Destructors أو الـ Finalizers. ينطبق على C++/CLI فقط.

وهذا تحصيل حاصل فإنك لو قمت بترك العنصر للـ GC فإنها سوف تقوم بإزالته وتنفيذ الأكواد في الـ Finalizer ومنع الأكواد في Destructor من التنفيذ. أما لو قمت بتنفيذ أمر الإزالة يدويا أو قمت بتعريف العنصر بالطريقة المعروفة لوضعه في Stack فإن كود الـ Destructor ينفذ وينمع كود الـ Finalizer.

جرب تشغيل آخر أسطر من الكود كتبناها والتي تقوم بتعريف العنصر ووضعه في Stack، ولاحظ أن الـ Destructor هي التي يتم النداء عليها (تصاعديا بالطبع، اقرأ القاعدة 3.)

ctor of BaseClass
ctor of DerivedClass
ctor of ChildClass
dtor of ChildClass
dtor of DerivedClass
dtor of BaseClass

القاعدة 9: يجب الحذر من الدوال الظاهرية Virtual Functions في دوال الإنشاء Constructors

يجب الحذر من الدوال الظاهرية Virtual Functions القابلة للتوارث Overridable من النداء عليها في أثناء كود الإنشاء Constructor وذلك لأن أسلوب النداء عليها يختلف من لغة إلى أخرى! ففي بيئة الدوت نت يتم النداء على الإصدار Version الخاص بالعنصر نفسه. أما في Classic C++ فيتم النداء على الإصدار الخاص بالتصنيف الذي يحوي الـ Constructor.

إلى الـ C#، قم بتجربة هذا الكود:

class BaseClass
{
    public BaseClass()
    {
        Foo();
    }
    public virtual void Foo()
    {
        Console.WriteLine("Foo() of BaseClass");
    }
}
class DerivedClass : BaseClass
{
    public DerivedClass()
    {
    }
    public override void Foo()
    {
        Console.WriteLine("Foo() of DerivedClass");
    }
}
class ChildClass : DerivedClass
{
    public ChildClass()
    {
    }
    public override void Foo()
    {
        Console.WriteLine("Foo() of ChildClass");
    }
}

هذا الكود قام بتعريف التدرج للتصنيفات على السياق الذي تبعناه. إضافة إلى ذلك، قام بتعريف دالة قابلة للتوارث Overridable Function وقام ببرمجتها في كل درجة من التدرج. ثم قم بالنداء عليها من دالة الـ Constructor الموجودة في أبعد أب وهو BaseClass.

الآن، قم بتشغيل هذا الكود:

    static void Main()
    {
        ChildClass cls = new ChildClass();
    }

فتحصل على النتيجة التالية:

Foo() of ChildClass

نلاحظ أنه رغم أن كود النداء على الدالة كان في الأب BaseClass ولكنه تم النداء على النسخة من الدالة الموجودة في التصنيف المراد إنشاء نسخه منه وهو ChildClass وهذا من خصائص بيئة الدوت نت.

نفس الكود في C++/CLI:

ref class BaseClass
{
public:
	BaseClass()
	{
		Foo();
	}
	virtual void Foo()
	{
		Console::WriteLine("Foo() of BaseClass");
	}
};
ref class DerivedClass : BaseClass
{
public:
	DerivedClass()
	{
	}
	virtual void Foo() override
	{
		Console::WriteLine("Foo() of DerivedClass");
	}
};
ref class ChildClass : DerivedClass
{
public:
	ChildClass()
	{
	}
	virtual void Foo() override
	{
		Console::WriteLine("Foo() of ChildClass");
	}
};

يقوم بإعطائنا نفس النتيجة.

ولكن هناك أحد مميزات C++/CLI وهي تحديد أي نسخة نريد النداء عليها بالنسبة للدوال الظاهرية Virtual.

ref class BaseClass
{
public:
	BaseClass()
	{
		BaseClass::Foo();
	}
	virtual void Foo()
	{
		Console::WriteLine("Foo() of BaseClass");
	}
};

الآن، الكود يطبع التالي:

Foo() of BaseClass

ثم ننتقل إلى ANSI C++ وهي مختلفة كثيرا عن سابقتيها. لاحظ الكود التالي وهو نفس الكود السابق ولكن في لغة الـ ANSI C++:

class CBaseClass
{
public:
	CBaseClass()
	{
		Foo();
	}
	virtual void Foo()
	{
		cout << "Foo() of CBaseClass" << endl;
	}
};
class CDerivedClass : CBaseClass
{
public:
	CDerivedClass()
	{
	}
	virtual void Foo() override
	{
		cout << "Foo() of CDerivedClass" << endl;
	}
};
class CChildClass : CDerivedClass
{
public:
	CChildClass()
	{
	}
	virtual void Foo() override
	{
		cout << "Foo() of CChildClass" << endl;
	}
};

عند تشغيله يقوم بطباعة التالي:

Foo() of BaseClass

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

خاتمة

كان هذا موجزا لـ 9 من أهم القواعد البرمجية في التعامل مع الـ Constructors، Destructors، والـ Finalizers. وإلى درس آخر وموضوع آخر بإذن الله تعالى.

مواضيع مشابهة:

اخترنا لك:

أحدث المواضيع:

هل أعجبتك؟ شارك بها...