الفريق العربي للبرمجةأرشيف المنتديات · 2000 – 2023
نسخة أرشيفية للقراءة فقط — التسجيل والمشاركة مغلقان، والمحتوى محفوظ كما كان.

#Overriding Equality Operators in C

بدأه AliBazzi في 21 مارس 2010 · 2 رد · 813 مشاهدة · في قسم تكنولوجيا Microsoft .NET العام
مشاركة: واتساب X فيسبوك تيليجرام
#1 صاحب الموضوع

هوية أي Type يمكن أن تحدد من عدة أمور , قد يفرق Instance عن اّخر عن طريق Field يحتويه , قد نطلق عليه Identity ,وقد يتم تمييز Instance أيضا عن طريق عدة Fields ,ما أود أن أسرده في هذا المقال هو كيفية تعريف الـ Equality Operator في أي Type بالطريقة التي تحلو لك ,ولكن قبل أنا نبدأ فلنناقش الفكرة التالية :

لو عرفنا Type من نوع Class إسمه Student كما يلي :

class Student
 {
 public string name;
 public int age;
 }

ونفذنا البرنامج التالي :

Student stu1 = new Student() { name ="Ali",age = 21};
 Student stu2 = new Student() { name ="Ali",age = 21};
 Console.WriteLine(stu1.Equals(stu2));

ما النتيجة المتوقعة من تنفيذ السطر الثالث ؟ النتيجة هي False لأن الـ Default Equality Operator يقوم بمقارنة الـ References بين الـ Object Stu1 و Stu2 ,مع أنهم يحوون نفس المعلومات , فالمقارنة التي جرت هي على الـ Pointers لكل Object وبالتالي النتيجة منطقية بأنهما غير متساويين (فهم فعلا كذلك , من وجهة نظر الـ References )

والاّن , لنقوم بما يلي , سنغير تعريف الـ Student Type إلى Struct وسنقوم بتنفيذ نفس البرنامج السابق , النتيجة المذهلة التي ستظهر هي True ,بعد الإستغراب فلنحاول تفسير ما حدث , الـ Equality Operator المعرف على الـ ValueType Class والذي يمثل كل Struct في عالم الـ Net. معرف بطريقة مختلفة عن الـ Object , ما سيقوم به الـ Equality Operator في الـ Struct هو مقارنة كل Field من كلا الطرفين (الطرفين هنا هما الـ Two Objects من نفس النوع , الذي يقوم بمقارنتهم) , وفي حالتنا هذه سيقوم بمقارنة Two Fields هما الـ name , age وسيكتشف أنهم متساويين , وبالتالي ستكون النتيجة True .

سنقوم بتعقيد المثال قليلا , وسيكون لدينا نمط كالتالي :

struct Student
 {
 public string name;
 public int age;
 public Faculty faculty;
 }
 class Faculty
 {
 public string name;
 }

وعند تنفيذ الأسطر التالية:

Student stu1 = new Student() { name = "Ali", age = 21, faculty = new Faculty() { name = "IE" }};
 Student stu2 = new Student() { name ="Ali",age = 21 , faculty = new Faculty() { name = "IE" }};
 Console.WriteLine(stu1.Equals(stu2));

نتيجة السطر الثالث هي False مع أن الـ Student هم Struct ! , والسبب في ذلك أن الـ Faculty Field هو من Reference Type وبالتالي المقارنة حاصلة على الـ Pointers وليس المحتوى .

لو قمنا بتحويل Faculty Type إلى Struct فستكون النتيجة كما هو متوقع , True والسبب أصبح واضح ...

طيب , فقط قبل أن أنهي هذا المثال , لنفترض أن الـ Student هو Struct والـ Faculty هو Calss ونفذنا الأسطر التالية:

Faculty fac = new Faculty() { name = "IE" };
 Student stu1 = new Student() { name = "Ali", age = 21, faculty = fac};
 Student stu2 = new Student() { name ="Ali",age = 21 , faculty = fac};
 Console.WriteLine(stu1.Equals(stu2));

النتيجة هي True لأن الـ Stu1 و Stu2 يتشاركان في نفس الـ Faculty Object وبالتالي الـ Two Pointer متساويين .

