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!

 


Связанные темы