ما هي الاختلافات بين الأدوية العامة في C# وJava... والقوالب في C++؟[مغلق]

StackOverflow https://stackoverflow.com/questions/31693

سؤال

أستخدم Java في الغالب والأدوية العامة جديدة نسبيًا.ما زلت أقرأ أن Java اتخذت القرار الخاطئ أو أن .NET لديه تطبيقات أفضل وما إلى ذلك.إلخ.

إذن، ما هي الاختلافات الرئيسية بين C++ وC# وJava في الأدوية العامة؟إيجابيات/سلبيات كل منهما؟

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

المحلول

سأضم صوتي إلى الضجيج وأحاول توضيح الأمور:

تسمح لك C# Generics بالإعلان عن شيء كهذا.

List<Person> foo = new List<Person>();

ومن ثم سيمنعك المترجم من وضع أشياء ليست كذلك Person في القائمة.
خلف الكواليس يقوم مترجم C# بوضع List<Person> في ملف .NET dll، ولكن في وقت التشغيل، يقوم برنامج التحويل البرمجي JIT ببناء مجموعة جديدة من التعليمات البرمجية، كما لو كنت قد كتبت فئة قائمة خاصة لاحتواء الأشخاص فقط - شيء من هذا القبيل ListOfPerson.

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

الجانب السلبي لهذا هو أن كود C# 1.0 و1.1 القديم (قبل إضافة الأدوية العامة) لا يفهم هذه العناصر الجديدة List<something>, ، لذلك يتعين عليك تحويل الأشياء يدويًا إلى وضعها القديم List للتفاعل معهم.هذه ليست مشكلة كبيرة، لأن الكود الثنائي C# 2.0 غير متوافق مع الإصدارات السابقة.المرة الوحيدة التي سيحدث فيها هذا هي إذا كنت تقوم بترقية بعض أكواد C# 1.0/1.1 القديمة إلى C# 2.0

تتيح لك Java Generics الإعلان عن شيء كهذا.

ArrayList<Person> foo = new ArrayList<Person>();

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

الفرق هو ما يحدث خلف الكواليس.على عكس C#، لا تقوم Java ببناء لغة خاصة ListOfPerson - يستخدم فقط القديم العادي ArrayList والذي كان دائمًا في جافا.عندما تخرج الأشياء من المصفوفة، فهذا هو المعتاد Person p = (Person)foo.get(1); لا يزال يتعين القيام بالرقص.يوفر لك المترجم ضغطات المفاتيح، لكن سرعة النقر/الإرسال لا تزال كما كانت دائمًا.
عندما يذكر الناس "محو الكتابة" فهذا ما يتحدثون عنه.يقوم المترجم بإدراج القوالب لك، ثم "يمحو" حقيقة أنه من المفترض أن تكون قائمة من Person ليس فقط Object

فائدة هذا النهج هي أن الكود القديم الذي لا يفهم الأدوية العامة لا يحتاج إلى الاهتمام.انها لا تزال تتعامل مع نفس القديم ArrayList كما كان الحال دائمًا.يعد هذا أكثر أهمية في عالم Java لأنهم أرادوا دعم تجميع التعليمات البرمجية باستخدام Java 5 مع الأدوية العامة، وتشغيلها على الإصدار 1.4 القديم أو JVM السابقة، والتي قررت Microsoft عمدًا عدم الاهتمام بها.

الجانب السلبي هو السرعة التي ذكرتها سابقًا، وأيضًا لأنه لا يوجد ListOfPerson فئة زائفة أو أي شيء من هذا القبيل يدخل في ملفات .class، أو الكود الذي ينظر إليها لاحقًا (مع الانعكاس، أو إذا قمت بسحبها من مجموعة أخرى حيث تم تحويلها إلى Object أو ما إلى ذلك) لا يمكن أن أقول بأي شكل من الأشكال أنه من المفترض أن تكون قائمة تحتوي فقط Person وليس فقط أي قائمة مصفوفة أخرى.

تسمح لك قوالب C++ بالإعلان عن شيء كهذا

std::list<Person>* foo = new std::list<Person>();

يبدو الأمر مثل الأدوية العامة الخاصة بـ C# وJava، وسيفعل ما تعتقد أنه ينبغي عليه فعله، ولكن وراء الكواليس تحدث أشياء مختلفة.

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

ينتج كل من C# وJava مخرجات مصممة للأجهزة الافتراضية.إذا كتبت بعض التعليمات البرمجية التي تحتوي على Person فئة فيه، وفي كلتا الحالتين بعض المعلومات حول أ Person ستنتقل الفئة إلى ملف .dll أو .class، وسيقوم JVM/CLR بأشياء بهذا.

