المسارات
/
C#
C#
/
المنهج
/
المساواة
ال

المساواة في C#

5 تمارين

نبذة عن المساواة

يوضّح التمرين البرمجي عددًا من خصائص التساوي في C#:

Object.Equals()

المواضيع التي يغطيها التمرين البرمجي

  • تُختبر الأنواع البسيطة (السلاسل النصية والأنواع الأولية) عادةً للتساوي باستخدام == و !=، ويُعدّ هذا أكثر شيوعًا في الاستخدام من استعمال طريقة Equals() المتاحة أيضًا مع هذه الأنواع. وينبغي لمبرمجي Java، عند التعامل مع السلاسل النصية، أن يكونوا منتبهين إلى أن == يقارن بالقيمة في C#، لكنه يقارن بالمرجع في Java عند عودتهم إلى لغتهم السابقة.
  • تُقارن الأنواع المرجعية (نسخ الأصناف) باستخدام طريقة Equals() الموروثة من object. وإذا كان هدفك من اختبار التساوي هو التأكد من أن كائنين هما نفس النسخة تمامًا، فسيكفي الاعتماد على تطبيق object. وإن لم يكن كذلك، فستحتاج إلى تجاوز object.Equals().
  • إذا كنت تعلم أن جميع نسخ صنفك تُنشأ في مكان واحد، كالشخصيات في لعبة أو محاكاة ما، فسيكفي تساوي المراجع. لكن من المرجّح أن تُنشأ نسخ متعددة للكيان الواحد نفسه في الواقع (مثلًا من قاعدة بيانات، أو من إدخال المستخدم، أو عبر طلب ويب). وفي هذه الحالة، تجب مقارنة القيم التي تميّز الكيان بشكل فريد. لذلك يجب تجاوز Equals().
  • يحتوي Equals() المتجاوَز على اختبارات تساوٍ لأعضاء من أنواع بسيطة باستخدام ==، ولأنواع مرجعية عبر استدعاءات متكررة لـ Equals().
class StatusBar
{
    private readonly int width = 200, height = 20;

    public override bool Equals(object other)
    {
        // ... null and type checks and performance optimisations
        return width == (other as StatusBar).width && height == (other as StatusBar).height;
    }
}
class Window
{
    private readonly string title = "Main";
    private readonly StatusBar statusBar = new StatusBar();

    public override bool Equals(object other)
    {
        // ... null and type checks and performance optimisations
        return title == (other as Window).title && statusBar.Equals((other as Window).statusBar);
    }
}

  • تُستخدم الطريقة الساكنة object.ReferenceEquals() لمقارنة كائنين والكشف عمّا إذا كانا نفس النسخة الواحدة. وهذا يمنح وضوحًا، وهو أمر ضروري حيث يكون قد جُوّز Equals() و == أو حُمّلا تحميلًا زائدًا.
var winA = new Window(); // above code shows that all windows are equal
var winB = new Window();
ReferenceEquals(winA, winB);
// => false
var winC = winA;
ReferenceEquals(winA, winC);
// => true

مواضيع ثانوية

  • بالإضافة إلى public override bool Equals(object obj)، تولّد بيئات التطوير المتكاملة عادةً التحميل الزائد protected bool Equals(FacialFeatures other) لاستخدامه عند وجود وراثة. ويمكن للصنف المشتق أن يستدعي Equals() في الصنف الأساس ثم يضيف اختباره الخاص.
  • لا تستخدم == إلا إذا كنت قد حمّلت العامل == تحميلًا زائدًا، وكذلك طريقة Equals() في صنفك (انظر تمرين operator-overloading)، أو كنت تهتم فقط بأن المراجع متساوية.
  • تُعالَج اختبارات التساوي في أنواع struct في تمرين structs.
  • تُعالَج مساواة أنواع tuple في تمرين tuples.
  • يعتمد كثير من المطوّرين على بيئات التطوير المتكاملة لديهم لتوفير تطبيق لطرق التساوي، لأنها تتولّى كل تفاصيل التساوي الدقيقة. وعلى سبيل المثال، يولّد RIDER من JetBrains (الإصدار 2020.1) طرق التساوي التالية لصنف:
protected bool Equals(T other)
{
    return field1 == other.field1 && field2.Equals(other.field2);
}

public override bool Equals(object obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    if (obj.GetType() != this.GetType()) return false;
    return Equals((T) obj);
}
  • كن حذرًا إذا قرّرت تحسين الكود الذي تولّده بيئة التطوير لديك. فهذا الكود محصّن عمومًا ضد القيم الفارغة والكائنات خاطئة النوع، ومُحسَّن إلى حد جيد.
  • لا يناقش التمرين تحديدًا اختبارات تساوي المفوّضات.
  • لا توجد اختبارات تساوٍ مدمجة للمصفوفات ولا لمعظم المجموعات. وتوفّر LINQ (التي تُناقش في تمارين لاحقة) SequenceEquals()، لكن في غياب LINQ يصبح الأمر مسألة المرور على المجموعتين معًا ومقارنة العناصر واحدًا واحدًا.
  • لمناقشة كيفية استخدام == و != مع أصنافك الخاصة، انظر تمرين operator-overloading.