والاّن سننتقل إلى الهدف الرئيسي من هذا المقال , كيف أقوم بتعريف Equality Operator خاص بالـ Type الذي أقوم بأنشائه , ببساطة كالتالي :

class Student
 {
 public string name;
 public int age;
 public Faculty faculty;
 public override bool Equals(object obj)
 {
 return obj is Student && (obj as Student).name == this.name && (obj as Student).age == this.age && (obj as Student).faculty.Equals(this.faculty);
 }
 }
 class Faculty
 {
 public string name;
 public override bool Equals(object obj)
 {
 return obj is Faculty && (obj as Faculty).name == this.name;
 }
 }

ما قمت به هو Override للـ Equals Method , وقمت بتضمين سلوكي الخاص في عملية المقارنة ,لاحظو أن أول شرط هو

obj is Student

هذا السطر مهم , لأن الـ Equals Method تقبل Object وبالتالي تستطيع تمرير أي Object ليتم المقارنة معه , ولكن أنا مهتم بالمقارنة مع أمثالي من الـ Objects وليس مع كل الـ Objects في العالم ...

عظيم , إذا عند تنفيذ البرنامج التالي , ستكون النتيجة True :

Student stu1 = new Student() { name = "Ali", age = 21, faculty = new Faculty() { name = "IE" }};
 Student stu2 = new Student() { name ="Ali",age = 21 , faculty = new Faculty() { name = "IE" }};
 Console.WriteLine(stu1.Equals(stu2));

ولكن هل ستكون نفس النتيجة عند تنفيذ السطر التالي ؟ :

Console.WriteLine(stu1==stu2);

للأسف لا , فالنتيجة ستكون False icon_e_sad.gif

التفسير : الـ == في الـ #C يختلف عن الـ Equals Method ولذلك , إذا أحببت أن يظهر الـ Type خاصتك نتائج منطقية عند كل أشكال المقارنة (بما فيها الـ Equals و ==) فعليك تعريف == operator في الـ Type خاصتك , وبالتالي سيكون الأصح كما يلي :

class Student
 {
 public string name;
 public int age;
 public Faculty faculty;
 public static bool operator ==(Student left, Student right)
 {
 return left.age == right.age && left.name == right.name && left.faculty == right.faculty;
 }
 public static bool operator !=(Student left, Student right)
 {
 return !(left == right);
 }
 public override bool Equals(object obj)
 {
 return obj is Student && (obj as Student).name == this.name && (obj as Student).age == this.age && (obj as Student).faculty.Equals(this.faculty);
 }
 }
 class Faculty
 {
 public string name;
 public override bool Equals(object obj)
 {
 return obj is Faculty && (obj as Faculty).name == this.name;
 }
 public static bool operator ==(Faculty left ,Faculty right)
 {
 return left.name == right.name;
 }
 public static bool operator !=(Faculty left, Faculty right)
 {
 return !(left == right);
 }
 }

ملاحظة: لا يمكن تعريف الـ == بدون الـ =! .

ولكن هناك أمور عدة قد تغاضينا عنها في هذا التصميم , وهي أمور هامة جدا نذكر منها :

1- تكرار الكود في أكثر من مكان . فالـ Equals و الـ == متشابهان تقريبا

2- تجاهلنا حالة أحد الطرفين أو كليهما null عند المقارنة , وسأعترف , بأن هذه الحالة هي أهم مسبب للجنون , إذا لم تقم بمعالجتها في تصميمك

لذلك سأطرح التصميم النهائي الذي تنصح به Microsoft و سأناقشه بتفاصيله :

class Student
 {
 public string name;
 public int age;
 public static bool operator ==(Student left, Student right)
 {
 if (left is Student)
 {
 return left.Equals(right);
 }
 if (right is Student)
 {
 return right.Equals(left);
 }
 return true;
 }
 public static bool operator !=(Student left, Student right)
 {
 return !(left == right);
 }
 public override bool Equals(object obj)
 {
 return obj is Student && (obj as Student).name == this.name && (obj as Student).age == this.age ;
 }
 public override int GetHashCode()
 {
 return this.name.GetHashCode() ^ this.age.GetHashCode();
 }
 }

