بر

برابری در C#

5 تمرین

درباره‌ی برابری

تمرین، شماری از ویژگی‌های برابری در C# را نشان می‌دهد:

Object.Equals()

موضوع‌های مطرح‌شده در تمرین

  • برابری انواع ساده (رشته‌ها و انواع اولیه) معمولاً با == و != Test می‌شود. این کار در مقایسه با استفاده از متد Equals() که برای این انواع هم در دسترس است، مرسوم‌تر به شمار می‌رود. برنامه‌نویسان جاوا هنگام کار با رشته‌ها باید به این نکته توجه داشته باشند که == در C# بر اساس مقدار مقایسه می‌کند، اما در جاوا، وقتی به زبان پیشین خود بازمی‌گردند، بر اساس ارجاع.
  • انواع ارجاعی (نمونه‌های کلاس‌ها) با متد Equals() که از object به ارث رسیده است مقایسه می‌شوند. اگر هدف شما از Test برابری این باشد که مطمئن شوید دو شیء دقیقاً همان نمونه‌ی یکسان‌اند، تکیه بر پیاده‌سازی object کافی است. در غیر این صورت باید object.Equals() را بازنویسی کنید.
  • اگر می‌دانید که همه‌ی نمونه‌های کلاس شما در یک جا ساخته می‌شوند، مثلاً شخصیت‌های یک بازی یا شبیه‌سازی، برابری ارجاعی کافی است. اما به احتمال زیاد نمونه‌های متعددی از یک موجودیت دنیای واقعی ساخته می‌شوند (برای مثال، از یک پایگاه داده، از ورودی کاربر، یا از طریق یک درخواست وب). در این حالت باید مقادیری که موجودیت را به‌طور یکتا مشخص می‌کنند از نظر برابری Test شوند. بنابراین باید Equals() را بازنویسی کنید.
  • یک Equals() بازنویسی‌شده شامل Testهای برابری روی اعضای انواع ساده با == و انواع ارجاعی با فراخوانی‌های بازگشتی 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() کلاس پایه را فراخوانی کند و سپس Test خودش را اضافه کند.
  • از == استفاده نکنید مگر اینکه عملگر == و همچنین متد Equals() را در کلاس خود سربارگذاری کرده باشید (به تمرین operator-overloading نگاه کنید)، یا اینکه فقط برابری ارجاع‌ها برایتان مهم باشد.
  • Testهای برابری در structs در تمرین structs بررسی می‌شوند.
  • برابری tuples در تمرین 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);
}
  • اگر تصمیم دارید Code تولیدشده توسط محیط توسعه‌ی یکپارچه‌ی خود را بهبود دهید، احتیاط کنید. آن Code عموماً در برابر مقادیر null و اشیای با نوع نادرست مقاوم و به‌قدر کافی بهینه است.
  • Testهای برابری نماینده‌ها در این تمرین به‌طور خاص بررسی نمی‌شوند.
  • برای آرایه‌ها و بیشتر مجموعه‌ها Test برابری توکاری وجود ندارد. LINQ (که در تمرین‌های بعدی بررسی می‌شود) متد SequenceEquals() را فراهم می‌کند، اما در نبود LINQ باید هر دو مجموعه را پیمایش کرد و عناصر را یکی‌یکی با هم مقایسه کرد.
  • برای بحث درباره‌ی نحوه‌ی استفاده از == و != با کلاس‌های خودتان، به تمرین operator-overloading نگاه کنید.

Object.GetHashCode()

  • object.GetHashCode() یک کد هش در قالب یک عدد صحیح ۳۲بیتی برمی‌گرداند. کد هش را کلاس‌های دیکشنری و مجموعه مانند Dictionary<T> و HashSet<T> برای ذخیره و بازیابی اشیا به شکلی کارا استفاده می‌کنند. در مورد دیکشنری‌ها، هش کردن به کلیدها مربوط می‌شود.
  • در میان توسعه‌دهندگان C# این انتظار وجود دارد که اگر Equals() را بازنویسی می‌کنید، GetHashCode() را هم بازنویسی کنید. بین Equals() و GetHashCode() رابطه‌ای هست که برای رفتار درست کلاس‌های دیکشنری و مجموعه‌ی هش و هر کلاس دیگری که از کد هش استفاده می‌کند باید برقرار بماند. انتظار می‌رود متد را طوری پیاده‌سازی کنید که برای نگهدارندگانی که ممکن است بعداً مجموعه‌ای مبتنی بر کد هش اضافه کنند، تله‌ای گذاشته نشود.
  • رابطه‌ی بین کد هش و برابری این است که اگر دو شیء برابر باشند (Equal() مقدار «درست» را برمی‌گرداند)، آنگاه GetHashCode() آن دو شیء باید مقدار یکسانی برگرداند. این رابطه در جهت معکوس برقرار نیست. متقارن نیست. یک تابع جست‌وجو را در نظر بگیرید که ابتدا بر اساس کد هش به یک «سبد» می‌رود و سپس عنصر خاص را با Test برابری بیرون می‌کشد.
  • ساده‌ترین راه ساختن کد هش این است که HashCode.Combine() را فراخوانی کنید و مقادیری را که در Test برابری استفاده شده‌اند (یا زیرمجموعه‌ای از آن‌ها) به آن بدهید. به یاد داشته باشید که هرچه اطلاعات بیشتری به Combine() بدهید، پیاده‌سازی هش احتمالاً کاراتر خواهد بود.
