سؤال

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

ما الأمر بميراث متعدد؟ هل هناك أي عينات ملموسة؟

هل كانت مفيدة؟

المحلول

المشكلة الأكثر وضوحا هي مع الإفراط في الوظيفة.

لنفترض أن يكون لديك فصلين A و B, ، وكلاهما يحدد طريقة doSomething. الآن تحدد الدرجة الثالثة C, الذي يرث من كليهما A و B, ، لكنك لا تتجاوز doSomething طريقة.

عندما بذرة المترجم هذا الرمز ...

C c = new C();
c.doSomething();

... ما هو تطبيق الطريقة التي يجب أن تستخدمها؟ دون أي توضيح إضافي ، من المستحيل على المترجم حل الغموض.

إلى جانب الإفراط ، فإن المشكلة الكبيرة الأخرى مع ميراث متعددة هي تخطيط الكائنات الفعلية في الذاكرة.

قم بإنشاء لغات مثل C ++ و Java و C# تخطيطًا ثابتًا قائمًا على العنوان لكل نوع من الكائنات. شيء من هذا القبيل:

class A:
    at offset 0 ... "abc" ... 4 byte int field
    at offset 4 ... "xyz" ... 8 byte double field
    at offset 12 ... "speak" ... 4 byte function pointer

class B:
    at offset 0 ... "foo" ... 2 byte short field
    at offset 2 ... 2 bytes of alignment padding
    at offset 4 ... "bar" ... 4 byte array pointer
    at offset 8 ... "baz" ... 4 byte function pointer

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

الميراث المتعدد يجعلها صعبة للغاية.

إذا فئة C يرث من كليهما A و B, ، يتعين على المترجم أن يقرر ما إذا كان سيتم تخطيط البيانات في AB طلب أو في BA ترتيب.

لكن الآن تخيل أنك تتصل بالأساليب على B هدف. هل هو حقا مجرد ملف B؟ أم أنها في الواقع C كائن يسمى متعددة الأشكال ، من خلال B واجهه المستخدم؟ اعتمادًا على الهوية الفعلية للكائن ، سيكون التخطيط المادي مختلفًا ، ومن المستحيل معرفة إزاحة الوظيفة للاستدعاء في موقع الاتصال.

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

لذلك ... قصة قصيرة طويلة ... إنها ألم في الرقبة لمؤلفي التحويل البرمجي لدعم الميراث المتعدد. لذلك عندما يصمم شخص مثل Guido Van Rossum Python ، أو عندما يصمم Anders Hejlsberg C#، فإنهم يعلمون أن دعم الميراث المتعدد سيجعل تطبيقات البروتشر أكثر تعقيدًا بشكل ملحوظ ، ويفترض أنها لا تعتقد أن الفائدة تستحق التكلفة.

نصائح أخرى

المشكلات التي ذكرتها يا رفاق ليست من الصعب حلها حقًا. في الواقع مثل إيفل يفعل ذلك بشكل جيد تماما! (ودون تقديم خيارات تعسفية أو أي شيء آخر)

على سبيل المثال ، إذا كنت ترث من A و B ، كلاهما طريقة Foo () ، فأنت بالطبع لا تريد اختيارًا تعسفيًا في الفصل C من كل من A و B. يستخدم إذا تم استدعاء C.FOO () أو يتعين عليك إعادة تسمية إحدى الأساليب في C. (يمكن أن تصبح BAR ())

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

مشكلة الماس:

غموض ينشأ عندما يرث فئتان B و C من A ، ويرث الفئة D من كل من B و C. إذا كانت هناك طريقة في A B و C تجاوز, ، و D لا يتجاوزها ، ثم أي إصدار من الطريقة يرث D: إصدار B ، أو إصدار C؟

... يطلق عليه "مشكلة الماس" بسبب شكل مخطط الميراث الطبقي في هذا الموقف. في هذه الحالة ، يكون الفئة A في الأعلى ، كل من B و C بشكل منفصل تحته ، و D ينضم إلى الاثنين معًا في الأسفل لتشكيل شكل ماسي ...

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

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

دعنا نقول أن لديك كائنات A و B والتي ورثتها كلاهما C. A و B على حد سواء تنفيذ Foo () و C لا. أدعو c.foo (). أي تطبيق يتم اختياره؟ هناك مشكلات أخرى ، ولكن هذا النوع من الأشياء هو واحد كبير.

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

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

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