أولا : قد يبدو ما كتب في الـ == operator غباء مطلق , لكن لو تمعنا قليلا لاكتشفنا أنه ذكاء مطلق

فنحن نقوم بإختبار left is Student أو right is Student مع إنه من غير المعقول تمرير شيء غير Student للـ == operator وبالتالي ما نقوم به هنا هو التحقق من شي نتيجته محسومة دوما للـ True , إلا حالة واحدة قد يظهر فيها False هي الـ null ,نعم الـ null حالة ممكنة جدا , فقد أقارن

stu1 == null

أو

stu2 == null

أو حتى

Student stu1 = null;
student stu2 = null;
Console.WriteLine(stu1 == stu2);

وبالتالي , قد شملنا هذه الحالات المؤلمة و الحرجة ,فإن كان الطرف الأول و هو الـ left لا يساوي الـ null سنقوم بطلب الـ Equals على الـ left ,لأننا ضمنا أنه object حقيقي و ليس null ,ونفس الشيء بالنسبة لحالة الـ right (ولا نستطيع الإستعاضة عن الـ left is Student بـ left !=null لسبب بسيط , Infinite Recursion )

ولكن إذا كان الطرفين null ووصلنا للسطر الأخير في الـ == سنقوم بإعادة true بكل بساطة , لأن الطرفين مؤكد أنهم null وبالتالي null == null وبالتالي فعلا true icon_e_biggrin.gif

ثانيا :

الـ Equals تحوي كود مألوف لنا ,ولكن هنا لم نتأكد من كل الأطراف , إن كانت Null أم لا , السبب بسيط لأنه من المستحيل تنفيذ ما تحويه الـ Equals إذا لم يكن هناك طرف واحد على الأقل .

ثالثا :

الـ GetHashCode مهم إعادة تعريفها عند إعادة تعريف الـ Equality Operators لأنها ستقوم بشكل إفتراضي بإعادة Hash من الـ Pointer الخاص بالـ Object (هذا هو الـ Default Implementation ) في الـ Net. , وبالتالي لا تريد أن ترى نتائج غريبة ,كمثال : تصور لو أن الـ Two Objects متساويين ولكن الـ HashCode مختلف فيما بينهما ...

كلام في الـ Struct :

بالنسبة للـ Struct فيفضل الإبتعاد عن إستعمال الـ Equals لأنها تحوي object في توقيعها , وبالتالي سيكون هناك عملية Boxing و Unboxing , وبالتالي يفضل الإبتعاد عنها (Performance Issues) وإستعمال عوض عنها الـ IEquatable<T> Interface التي تحوي Method وحيدة وهي Equals(T other):bool ,وبالتالي عند طلب Equals على الـ Object سيكون لها تفضيل على الـ Equals(object obj):bool ...

struct Student :IEquatable<Student>
 {
 public string name;
 public int age;
 public static bool operator ==(Student left, Student right)
 {
 if (left is Student)
 {
 return left.Equals(right);
 }
 if (right is Student)
 {
 return right.Equals(left);
 }
 return true;
 }
 public static bool operator !=(Student left, Student right)
 {
 return !(left == right);
 }
 public override int GetHashCode()
 {
 return this.name.GetHashCode() ^ this.age.GetHashCode();
 }
 public bool Equals(Student other)
 {
 return this.age == other.age && this.name == other.name;
 }
 }

أتمنى أن أكون قد أضأت على هذا الموضوع المهم , بإنتظار الأسئلة ...

تم تعديل هذه المشاركة بواسطة AliBazzi في 21 مارس 2010 في 16:10

#2

جزاك الله خيراً على هذه المقالة الرائعة , فقط تصحيح بسيط لعنوان الموضوع وهو Overloading equality operator وليس Overriding فالإجراء Equals هو الذي يتم عمل Overriding أو Overloading له بينما Equality Operator يتم عمل Overloading له فقط.

#3

أهلا بك أخي الكريم ...

عفوا ولكن الـ Equality operator يتم عمل Overriding له أيضا , لأنه يحوي Default Implementation أيضا مثله مثل الـ Object.Equals ...

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