C#. Порівняння екземплярів узагальнених типів

Порівняння екземплярів узагальнених типів. Інтерфейси IComparable<T> та IEquatable<T>


Зміст


Пошук на інших ресурсах:

1. Яким чином реалізувати порівняння екземплярів узагальненого типу T?

В узагальненому класі, який отримує параметром тип T, можна реалізувати порівняння екземплярів цього параметру T. Звичайне порівняння з допомогою оператора порівняння == (!=) буде викликати помилку компіляції. Це пояснюється тим, що параметр T відноситься до узагальненого типу, а тому компілятор не буде знати яким способом порівнювати два об’єкти. Оскільки, два об’єкти можуть порівнюватись різними способами. Наприклад, можна передбачати порівняння на основі значень декількох полів об’єктів або порівняння за одним полем.

Висновок: спосіб порівняння двох об’єктів узагальненого типу T потрібно закласти в деякому програмному коді, точніше спеціальному методі класу.

Для вирішення цієї проблеми у мові C# існують відповідні засоби. Щоб забезпечити коректне порівняння об’єктів параметру типу T в узагальненому класі, цей клас повинен реалізувати один з наступних інтерфейсів:

  • System.IComparable або System.IComparable<T>;
  • System.IEquatable<T>.

Вибір того чи іншого інтерфейсу визначається умовою задачі. Якщо потрібно порівнювати об’єкти типу T на більше, менше то тут підійде інтерфейс IComparable<T>. Таке порівняння потрібне, наприклад, при сортуванні.

Якщо потрібно порівнювати об’єкти типу T на рівність/нерівність, то тут можна скористатися інтерфейсом IEquatable<T>. До задач порівняння відносяться пошук елементу в масиві даних.

Згідно з синтаксисом мови C#, реалізація інтерфейсів передбачає реалізацію методів цих інтерфейсів. Клас успадковує (реалізує) відповідний інтерфейс та реалізує (перевизначає) методи цього інтерфейсу.

Базовим типам int, double, char тощо відповідають структури Int32, Double, Char та інші. У цих структурах вищезгадані інтерфейси є реалізовані а тому порівняння даних базових типів не викликає проблем.

 

2. Реалізація методу CompareTo() інтерфейсу IComparable в класах. Необхідність застосування. Пояснення

В інтерфейсах IComparable або IComparable<T> оголошується один єдиний метод CompareTo(). Саме цей метод повинен бути реалізований у класі, в якому потрібно порівнювати екземпляри узагальненого типу. Згідно з документацією, синтаксис оголошення методу наступний

int CompareTo(T other);

тут

  • T – узагальнений тип, що є параметром класу;
  • other – посилання на екземпляр типу T, яке порівнюється з поточним екземпляром класу.

Метод CompareTo() повинен повертати наступні значення:

  • <0 – якщо поточний екземпляр класу передує екземпляру other у випадку сортування. Іншими словами, поточний екземпляр менше екземпляру other;
  • =0 – якщо поточний екземпляр класу рівний (на тій самій позиції) екземпляру other;
  • >0 – якщо поточний екземпляр класу слідує після екземпляру other при сортуванні. Іншими словами, поточний екземпляр більше екземпляру other.

У метод CompareTo() можна закласти будь-яку логіку порівняння.

З врахуванням вищесказаного, оголошення узагальненого класу MyGenClass<T>, в якому потрібно порівнювати об’єкти типу T, може бути, наприклад, таким:

class MyGenClass<T> where T : IComparable<T>
{
  T obj; // поточний об’єкт типу T

  // ...
  // Метод, в якому порівнюються об’єкти типу T.
  // return_type - значення, яке повертає метод.
  return_type CompareMethod(T other)
  {
    // Викликається метод obj.CompareTo(other)
    if (obj.CompareTo(other)>0)
    {
      // Дії, якщо obj>other
      // ...
    }
    else
    if (obj.CompareTo(other)<0)
    {
      // Дії, якщо obj<other
      // ...
    }
    else
    {
      // Дії, якщо obj==other
      // ...
    }
  }
}

У вищенаведеному коді при оголошенні класу MyGenClass<T> вказується обмеження на тип T

where T : IComparable<T>

це означає, що тип T повинен реалізовувати інтерфейс IComparable<T>. Тобто, якщо типом T виступає деякий клас (структура), то цей клас повинен містити реалізацію методу CompareTo() згідно з домовленістю.

Типом T може бути будь-який клас чи структура який реалізує інтерфейс IComparable<T>. Скорочений код класу MyClass, який реалізує інтерфейс IComparable<T> може бути таким

class MyClass : IComparable<MyClass>
{
  // ...

  public int CompareTo(MyClass other)
  {
    // Реалізація логіки порівняння поточного екземпляру з other
    // ...
  }
}