تنتج لغة C++ كودًا ثنائيًا خامًا إلى x86.كل شيء لا كائن، ولا يوجد جهاز ظاهري أساسي يحتاج إلى معرفته Person فصل.لا يوجد ملاكمة أو فتح ملاكمة، ولا يجب أن تنتمي الوظائف إلى فئات، أو أي شيء بالفعل.

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

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

string addNames<T>( T first, T second ) { return first.Name() + second.Name(); }

لن يتم تجميع هذا الرمز في C# أو Java، لأنه لا يعرف هذا النوع T يوفر في الواقع طريقة تسمى Name().عليك أن تقول ذلك - في C# مثل هذا:

interface IHasName{ string Name(); };
string addNames<T>( T first, T second ) where T : IHasName { .... }

وبعد ذلك عليك التأكد من أن الأشياء التي تمررها إلى addNames تنفذ واجهة IHasName وما إلى ذلك.بناء جملة جافا مختلف (<T extends IHasName>)، لكنه يعاني من نفس المشاكل.

الحالة "الكلاسيكية" لهذه المشكلة هي محاولة كتابة دالة تقوم بذلك

string addNames<T>( T first, T second ) { return first + second; }

لا يمكنك بالفعل كتابة هذا الرمز لأنه لا توجد طرق للإعلان عن واجهة مع ملف + الطريقة فيه.لقد فشلت.

لا تعاني لغة C++ من أي من هذه المشاكل.لا يهتم المترجم بتمرير الأنواع إلى أي جهاز افتراضي - إذا كان كلا الكائنين لديك يحتوي على وظيفة .Name()، فسيتم تجميعه.إذا لم يفعلوا ذلك، فلن يفعلوا ذلك.بسيط.

إذن، إليكم الأمر :-)

نصائح أخرى

نادرًا ما تستخدم لغة C++ المصطلحات "العامة".وبدلاً من ذلك، يتم استخدام كلمة "قوالب" وهي أكثر دقة.تصف القوالب واحدة تقنية لتحقيق تصميم عام.

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

template <unsigned int N>
struct product {
    static unsigned int const VALUE = N * product<N - 1>::VALUE;
};

template <>
struct product<1> {
    static unsigned int const VALUE = 1;
};

// Usage:
unsigned int const p5 = product<5>::VALUE;

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

يعد تخصص القالب مهمًا لـ C++ لأنه يسمح بالاختلافات الهيكلية في هياكل البيانات.تعد القوالب ككل وسيلة لتوحيد الواجهة عبر الأنواع.ومع ذلك، على الرغم من أن هذا أمر مرغوب فيه، لا يمكن التعامل مع جميع الأنواع على قدم المساواة داخل التنفيذ.تأخذ قوالب C++ هذا في الاعتبار.وهذا هو نفس الفرق الذي يحدثه OOP بين الواجهة والتنفيذ مع تجاوز الأساليب الافتراضية.

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

يعد استخدام التكرارات بدلاً من الحاويات مفيدًا لأنه يسمح بالعمل على أجزاء من الحاوية بدلاً من العمل عليها بالكامل.

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

template <typename T>
class Store { … }; // (1)

هذا يعمل لأي نوع من العناصر.ولكن لنفترض أنه يمكننا تخزين المؤشرات بكفاءة أكبر من الأنواع الأخرى من خلال تطبيق بعض الحيل الخاصة.يمكننا القيام بذلك عن طريق جزئيا متخصص لجميع أنواع المؤشر:

template <typename T>
class Store<T*> { … }; // (2)

الآن، عندما نقوم بمثيل قالب حاوية لنوع واحد، يتم استخدام التعريف المناسب:

Store<int> x; // Uses (1)
Store<int*> y; // Uses (2)
Store<string**> z; // Uses (2), with T = string*.

وصف أندرس هيجلسبيرج بنفسه الاختلافات هنا "الأدوية العامة في C# وJava وC++".

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

وكما سبق أن أوضحنا، فإن الفرق الرئيسي هو نوع المحو, ، أي.حقيقة أن مترجم Java يمحو الأنواع العامة ولا ينتهي بها الأمر في الكود الثانوي الذي تم إنشاؤه.ومع ذلك فإن السؤال هو:لماذا لأي شخص أن يفعل ذلك؟هذا غير منطقي!أم أنه كذلك؟

