Треки
/
C#
C#
/
Салабус
/
Рівність
Рі

Рівність у C#

5 вправ

Про концепцію Рівність

Вправа ілюструє низку властивостей рівності в C#:

Object.Equals()

Теми, які охоплює вправа

  • Прості типи, як-от рядки тексту (англ. string) і примітиви, зазвичай перевіряють на рівність за допомогою == і !=. Це вважають ідіоматичнішим, ніж використовувати метод 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), середовища розробки (IDE) зазвичай генерують перевантаження protected bool Equals(FacialFeatures other) для використання, коли задіяне успадкування. Похідний клас може викликати Equals() базового класу, а потім додати власну перевірку.
  • Не варто використовувати ==, якщо оператор == і метод Equals() у класі не перевантажені (див. вправу operator-overloading) або якщо важливо лише те, що посилання рівні.
  • Перевірки на рівність для структур struct розглядаються у вправі structs.
  • Рівність кортежів tuple розглядається у вправі tuples.
  • Багато розробників покладаються на свої IDE, щоб ті реалізували методи рівності, адже вони подбають про всі дрібниці рівності. Наприклад, JetBrains RIDER (v 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);
}
  • Варто бути обережним, якщо вирішено вдосконалити код, згенерований IDE. Такий код зазвичай надійний проти null і обʼєктів неправильного типу та досить добре оптимізований.
  • Перевірку рівності делегатів у вправі окремо не розглядають.
  • Для масивів і більшості колекцій вбудованих перевірок на рівність немає. 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).

Це заощадить певний обсяг перевірок типу для типів-посилань і крок упакування для типів-значень, адже їх не доведеться перетворювати на обʼєкт (упаковувати) як аргумент для 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 вправи, щоб практикувати концепцію Рівність