public class Assessment
{
    private int rating;
    private Person boss;

    public override int GetHashCode()
    {
        return HashCode.Combine(rating, boss);
    }
}
  • مقادیری که در Test برابری استفاده می‌شوند باید تا وقتی مجموعه‌ی هش‌شده در حال استفاده است پایدار بمانند. اگر شیئی را با یک مجموعه مقدار به مجموعه اضافه کنید و بعد آن مقادیر را تغییر دهید، کد هش دیگر به «سبد» درست اشاره نمی‌کند. در عمل این یعنی شیء باید تغییرناپذیر باشد. رویکردهای دیگر خطر ساختن دردسر برای نگهدارندگان را دارند. تغییرناپذیری در تمرین‌های دیگر بررسی می‌شود.
  • ممکن است بتوانید کد هشی بهتر از آنچه روتین‌های کتابخانه تولید می‌کنند طراحی کنید، اما این یا به این دلیل است که درکی دقیق از ویژگی‌های داده دارید، یا به این دلیل که مجموعه‌ای بسیار ساده است و مقادیرش را می‌توان بدون هش کردن مستقیماً استفاده کرد. ممکن است ارزش تلاش اضافی را نداشته باشد.

بهبود کارایی

برای بهبود جزئی کارایی، به‌ویژه وقتی اشیا به مجموعه‌ها تعلق دارند، می‌توانید یک عضو سربارگذاری‌شده‌ی public bool Equals(T other) اضافه کنید.

این کار مقداری بررسی نوع برای انواع ارجاعی را حذف می‌کند و برای انواع مقداری یک مرحله جعبه‌سازی را صرفه‌جویی می‌کند، چون دیگر نیازی نیست به‌عنوان آرگومان public override bool Equals(object other) به یک شیء تبدیل شوند (جعبه‌سازی شوند).

اگر رابط IEquatable<T> را به کلاس خود اضافه کنید، باید این سربارگذاری را پیاده‌سازی کنید. مگر اینکه Code شما روتین‌هایی داشته باشد که اشیایی از نوع IEquatable<T> می‌گیرند (و احتمالاً کار جالبی مربوط به برابری، مستقل از کلاس پیاده‌ساز، انجام می‌دهند)، واقعاً دلیل قانع‌کننده‌ی دیگری برای گنجاندن این رابط وجود ندارد.

IEqualityComparer<T>

اگر کلاسی دارید که می‌توان آن را به دو روش متفاوت به‌طور یکتا شناسایی کرد، مثلاً کلاس Person که یک SSID و یک نشانی ایمیل یکتا دارد، آنگاه .NET ابزاری فراهم می‌کند تا دو مجموعه‌ی متفاوت بتوانند از Testهای کد هش و برابری متفاوتی استفاده کنند. هر کدام می‌توانند پیاده‌سازی متفاوتی از IEqualityComparer<T> با متدهای Equals() و GetHashCode() خود داشته باشند. می‌توانید یک دیکشنری با کلید SSID و دیگری با کلید نشانی ایمیل داشته باشید.

جایی که IEqualityComparer<T> در کار است، معمولاً همچنان Equals() و GetHashCode() را روی کلاس عنصر خود بازنویسی می‌کنید تا از بروز مشکل بیرون از کلاس‌های مجموعه جلوگیری کنید.

هنگام استفاده از IEqualityComparer<T> یک نکته این است که متدهای خصوصی و مانند آن برای Test برابری در دسترس نخواهند بود.

اگر فقط یک مجموعه‌ی هش‌شده در کار باشد، ممکن است بهتر باشد از IEqualityComparer<T> پرهیز کنید و برابری و کد هش را در خود شیء کپسوله کنید. در وهله‌ی اول ایده‌آل نیست که وابستگی‌ای کلیدی مانند وابستگی بین شیء و مجموعه‌ی هش‌شده را نتوان با کامپایلر الزام کرد، اما اگر منطق هش و مقایسه جای دیگری باشد، برای نگهدارنده حتی آسان‌تر می‌شود که بدون توجه به پیامدهایش برای مجموعه، اعضای یک کلاس را تغییر دهد.

نکته‌ای درباره‌ی برابری اعداد اعشاری

یکی از انواع اولیه‌ای که می‌تواند برنامه‌نویس غافل را به چالش بکشد، Test کردنِ برابری مقادیر اعشاری است. این موضوع در سند about.md مفهوم floating-point-numbers بررسی می‌شود.

برابری و وراثت

این مقاله برخی از تصمیم‌هایی را نشان می‌دهد که باید درباره‌ی برابری و وراثت گرفته شوند. این‌ها تنها زمانی به کار شما می‌آیند که نمونه‌هایی از یک کلاس و یک کلاس مشتق‌شده را دستکاری می‌کنید و لازم است برابری بین آن دو را Test کنید.

ویرایش از طریق GitHub این پیوند در پنجره یا زبانه‌ی جدیدی باز می‌شود

برابری را یاد بگیرید

تمرین کردن قفل شده است

برای تمرین برابری قفل 4 تمرین دیگر را باز کنید