Після вищенаведеної реалізації, клас MyClass може виступати в якості параметра типу T, який підходить для порівняння екземплярів в узагальнених класах.

 

3. Реалізація методу Equals() інтерфейсу IEquatable<T>

Щоб в узагальненому класі порівняти на рівність два об’єкти узагальненого типу T, потрібно щоб цей тип T (клас, структура) реалізовував інтерфейс System.IEquatable<T>. У цьому інтерфейсі оголошується один єдиний метод

bool Equals<T>(T other)

який повертає true, у випадку якщо поточний об’єкт рівний об’єкту other. Інакше метод повертає false.

З метою забезпечення сумісності з конкретною реалізацією методу Equals(), додатково тип T повинен перевизначати методи Equals() та GetHashCode() з класу Object (див. приклад нижче).

Якщо типом T є деякий клас з іменем MyClass, то приблизне оголошення цього класу наступне

class MyClass : IEquatable<MyClass>
{
  // ...

  // Реалізація методу Equals() з інтерфейсу IEquatable
  public bool Equals(MyClass other)
  {
    // Реалізація логіки порівняння поточних даних (об’єкту) з other
    // ...
  }

  // Перевизначення методу Equals() з класу Object
  public override bool Equals(object obj)
  {
    // Реалізація порівняння поточних даних з obj
    // ...
  }

  // Перевизначення методу GetHashCode() з класу Object
  public override int GetHashCode()
  {
    // ...
  }
}

Після вищенаведеного оголошення клас MyClass може бути використаний як аргумент типу, що передається в інший узагальнений клас, який здійснює порівняння об’єктів.

В свою чергу, інший узагальнений клас що отримує параметром тип T, повинен вказувати обмеження на цей тип у вигляді інтерфейсу IEquatable<T>.

Наприклад, якщо в узагальненому класі (структурі) з іменем MyGenClass<T> потрібно порівнювати два об’єкти типу T на рівність, то скорочений код оголошення такого класу з методом порівняння CompareMethod() може бути таким

class MyGenClass<T> where T : IEquatable<T>
{
  T obj; // поточний об’єкт типу T

  // ...

  // Метод, в якому порівнюються поточний об’єкт з іншим об’єктом.
  return_type CompareMethod(T other)
  {
    // Виклик методу Equals() для порівняння
    if (obj.Equals(other))
    {
      // Дії, які потрібно виконати у випадку коли obj рівне other
      // ...
    }
    else
    {
      // Дії, які потрібно виконати у випадку коли obj не рівне other
      // ...
    }
  }
}

 

4. Порівняння екземплярів класу Number в узагальненому класі. Приклад реалізації інтерфейсу IComparable<T>

У прикладі оголошується два класи:

  • узагальнений клас MyGenClass<T>. У цьому класі параметр типу T обмежується інтерфейсом IComparable<T>;
  • неузагальнений клас Number – який служить типом-заповнювачем в узагальненому класі MyGenClass<T>.
using System;

namespace ConsoleApp19
{
  // У класі параметр типу T обмежується інтерфейсом IComparable<T>.
  // Це означає, що тип T повинен реалізувати метод CompareTo() інтерфейсу IComparable<T>.
  class MyGenClass<T> where T : IComparable<T>
  {
    T obj; // деяке внутрішнє поле - екземпляр типу T

    // Конструктор
    public MyGenClass(T _obj)
    {
      obj = _obj;
    }

    // Властивість доступу до внутрішнього поля
    public T Obj
    {
      get { return obj; }
      set { obj = value; }
    }

    // Демонстраційний метод, що виводить результат порівняння
    // поточного значення obj з other.obj
    public void GreateThen(T other)
    {
      if (obj.CompareTo(other) > 0)
        Console.WriteLine("obj > other.obj");
      else
      if (obj.CompareTo(other) < 0)
        Console.WriteLine("obj < other.obj");
      else
        Console.WriteLine("obj == other.obj");
    }
  }

  // Клас, що підходить для порівняння екземплярів в узагальнених класах.
  // Клас визначає деяке число. Клас реалізує інтерфейс IComparable<T>
  class Number : IComparable<Number>
  {
    // Внутрішнє поле
    double num = 0;

    // Конструктор
    public Number(double num)
    {
      this.num = num;
    }

    // Властивість доступу до внутрішнього поля
    public double Num
    {
      get { return num; }
      set { num = value; }
    }