أجد أنه عند القيام بتصميم جيد لا ، لا أحتاج أبدًا إلى ميراث متعدد. في الحالات ، أحتاج إلى ذلك ، عادةً ما أجد أنني أستخدم الميراث لإعادة استخدام الوظيفة في حين أن الميراث مناسب فقط للعلاقات "IS-A".

هناك تقنيات أخرى مثل Mixins التي تحل نفس المشكلات وليس لديها المشكلات التي تواجهها الميراث المتعدد.

لا أعتقد أن مشكلة الماس تشكل مشكلة ، وسأفكر في تلك المرتبة ، لا شيء آخر.

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

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

public sealed class CustomerEditView : Form, MVCView<Customer>

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

في رأيي ، يجب ألا تكون هناك حاجة على الإطلاق ، وليس أقل ، لأي تكرار للرمز بلغة حديثة.

يعد نظام كائن LISP الشائع (CLOS) مثالًا آخر على شيء يدعم MI مع تجنب مشاكل النمط C ++: يتم إعطاء الميراث الافتراضي المعقول, ، بينما لا تزال تسمح لك بالحرية في أن تقرر صراحة كيف ، على سبيل المثال ، استدعاء سلوك Super.

لا حرج في الميراث المتعدد نفسه. تكمن المشكلة في إضافة ميراث متعددة إلى لغة لم يتم تصميمها مع مراعاة ميراث متعددة منذ البداية.

تدعم لغة إيفل الميراث المتعدد دون قيود بطريقة فعالة ومثمرة للغاية ، لكن اللغة تم تصميمها من تلك البدء في دعمها.

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

أعتقد أن دعم الميراث المتعدد أم لا هو مسألة اختيار أكثر من أولويات. تستغرق ميزة أكثر تعقيدًا مزيدًا من الوقت لتنفيذها بشكل صحيح وتشغيلها وقد تكون أكثر إثارة للجدل. قد يكون تطبيق C ++ هو السبب في عدم تنفيذ الميراث المتعدد في C# و Java ...

أحد أهداف تصميم الأطر مثل Java و .NET هو جعل من الممكن للرمز الذي يتم تجميعه للعمل مع إصدار واحد من مكتبة مسبقة ، للعمل بشكل جيد مع الإصدارات اللاحقة من تلك المكتبة ، حتى لو كانت تلك الإصدارات اللاحقة أضف ميزات جديدة. في حين أن النموذج الطبيعي بلغات مثل C أو C ++ هو توزيع المواد التنفيذية المرتبطة بشكل ثابت والتي تحتوي على جميع المكتبات التي يحتاجونها ، فإن النموذج في .NET و Java هو توزيع التطبيقات على أنها مجموعات من المكونات "مرتبطة" في وقت التشغيل .

حاول نموذج COM الذي سبق .NET استخدام هذا النهج العام ، لكنه لم يكن له ميراث حقًا-instead ، حدد كل تعريف فئة فعليًا فئة وواجهة تحمل نفس الاسم التي تحتوي على جميع أعضاءها العامين. كانت الحالات من نوع الفصل ، في حين كانت المراجع من نوع الواجهة. أعلن أن فئة مستمدة من آخر كانت مكافئة لإعلان فئة على أنها تنفيذ واجهة الآخر ، وتطلب من الفئة الجديدة إعادة تنفيذ جميع أعضاء الجمهور من الفئات التي اشتق منها أحدهم. إذا كانت Y و Z مستمدة من X ، ثم مستمدة من Y و Z ، فلن يهم ما إذا كانت Y و z تنفذ أعضاء X بشكل مختلف ، لأن Z لن يتمكن من استخدام تطبيقاتها-سيتعين عليها تحديدها ملك. قد يتغلف W مثيلات Y و/أو Z ، وترسل تطبيقاتها لأساليب X من خلال أساليبهم ، ولكن لن يكون هناك أي غموض فيما يتعلق بما يجب أن تفعله أساليب X-لقد قاموا بكل ما قام به رمز Z إلى القيام به بشكل صريح.

الصعوبة في Java و .NET هي أن الكود يسمح له براث الأعضاء والوصول إليهم بشكل ضمني الرجوع إلى الأعضاء الوالدين. لنفترض أن أحدهم لديه فصول WZ ذات الصلة على النحو الوارد أعلاه:

