وظيفة افتراضية

في البرمجة كائنية التوجه، كما هو شائع في لغتي C++ و Object Pascal ، تُعرَّف الدالة الافتراضية أو الطريقة الافتراضية بأنها دالة أو طريقة قابلة للتوريث وإعادة التعريف، ويتم استدعاؤها ديناميكيًا . تُعدّ الدوال الافتراضية جزءًا أساسيًا من تعدد الأشكال (وقت التشغيل) في البرمجة كائنية التوجه (OOP). فهي تُمكّن من تنفيذ الدوال المستهدفة التي لم يتم تحديدها بدقة أثناء الترجمة.

تتعامل العديد من لغات البرمجة، مثل جافا ، مع جميع الدوال على أنها دوال افتراضية افتراضياً. [ 1 ] بعض هذه اللغات، مثل بايثون ، لا تسمح بتغيير هذا السلوك. [ 2 ] ومع ذلك، قد توفر لغات أخرى مُعدِّلات تمنع إعادة تعريف الدوال بواسطة الفئات المشتقة (مثل الكلمتين المفتاحيتين final و private في جافا [ 3 ] و PHP [ 4 ] ).

غاية

يحل مفهوم الدالة الافتراضية المشكلة التالية:

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

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

تسمح الدوال الافتراضية للبرنامج باستدعاء طرق قد لا تكون موجودة بالضرورة في لحظة تجميع الكود.

في لغة C++، تُعلن الدوال الافتراضية بإضافة virtualالكلمة المفتاحية `virtual` إلى تعريف الدالة في الفئة الأساسية. يرث جميع تطبيقات تلك الدالة في الفئات المشتقة هذا المُعدِّل، مما يعني إمكانية استمرارها في إعادة تعريف بعضها البعض وربطها لاحقًا. وحتى إذا استدعت دوالٌ تابعة للفئة الأساسية الدالة الافتراضية، فإنها ستستدعي الدالة المشتقة. يحدث التحميل الزائد عندما يكون لدالتين أو أكثر في فئة واحدة نفس اسم الدالة ولكن بمعاملات مختلفة. أما إعادة التعريف فتعني وجود دالتين بنفس اسم الدالة والمعاملات. يُشار إلى التحميل الزائد أيضًا بمطابقة الدوال، وإلى إعادة التعريف بربط الدوال الديناميكي.

مثال

لغة سي++

مخطط تصنيف الحيوانات

على سبيل المثال، قد تحتوي الفئة الأساسية Animalعلى دالة افتراضية eat. Llamaستنفذ الفئة الفرعية هذه الدالة eatبشكل مختلف عن الفئة الفرعية الأخرى Wolf، ولكن يمكن استدعاء الدالة eatعلى أي نسخة من الفئة المشار إليها باسم Animal، والحصول على eatسلوك الفئة الفرعية المحددة.