    // Реалізація методу CompareTo() з інтерфейсу IComparable<Number>
    public int CompareTo(Number other)
    {
      if (this.num > other.num)
        return 1;
      if (this.num < other.num)
        return -1;
      return 0;
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      // 1. Клас Number. Створити та порівняти 2 екземпляри типу Number
      Number num1 = new Number(33);
      Number num2 = new Number(40);
      Console.WriteLine("num1.CompareTo(num2) == {0}", num1.CompareTo(num2));

      // 2. Створення екземпляру узагальненого класу з типом-заповнювачем int
      MyGenClass<int> obj1 = new MyGenClass<int>(45);
      MyGenClass<int> obj2 = new MyGenClass<int>(37);

      // Викликати метод порівняння для типу-заповнювача int
      obj1.GreateThen(39);
      obj2.GreateThen(100);

      // 3. Створення екземпляру узагальненого класу з типом-заповнювачем Number
      Number num3 = new Number(23.8);
      Number num4 = new Number(23.8);
      MyGenClass<Number> obj3 = new MyGenClass<Number>(num3);

      // Викликати метод порівняння
      obj3.GreateThen(num4); // obj == other.obj

      Console.WriteLine("Ok!");
      Console.ReadKey();
    }
  }
}

 

5. Порівняння екземплярів класу Number в узагальненій структурі. Приклад реалізації інтерфейсу IEquatable<T>

Даний приклад демонструє порівняння об’єктів типу T в узагальнених структурах. Оголошуються наступні елементи:

  • узагальнена структура MyGenStruct<T>, в якій параметр типу T обмежується інтерфейсом IEquatable<T>;
  • клас Number, що реалізує інтерфейс IEquatable<T>. У функції main() клас передається в структуру MyGenStruct<T> в якості аргументу типу.

 

using System;

namespace ConsoleApp19
{
  // Структура, в якій параметр типу T реалізує інтерфейс IEquatable<T>
  struct MyGenStruct<T> where T : IEquatable<T>
  {
    T obj; // внутрішній об'єкт

    // Конструктор
    public MyGenStruct(T _obj)
    {
      obj = _obj;
    }

    // Властивість доступу до поля obj
    public T Obj
    {
      get { return obj; }
      set { obj = value; }
    }

    // Демонстраційний метод, в якому порівнюються об'єкти типу T
    public void CompareMethod(T other)
    {
      // Виклик методу Equals(), реалізованого в типі T
      if (obj.Equals(other))
      {
        Console.WriteLine("obj == other.obj");
      }
      else
      {
        Console.WriteLine("obj != other.obj");
      }
    }
  }

  // Клас, що служить типом-заповнювачем
  class Number : IEquatable<Number>
  {
    // Внутрішнє поле
    double num;

    // Конструктор
    public Number(double _num)
    {
      num = _num;
    }

    // Властивість доступу до поля num
    public double Num
    {
      get { return num; }
      set { num = value; }
    }

    // Реалізація методу Equals() з інтерфейсу IEquatable<T>
    public bool Equals(Number other)
    {
      return num == other.num;
    }

    // Перевизначення методу Equals(Object) з класу Object
    public override bool Equals(object obj)
    {
      // Реалізація порівняння поточних даних з obj
      if (obj is Number)
        return Equals((Number)obj);
      return false;
    }

    // Перевизначення методу GetHashCode() з класу Object
    public override int GetHashCode()
    {
      return num.GetHashCode();
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      // Оголосити два числа
      Number num1 = new Number(2.8);
      Number num2 = new Number(3.5);

      // Оголосити екземпляр класу MyGenClass<T>
      MyGenStruct<Number> obj1 = new MyGenStruct<Number>(num1);

      // Порівняти дані екземпляру з num2
      obj1.CompareMethod(num2);

      Console.WriteLine("Ok!");
      Console.ReadKey();
    }
  }
}

Результат виконання програми

obj != other.obj
Ok!

 

6. Порівняння об’єктів узагальненого типу T в узагальнених методах

Якщо в деякому неузагальненому класі (структурі) реалізовано узагальнений метод, що отримує параметром тип T, то в цьому методі також можна здійснювати порівняння об’єктів типу T. У цьому випадку до методу пред’являються ті самі вимоги що й до класу чи структури. Узагальнений метод обов’язково повинен вказати обмеження на параметр типу T. Обмеженнями мають бути один з інтерфейсів IComparable<T> та IEquatable<T>.

Крім того, сам тип-параметр T повинен реалізовувати один з інтерфейсів IComparable<T> чи IEquatable<T>.

Синтаксис оголошення методу, в якому потрібно порівнювати об’єкти типу T може бути приблизно наступним

return_type MethodName<T>(parameters)
    where T : IComparable<T>
    where T : IEquatable<T>
{
    // Дії, які потрібно виконати,
    // тут можна викликати методи CompareTo() або Equals()
    // в об’єкті типу T
    // ...
}

тут

  • MethodName – ім’я методу;
  • T – параметр типу, який реалізує інтерфейси IComparable<T> або IEquatable<T>. Тобто в цьому типі реалізовано метод CompareTo();
  • return_type – тип, який повертає метод. Тут може фігурувати також тип T;
  • parameters – параметри, які отримує метод.