class X { public virtual void Foo() { Console.WriteLine("XFoo"); }
class Y : X {};
class Z : X {};
class W : Y, Z  // Not actually permitted in C#
{
  public static void Test()
  {
    var it = new W();
    it.Foo();
  }
}

يبدو الأمر W.Test() يجب إنشاء مثيل W استدعاء تنفيذ الطريقة الافتراضية Foo المعرفة في X. ومع ذلك ، لنفترض أن y و z كانتا بالفعل في وحدة نمطية منفصلة بشكل منفصل ، وعلى الرغم من أنهما تم تعريفهما على النحو الوارد أعلاه عندما تم تجميع x و w ، فقد تم تغييرهما لاحقًا وإعادة تجميعهما:

class Y : X { public override void Foo() { Console.WriteLine("YFoo"); }
class Z : X { public override void Foo() { Console.WriteLine("ZFoo"); }

الآن ما الذي يجب أن يكون تأثير الاتصال W.Test()؟ إذا كان لا بد من ربط البرنامج بشكل ثابت قبل التوزيع ، فقد تكون مرحلة الارتباط الثابتة قادرة على تمييز أنه على الرغم من عدم وجود غموض قبل تغيير y و z ، فإن التغييرات على y و z جعلت الأمور غامضة ويمكن أن يرفض الرابط بناء البرنامج ما لم يتم حل هذا الغموض أو حتى يتم حل هذا الغموض. من ناحية أخرى ، من المحتمل أن يكون الشخص الذي لديه W والإصدارات الجديدة من Y و Z هو الشخص الذي يريد ببساطة تشغيل البرنامج وليس لديه رمز مصدر لأي منها. متي W.Test() يدير ، لن يكون من الواضح ماذا W.Test() يجب أن يفعل ذلك ، ولكن حتى حاول المستخدم تشغيل W مع الإصدار الجديد من Y و Z لن يكون هناك أي جزء من النظام يمكن أن يدرك وجود مشكلة (ما لم يتم اعتبار W غير شرعية حتى قبل التغييرات على Y و Z) .

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

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

  A       A
 / \     / \
B   C   D   E
 \ /     \ /
  F       G
    \   /
      H

في C ++ من المستحيل تمامًا: بمجرد F و G يتم دمجها في فصل واحد ، Aيتم دمج S أيضا ، الفترة. هذا يعني أنك قد لا تفكر أبدًا A في H لذلك عليك أن تعرف أنها موجودة في مكان ما في التسلسل الهرمي). في لغات أخرى قد تعمل ، ومع ذلك ؛ فمثلا، F و G يمكن أن يعلن صراحةً أنه "داخلي" ، مما يمنع الاندماج ويجعل أنفسهم قويين بشكل فعال.

مثال آخر مثير للاهتمام (ليس C ++-محدد):

  A
 / \
B   B
|   |
C   D
 \ /
  E

فقط هنا B يستخدم الميراث الافتراضي. لذا E يحتوي على اثنين BS التي تشترك في نفس الشيء A. بهذه الطريقة ، يمكنك الحصول على A* المؤشر الذي يشير إلى E, ، لكن لا يمكنك إلقاءها على B* المؤشر على الرغم من الكائن هو في الحقيقة B على هذا النحو ، فإن هذا الممثل غامض ، ولا يمكن اكتشاف هذا الغموض في وقت الترجمة (ما لم ير المترجم البرنامج بأكمله). هنا هو رمز الاختبار:

struct A { virtual ~A() {} /* so that the class is polymorphic */ };
struct B: virtual A {};
struct C: B {};
struct D: B {};
struct E: C, D {};

int main() {
        E data;
        E *e = &data;
        A *a = dynamic_cast<A *>(e); // works, A is unambiguous
//      B *b = dynamic_cast<B *>(e); // doesn't compile
        B *b = dynamic_cast<B *>(a); // NULL: B is ambiguous
        std::cout << "E: " << e << std::endl;
        std::cout << "A: " << a << std::endl;
        std::cout << "B: " << b << std::endl;
// the next casts work
        std::cout << "A::C::B: " << dynamic_cast<B *>(dynamic_cast<C *>(e)) << std::endl;
        std::cout << "A::D::B: " << dynamic_cast<B *>(dynamic_cast<D *>(e)) << std::endl;
        std::cout << "A=>C=>B: " << dynamic_cast<B *>(dynamic_cast<C *>(a)) << std::endl;
        std::cout << "A=>D=>B: " << dynamic_cast<B *>(dynamic_cast<D *>(a)) << std::endl;
        return 0;
}

علاوة على ذلك ، قد يكون التنفيذ معقدًا للغاية (يعتمد على اللغة ؛ انظر إجابة Benjismith).

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top