استيراد std ؛class Animal { public : // ليس افتراضيًا عمدًا: void move () { std :: println ( "هذا الحيوان يتحرك بطريقة ما" ); }virtual void eat () = 0 ; };// يمكن أن تحتوي الفئة "Animal" على تعريف للدالة Eat إذا لزم الأمر. class Llama : public Animal { public : // يتم توريث الدالة Move غير الافتراضية ولكن لا يتم تجاوزها. void eat () override { std :: println ( "تأكل اللاما العشب!" ); } };

يسمح هذا للمبرمج بمعالجة قائمة من الكائنات من الفئة Animal، وإخبار كل منها بدوره أن يأكل (عن طريق استدعاء eat)، دون الحاجة إلى معرفة نوع الحيوان الذي قد يكون موجودًا في القائمة، أو كيف يأكل كل حيوان، أو ما هي المجموعة الكاملة لأنواع الحيوانات الممكنة.

ج

في لغة C، يمكن توفير الآلية الكامنة وراء الدوال الافتراضية بالطريقة التالية:

#include <stdio.h>// يشير الكائن إلى فئته... typedef struct Animal { const struct AnimalVTable * vtable ; } Animal ;// ...والتي تحتوي على الدالة الافتراضية Animal.eat typedef struct AnimalVTable { void ( * eat )( Animal * self ); // دالة افتراضية } AnimalVTable ;/*  بما أن Animal.move ليست دالة افتراضية،  فهي غير موجودة في البنية أعلاه. */ void move ( const Animal * self ) { printf ( "<Animal at %p> moved in some way \n " , ( void * )( self )); }/*  على عكس دالة move، التي تُنفّذ Animal.move مباشرةً،  لا تستطيع دالة eat معرفة الدالة التي يجب استدعاؤها (إن وُجدت) أثناء وقت الترجمة.  لا يُمكن تحديد Animal.eat إلا أثناء وقت التشغيل عند استدعاء eat. */ void eat ( Animal * self ) { const AnimalVTable * vtable = self -> vtable ; if ( vtable -> eat ) { ( * vtable -> eat )( self ); // تنفيذ Animal.eat } else { fprintf ( stderr , "'eat' virtual method not implemented \n " ); } }/*  تنفيذ دالة Llama.eat، وهي الدالة المستهدفة  التي يتم استدعاؤها بواسطة 'void eat(Animal* self).' */ static void Llama_eat ( Animal * self ) { printf ( "<Llama at %p> اللاما تأكل العشب! \n " , ( void * )( self )); }// تهيئة الفئة const AnimalVTable Animal_vt = { NULL }; // الفئة الأساسية لا تُنفذ واجهة Animal.Eat const AnimalVTable Llama_vt = { Llama_eat }; // لكن الفئة المشتقة تُنفذهاint main ( void ) { // تهيئة الكائنات كنسخ من فئتها Animal animal = { & Animal_vt }; Animal llama = { & Llama_vt }; move ( & animal ); // Animal.move move ( & llama ); // Llama.move eat ( & animal ); // لا يمكن استدعاء Animal.eat، لذا اطبع "غير مُنفذ" إلى الخطأ القياسي eat ( & llama ); // يتم استدعاء Llama.eat وتنفيذه }

الفئات المجردة والوظائف الافتراضية البحتة

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

على سبيل المثال، MathSymbolقد توفر فئة أساسية مجردة دالة افتراضية بحتة ، وتقوم doOperation()الفئات المشتقة بتنفيذها لتوفير تطبيقات ملموسة. لن يكون لتنفيذها معنى في الفئة، لأنها مفهوم مجرد يُحدد سلوكه فقط لكل نوع (فئة فرعية) مُعطى من الفئة الأساسية . وبالمثل، لن تكون أي فئة فرعية من الفئة الأساسية مكتملة بدون تطبيق للدالة الافتراضية البحتة .PlusMinusdoOperation()doOperation()MathSymbolMathSymbolMathSymbolMathSymboldoOperation()

على الرغم من أن الدوال الافتراضية البحتة لا تحتوي عادةً على أي تنفيذ في الفئة التي تُعلن عنها، إلا أنه يُسمح في بعض اللغات (مثل C++ و Python) بأن تحتوي الدوال الافتراضية البحتة على تنفيذ في الفئة المُعلنة عنها، مما يوفر سلوكًا احتياطيًا أو افتراضيًا يمكن للفئة المشتقة تفويضه إليه، إذا لزم الأمر. [ 5 ] [ 6 ]

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

السلوك أثناء البناء والهدم

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

في لغة C++، يتم استدعاء الدالة "الأساسية". تحديدًا، يتم استدعاء الدالة الأكثر اشتقاقًا التي لا تتجاوز اشتقاق فئة المُنشئ أو المُدمِّر الحالي. [ 7 ] : §15.7.3 [ 8 ] [ 9 ] إذا كانت تلك الدالة دالة افتراضية بحتة، فسيحدث سلوك غير مُعرَّف . [ 7 ] : §13.4.6 [ 8 ] ينطبق هذا حتى لو احتوت الفئة على تطبيق لتلك الدالة الافتراضية البحتة، حيث يجب تحديد استدعاء الدالة الافتراضية البحتة بشكل صريح. [ 10 ] لا يُشترط على تطبيق C++ المتوافق (ولا يستطيع عمومًا) اكتشاف الاستدعاءات غير المباشرة للدوال الافتراضية البحتة في وقت الترجمة أو وقت الربط . ستُصدر بعض أنظمة التشغيل خطأ استدعاء دالة افتراضية بحتة عند مواجهة استدعاء لدالة افتراضية بحتة في وقت التشغيل .

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

المدمرات الافتراضية

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

في سياقات إدارة الذاكرة اليدوية، قد يكون الوضع أكثر تعقيدًا، لا سيما فيما يتعلق بالتوزيع الثابت . إذا تم إنشاء كائن من نوع Wolf ولكن تمت الإشارة إليه بواسطة مؤشر من نوع Animal، وتم حذف هذا المؤشر من نوع Animal، فقد يتم استدعاء المُدمِّر المُعرَّف لنوع Animal وليس لنوع Wolf، إلا إذا كان المُدمِّر افتراضيًا. ينطبق هذا بشكل خاص على لغة C++، حيث يُعد هذا السلوك مصدرًا شائعًا لأخطاء البرمجة إذا لم تكن المُدمِّرات افتراضية.

انظر أيضاً

مراجع

  1. "تعدد الأشكال (دروس جافا™ > تعلم لغة جافا > الواجهات والوراثة)" . docs.oracle.com . تم الاطلاع عليه بتاريخ 11 يوليو 2020 .
  2. "9. الفئات - وثائق بايثون 3.15.5" . docs.python.org . تم الاطلاع عليه بتاريخ 2025-07-05 .
  3. "كتابة الفئات والأساليب النهائية (دروس جافا™ > تعلم لغة جافا > الواجهات والوراثة)" . docs.oracle.com . تم الاطلاع عليه بتاريخ 11 يوليو 2020 .
  4. "PHP: الكلمة المفتاحية النهائية - الدليل" . www.php.net . تم الاطلاع عليه بتاريخ 11 يوليو 2020 .
  5. المدمرات الافتراضية البحتة – cppreference.com
  6. "abc — الفئات الأساسية المجردة: @abc.abstractmethod"
  7. 1 2 "N4659: مسودة عمل، معيار للغة البرمجة C++" (PDF) .
  8. 1 2 تشين، ريموند (28 أبريل 2004). "ما هو __purecall؟" .
  9. مايرز، سكوت (6 يونيو 2005). "لا تقم أبدًا باستدعاء الوظائف الافتراضية أثناء الإنشاء أو التدمير" .
  10. تشين، ريموند (11 أكتوبر 2013). "حالة خاصة بلغة C++: يمكنك تنفيذ الدوال الافتراضية البحتة في الفئة الأساسية" .
  11. غانيش، إس جي (1 أغسطس 2011). "متعة البرمجة: استدعاء الدوال الافتراضية من المنشئات" .