У вищенаведеному коді одне з двох обмежень на тип T

...
where T : IComparable<T>
where T : IEquatable<T>
...

може бути відсутнім. Це все залежить від умови задачі.

Узагальнений метод може бути як методом екземпляру так і статичним методом.

 

7. Приклад сортування екземплярів типу Point<T>. Реалізація інтерфейсів IComparable<T> та IEquatable<T> в узагальненому статичному методі

У прикладі оголошуються два класи:

  • клас Point – описує точку на координатній площині. Клас реалізує інтерфейси IComparable<Point> та IEquatable<Point> для того, щоб можна було порівнювати об’єкти цього класу;
  • клас SortMethods, в якому реалізовано узагальнений статичний метод SortInsertion<T>(). Метод реалізує алгоритм сортування вставками для сортування екземплярів класу Point.

 

using System;

namespace ConsoleApp19
{
  // Клас, що описує точку на координатній площині.
  // Для забезпечення можливості порівняння, клас реалізує
  // інтерфейси IComparable<T> та IEquatable<T>
  class Point : IComparable<Point>, IEquatable<Point>
  {
    // Внутрішні поля - координати точки
    private double x, y;

    // Конструктор
    public Point(double x, double y)
    {
      this.x = x;
      this.y = y;
    }

    // Властивість LengthOrigin() - повертає відстань від поточної точки до початку координат
    public double LengthOrigin
    {
      get { return Math.Sqrt(x * x + y * y); }
    }

    // Реалізація методу CompareTo() інтерфейсу IComparable<Point>.
    // Порівнюється відстань від точки до початку координат:
    // - якщо відстань для поточного екземпляру більше за відстань pt, повертається 1;
    // - якщо відстань для поточного екземпляру менше за відстань pt, повертається -1;
    // - якщо відстані рівні, повертається 0.
    public int CompareTo(Point pt)
    {
      if (LengthOrigin > pt.LengthOrigin)
        return 1;
      if (LengthOrigin < pt.LengthOrigin)
        return -1;
      return 0;
    }

    // Реалізація методу Equals() інтерфейсу IComparable<T>
    public bool Equals(Point pt)
    {
      return LengthOrigin == pt.LengthOrigin;
    }

    // Перевизначення методу Equals() класу Object,
    // потрібне для сумісності з новим методом Equals(Point)
    public override bool Equals(object obj)
    {
      if (obj is Point)
        return Equals((Point)obj);
      return false;
    }

    // Перевизначити метод GetHashCode(),
    // необхідно для сумісності з новим методом Equals(Point)
    public override int GetHashCode()
    {
      return base.GetHashCode();
    }

    // Перевизначити метод ToString() класу Object
    public override string ToString()
    {
      return base.ToString();
    }

    // Метод, що виводить інформацію про поточну точку.
    // Метод використовується для тестування.
    public void ShowPoint()
    {
      // Вивести координати поточної точки та відстань до початку координат
      Console.WriteLine("({0}, {1}) => {2:f2}", x, y, LengthOrigin);
    }
  }

  // Клас, що містить узагальнений статичний метод сортування вставками об'єктів узагальненого типу T
  class SortMethods
  {
    // Статичний метод сортування вставками.
    // Параметри:
    // - array - масив, який потрібно відсортувати у порядку зростання.
    // Метод повертає відсортований масив T[].
    public static T[] SortInsertion<T>(T[] array)
      where T : IComparable<T>
    {
      for (int i = 0; i < array.Length - 1; i++)
        for (int j = i; j >= 0; j--)
          if (array[j].CompareTo(array[j + 1]) > 0)
          {
            T item = array[j];
            array[j] = array[j + 1];
            array[j + 1] = item;
          }
      return array;
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      Point pt1 = new Point(2, 4);

      // Створити масив точок
      Point[] AP =
      {
        new Point(3,5),
        new Point(2,7),
        new Point(4,6),
        new Point(-3,1)
      };

      // Вивести список точок до сортування
      foreach (Point p in AP)
        p.ShowPoint();

      // Відстортувати масив, викликати статичний метод SortInsertion
      Point[] AP2 = SortMethods.SortInsertion<Point>(AP);

      // Вивести список точок після сортування
      Console.WriteLine("--------------");
      Console.WriteLine("After sorting:");

      foreach (Point p in AP2)
        p.ShowPoint();

      Console.WriteLine("Ok!");
      Console.ReadKey();
    }
  }
}

Результат виконання програми

(3, 5) => 5.83
(2, 7) => 7.28
(4, 6) => 7.21
(-3, 1) => 3.16
--------------
After sorting:
(-3, 1) => 3.16
(3, 5) => 5.83
(4, 6) => 7.21
(2, 7) => 7.28
Ok!

 


Споріднені теми