Object.GetHashCode()

  • تُرجع object.GetHashCode() رمز تجزئة على هيئة عدد صحيح بطول 32 بت. وتستخدم أصناف القواميس والمجموعات، مثل Dictionary<T> و HashSet<T>، رمز التجزئة لتخزين الكائنات واسترجاعها بأداء عالٍ. وفي حالة القواميس تتعلق التجزئة بالمفاتيح.
  • هناك توقّع لدى مطوّري C# بأنك إذا تجاوزت Equals() فستتجاوز GetHashCode() أيضًا. وهناك علاقة بين Equals() و GetHashCode() يجب أن تظل صحيحة ليعمل سلوك أصناف القواميس ومجموعات التجزئة وأي أصناف أخرى تستخدم رمز تجزئة بشكل صحيح. ويُتوقّع منك أن تطبّق الطريقة بحيث لا تنصبّ أي فخاخ للقائمين على الصيانة ممن قد يضيفون مجموعة قائمة على رمز التجزئة في وقت لاحق.
  • العلاقة بين رمز التجزئة والتساوي أنه إذا كان كائنان متساويين (Equal() تُرجع صحيحًا)، فيجب أن تُرجع GetHashCode() للكائنين القيمة نفسها. ولا ينطبق هذا في الاتجاه المعاكس، فهو ليس متماثلًا. تخيّل دالة بحث تذهب أولًا إلى «دلو» استنادًا إلى رمز التجزئة، ثم تختار العنصر المعيّن باستخدام اختبار التساوي.
  • أسهل أسلوب لإنشاء رمز تجزئة هو استدعاء HashCode.Combine() وتمرير القيم المستخدمة في اختبار التساوي (أو مجموعة فرعية منها). وضع في اعتبارك أنه كلما زادت المعلومات التي تمرّرها إلى Combine()، كان تطبيق التجزئة أسرع أداءً على الأرجح.
public class Assessment
{
    private int rating;
    private Person boss;

    public override int GetHashCode()
    {
        return HashCode.Combine(rating, boss);
    }
}
  • يجب أن تبقى القيم المستخدمة في اختبار التساوي ثابتة طوال فترة استخدام المجموعة المجزّأة. فإذا أضفت كائنًا إلى المجموعة بقيم معيّنة ثم غيّرت تلك القيم، فلن يشير رمز التجزئة بعد ذلك إلى «الدلو» الصحيح. وعمليًا، يعني هذا أنه ينبغي أن يكون الكائن غير قابل للتغيير. أما الأساليب الأخرى فتحمل خطر خلق مفاجآت غير سارة للقائمين على الصيانة. وتُناقش عدم القابلية للتغيير في تمارين أخرى.
  • من الممكن أن تصمّم رمز تجزئة أفضل من الذي تنتجه إجراءات المكتبة، لكن إما لأن لديك فهمًا مفصلًا لخصائص البيانات، أو لأن المجموعة بسيطة جدًا بحيث يمكن استخدام القيم مباشرة دون تجزئة. وقد لا يستحق الأمر الجهد الإضافي.

تحسينات الأداء

لتحسين الأداء قليلًا، خصوصًا عندما تنتمي الكائنات إلى مجموعات، يمكنك إضافة عضو محمّل تحميلًا زائدًا public bool Equals(T other).

سوف يوفّر هذا قدرًا من فحص الأنواع للأنواع المرجعية، ويوفّر خطوة تغليف (boxing) للأنواع القيمية، إذ لن تحتاج إلى تحويلها إلى كائن (تغليفها) كوسيط لـ public override bool Equals(object other).

إذا أضفت الواجهة IEquatable<T> إلى صنفك، فسيستلزم ذلك تطبيق التحميل الزائد. وما لم يكن الكود لديك يحتوي على إجراءات تأخذ كائنات من النوع IEquatable<T> (ويُفترض أنها تفعل شيئًا مهمًا يتعلق بالتساوي بصرف النظر عن الصنف المطبِّق)، فلا يوجد في الحقيقة سبب مُقنِع آخر لتضمين الواجهة.

IEqualityComparer<T>

إذا كان لديك صنف يمكن التعرف عليه بشكل فريد بأسلوبين مختلفين، كصنف Person له SSID وعنوان بريد إلكتروني فريد، فإن .NET يوفّر وسيلة تتيح لمجموعتين مختلفتين استخدام اختبارات مختلفة لرمز التجزئة والتساوي. ويمكن لكل منهما أن يكون له تطبيق مختلف لـ IEqualityComparer<T> بطريقة Equals() وطريقة GetHashCode() خاصتين به. ويمكن أن يكون لديك قاموس مفتاحه SSID وآخر مفتاحه عنوان البريد الإلكتروني.

وحين يكون IEqualityComparer<T> قيد الاستخدام، فستظل عادةً بحاجة إلى تجاوز Equals() و GetHashCode() في صنف العنصر لديك لتجنّب مشكلات خارج أصناف المجموعات.

ومن الاعتبارات عند استخدام IEqualityComparer<T> أن الطرق الخاصة وما شابهها لن تكون متاحة لاختبار التساوي.

وإذا كانت هناك مجموعة مجزّأة واحدة فقط قيد الاستخدام، فقد يكون من الأفضل تجنّب IEqualityComparer<T> وتغليف التساوي ورمز التجزئة داخل الكائن نفسه. وليس مثاليًا أصلًا ألا يتمكّن المترجم من فرض تبعية أساسية كالتي بين الكائن والمجموعة المجزّأة، لكن مع وجود منطق التجزئة والمقارنة في مكان آخر سيصبح من الأسهل على القائم بالصيانة إجراء تغييرات على أعضاء صنف دون اعتبار لعواقب ذلك على المجموعة.

ملاحظة حول تساوي قيم الفاصلة العائمة

من الأمور التي قد تربك المبرمج غير الحذر اختبار تساوي قيم الفاصلة العائمة. وتُناقش هذه المسألة في مستند about.md لمفهوم floating-point-numbers.

التساوي والوراثة

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

تعديل عبر GitHub يفتح الرابط في نافذة أو علامة تبويب جديدة

تعلّم المساواة

التدريب مقفل

افتح 4 تمارين إضافية لممارسة المساواة