طيب ما هو البديل؟إذا لم تقم بتطبيق الأدوية العامة في اللغة، فأين يفعل قمت بتنفيذها؟والجواب هو:في الجهاز الظاهري.مما يكسر التوافق مع الإصدارات السابقة.

من ناحية أخرى، يسمح لك محو الكتابة بخلط العملاء العامين مع المكتبات غير العامة.بعبارة أخرى:لا يزال من الممكن نشر التعليمات البرمجية التي تم تجميعها في Java 5 إلى Java 1.4.

ومع ذلك، قررت شركة Microsoft كسر التوافق مع الإصدارات السابقة للأدوية الجنيسة. هذا لماذا تعتبر .NET Generics "أفضل" من Java Generics.

بالطبع، صن ليس أغبياء أو جبناء.كان السبب وراء "تقطيعهم" هو أن Java كانت أقدم بكثير وأكثر انتشارًا من .NET عندما قدموا الأدوية الجنيسة.(لقد تم تقديمهما في نفس الوقت تقريبًا في كلا العالمين.) وكان كسر التوافق مع الإصدارات السابقة يمثل ألمًا كبيرًا.

ضع طريقة أخرى:في جافا، الأدوية العامة هي جزء من لغة (مما يعني أنهم يطبقون فقط إلى Java، وليس إلى لغات أخرى)، في .NET فهي جزء من آلة افتراضية (مما يعني أنها تنطبق على الجميع اللغات، وليس فقط C# وVisual Basic.NET).

قارن ذلك مع ميزات .NET مثل LINQ وتعبيرات lambda واستدلال نوع المتغير المحلي والأنواع المجهولة وأشجار التعبير:هذه كلها لغة سمات.ولهذا السبب توجد اختلافات طفيفة بين VB.NET وC#:إذا كانت هذه الميزات جزءًا من الجهاز الافتراضي، فستكون هي نفسها في الجميع اللغات.لكن CLR لم يتغير:فهو لا يزال كما هو في .NET 3.5 SP1 كما كان في .NET 2.0.يمكنك ترجمة برنامج C# يستخدم LINQ مع برنامج التحويل البرمجي .NET 3.5 مع الاستمرار في تشغيله على .NET 2.0، بشرط عدم استخدام أي مكتبات .NET 3.5.التي من شأنها أن لا العمل مع الأدوية العامة و.NET 1.1، لكنه كان العمل مع جافا وجافا 1.4.

متابعة لمنشوري السابق .

تعد القوالب أحد الأسباب الرئيسية لفشل C++ بشكل ذريع في التحسس، بغض النظر عن IDE المستخدم.بسبب تخصص القالب، لا يمكن لـ IDE التأكد من وجود عضو معين أم لا.يعتبر:

template <typename T>
struct X {
    void foo() { }
};

template <>
struct X<int> { };

typedef int my_int_type;

X<my_int_type> a;
a.|

الآن، المؤشر في الموضع المشار إليه ومن الصعب جدًا على IDE أن يقول في هذه المرحلة ما إذا كان الأعضاء وماذا a لديه.بالنسبة للغات الأخرى، سيكون التحليل واضحًا ولكن بالنسبة للغة C++، هناك حاجة إلى قدر كبير من التقييم مسبقًا.

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

template <typename T>
struct Y {
    typedef T my_type;
};

X<Y<int>::my_type> b;

بعد قليل من التفكير، سيستنتج المبرمج أن هذا الكود هو نفس الكود أعلاه: Y<int>::my_type يقرر ل int, ، لذلك b يجب أن يكون من نفس النوع a, ، يمين؟

خطأ.عند النقطة التي يحاول فيها المترجم حل هذه العبارة، فإنه لا يعرف في الواقع Y<int>::my_type حتى الآن!ولذلك، فإنه لا يعرف أن هذا هو النوع.يمكن أن يكون شيئا آخر، على سبيل المثال.وظيفة عضو أو حقل.قد يؤدي هذا إلى الغموض (لكن ليس في الحالة الحالية)، وبالتالي يفشل المترجم.علينا أن نخبرها صراحةً أننا نشير إلى اسم النوع:

X<typename Y<int>::my_type> b;

الآن، يتم تجميع التعليمات البرمجية.لمعرفة كيفية ظهور الغموض من هذا الموقف، خذ بعين الاعتبار الكود التالي:

Y<int>::my_type(123);

بيان الكود هذا صالح تمامًا ويطلب من C++ تنفيذ استدعاء الوظيفة إليه Y<int>::my_type.ومع ذلك، إذا my_type ليست وظيفة بل هي نوع، فإن هذا البيان سيظل صالحًا ويؤدي عملية تحويل خاصة (طاقة نمط الوظيفة) والتي غالبًا ما تكون استدعاءًا منشئًا.لا يستطيع المترجم معرفة ما نعنيه، لذا يتعين علينا إزالة الغموض هنا.

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

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

هناك اختلاف ملحوظ آخر وهو فئات Enum في Java وC#. يحتوي Java's Enum على هذا التعريف المتعرج إلى حد ما:

//  java.lang.Enum Definition in Java
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {

(انظر أنجليكا لانجر واضحة جدًا شرح السبب بالضبط هذا هو الحال.بشكل أساسي، هذا يعني أن Java يمكنها منح الكتابة وصولاً آمنًا من سلسلة إلى قيمة Enum الخاصة بها:

//  Parsing String to Enum in Java
Colour colour = Colour.valueOf("RED");

قارن هذا بإصدار C#:

//  Parsing String to Enum in C#
Colour colour = (Colour)Enum.Parse(typeof(Colour), "RED");

نظرًا لأن Enum كان موجودًا بالفعل في لغة C# قبل تقديم الأدوية العامة إلى اللغة، فلا يمكن تغيير التعريف دون كسر التعليمات البرمجية الموجودة.لذلك، مثل المجموعات، تظل في المكتبات الأساسية في هذه الحالة القديمة.

لقد تأخرت 11 شهرًا، ولكن أعتقد أن هذا السؤال جاهز لبعض عناصر Java Wildcard.

هذه هي الميزة النحوية لجافا.لنفترض أن لديك طريقة:

public <T> void Foo(Collection<T> thing)

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

public void Foo(Collection<?> thing)

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

لا يوجد شيء يمكنك فعله باستخدام أحرف البدل ولا يمكنك فعله أيضًا باستخدام معلمة نوع مسماة (وهي الطريقة التي تتم بها هذه الأشياء دائمًا في C++ وC#).

تحتوي ويكيبيديا على كتابات رائعة تقارن بين الاثنين جافا/C# الأدوية العامة و جينات جافا/C++ قوالب.ال المقال الرئيسي عن الأدوية الجنيسة يبدو تشوشًا بعض الشيء ولكنه يحتوي على بعض المعلومات الجيدة فيه.

أكبر شكوى هي محو الكتابة.في ذلك، لا يتم فرض الأدوية العامة في وقت التشغيل. إليك رابط لبعض مستندات Sun حول هذا الموضوع.

يتم تنفيذ الأعياد عن طريق محو النوع:معلومات النوع العام موجودة فقط في وقت الترجمة ، وبعد ذلك تمحى من قبل المترجم.

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

في Java، تكون الأدوية العامة على مستوى المترجم فقط، لذا تحصل على:

a = new ArrayList<String>()
a.getClass() => ArrayList

لاحظ أن نوع "a" عبارة عن قائمة مصفوفات، وليس قائمة سلاسل.وبالتالي فإن نوع قائمة الموز يساوي() قائمة القرود.

إذا جاز التعبير.

يبدو أنه من بين المقترحات الأخرى المثيرة للاهتمام، هناك اقتراح يتعلق بتحسين الأدوية العامة وكسر التوافق مع الإصدارات السابقة:

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

في مقالة Alex Miller حول مقترحات Java 7

ملحوظة:ليس لدي ما يكفي من النقاط للتعليق، لذا لا تتردد في نقل هذا كتعليق إلى الإجابة المناسبة.

وخلافًا للاعتقاد الشائع، الذي لا أفهم أبدًا من أين جاء، فقد قام .net بتنفيذ أدوية عامة حقيقية دون كسر التوافق مع الإصدارات السابقة، وقد بذلوا جهدًا واضحًا لتحقيق ذلك.لا يلزمك تغيير كود .net 1.0 غير العام إلى كود عام لاستخدامه في .net 2.0 فقط.لا تزال كل من القوائم العامة وغير العامة متاحة في .Net Framework 2.0 حتى حتى 4.0، لا لشيء آخر سوى سبب التوافق مع الإصدارات السابقة.لذلك، ستظل الرموز القديمة التي لا تزال تستخدم ArrayList غير عامة تعمل، وستستخدم نفس فئة ArrayList كما كان من قبل.يتم دائمًا الحفاظ على التوافق مع التعليمات البرمجية السابقة منذ الإصدار 1.0 وحتى الآن...لذلك، حتى في .net 4.0، لا يزال لديك خيار استخدام أي فئة غير عامة من 1.0 BCL إذا اخترت القيام بذلك.

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

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