C#. Comparison of instances of generic types

Comparison of instances of generic types. IComparable<T> and IEquatable<T> interfaces

In this topic, you will learn how to properly compare instances of generic types with each other.


Contents


Search other resources:

1. How to implement comparison of instances of generic type T?

In a generic class that receives a type T as a parameter, you can implement a comparison of instances of this parameter T. A normal comparison using the == (!=) comparison operator will throw a compilation error. This is because the T parameter is of a generic type, and therefore the compiler will not know how to compare the two objects. Because two objects can be compared in different ways. For example, you might consider comparing based on the values of multiple object fields, or comparing on a single field.

Conclusion: the way to compare two objects of the generic type T must be laid down in some program code, more precisely in a special class method.

To solve this problem, there are corresponding tools in the C# language. To ensure correct comparison of objects of type parameter T in a generic class, the class must implement one of the following interfaces:

  • System.IComparable or System.IComparable<T>;
  • System.IEquatable<T>.

The choice of this or that interface is determined by the condition of the problem. If you need to compare objects of type T by more or less, then the IComparable<T> interface will do. Such a comparison is necessary, for example, when sorting.

If you need to compare objects of type T for equality/inequality, you can use the IEquatable<T> interface. Comparison tasks include searching for an element in a data array.

According to the syntax of the C# language, the implementation of interfaces implies the implementation of the methods of those interfaces. The class inherits (implements) the corresponding interface and implements (overrides) the methods of this interface.

Basic types int, double, char, etc. the structures Int32, Double, Char and others correspond. In these structures, the above interfaces are implemented, so comparing the data of the base types does not cause problems.

 

2. Implementation of the CompareTo() method of the IComparable interface in classes. The need for application. Explanation

The IComparable or IComparable<T> interfaces declare a single CompareTo() method. It is this method that must be implemented in the class in which you want to compare instances of the generic type. According to the documentation, the syntax for declaring a method is as follows

int CompareTo(T other);

here

  • T – generic type, which is a class parameter;
  • other – a reference to an instance of type T, which is compared to the current instance of the class.

The CompareTo() method must return the following values:

  • <0 – if the current instance of the class precedes the instance of other in the case of sorting. In other words, the current instance is smaller than the other;
  • =0 – if the current instance of the class is equal (at the same position) to the instance of other;
  • >0 – if the current instance of the class follows after the instance of other when sorting. In other words, the current instance is larger than the other instance.

Any comparison logic can be put into the CompareTo() method.

In view of the above, the declaration of the generic class MyGenClass<T>, in which you need to compare objects of type T, can be, for example, like this:

class MyGenClass<T> where T : IComparable<T>
{
  T obj; // current object of type T

  // ...
  // A method that compares objects of type T.
  // return_type is the value returned by the method.
  return_type CompareMethod(T other)
  {
    // The obj.CompareTo(other) method is called
    if (obj.CompareTo(other)>0)
    {
      // Actions, if obj>other
      // ...
    }
    else
    if (obj.CompareTo(other)<0)
    {
      // Actions, if obj<other
      // ...
    }
    else
    {
      // Actions, if obj==other
      // ...
    }
  }
}

In the above code, when declaring the class MyGenClass<T>, a constraint on the type T is specified

where T : IComparable<T>

this means that the type T must implement the IComparable<T> interface. That is, if the type T is a class (structure), then this class must contain the implementation of the CompareTo() method as agreed.

The type T can be any class or structure that implements the IComparable<T> interface.

The abbreviated code of the class MyClass that implements the IComparable<T> interface can be as follows

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

  public int CompareTo(MyClass other)
  {
    // Implementing the logic for comparing the current instance with other
    // ...
  }
}

After the above implementation, the MyClass class can act as a parameter of type T, which is suitable for comparing instances in generic classes.

 

3. Implementation of the Equals() method of the IEquatable<T> interface

To compare two objects of generic type T for equality in a generic class, this type T (class, structure) must implement the System.IEquatable<T> interface. This interface declares one single method

bool Equals<T>(T other)

which returns true if the current object is equal to the other object. Otherwise, the method returns false.

In order to ensure compatibility with a concrete implementation of the Equals() method, in addition, the T type must override the Equals() and GetHashCode() methods from the Object class (see the example below).

If the type T is some class named MyClass, then the approximate declaration of this class is as follows

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

  // implementation of the method Equals() of the interface IEquatable
  public bool Equals(MyClass other)
  {
    // Implementation of the logic for comparing the current data (object) with other
    // ...
  }

  // Overriding the Equals() method from the Object class
  public override bool Equals(object obj)
  {
    // Implementation of comparing current data with obj
    // ...
  }

  // Overriding the GetHashCode() method from the Object class
  public override int GetHashCode()
  {
    // ...
  }
}

After the above declaration, the MyClass class can be used as a type argument, which is passed to another generic class that compares objects.

In turn, another generic class that takes a type T as a parameter must specify a constraint on this type in the form of the IEquatable<T> interface.

For example, if in a generic class (structure) named MyGenClass<T> you need to compare two objects of type T for equality, then the shorthand code for declaring such a class with the CompareMethod() comparison method can be as follows

class MyGenClass<T> where T : IEquatable<T>
{
  T obj; // the current object of type T

  // ...

  // A method that compares the current object to another object.
  return_type CompareMethod(T other)
  {
    // Calling Equals() method for comparison
    if (obj.Equals(other))
    {
      // Actions to be performed if obj is equal to other
      // ...
    }
    else
    {
      // Actions to be performed if obj is not equal to other
      // ...
    }
  }
}

 

4. Comparison of instances of the Number class in a generic class. An example implementation of the IComparable<T> interface

The example declares two classes:

  • generic class MyGenClass<T>. In this class, the type parameter T is constrained by the IComparable<T> interface;
  • the non-generic Number class — serving as a placeholder type in the generic class MyGenClass<T>.

 

using System;

namespace ConsoleApp19
{
  // In a class, a parameter of type T is limited to the IComparable<T> interface.
  // This means that the T type must implement the CompareTo() method of the IComparable<T> interface.

  class MyGenClass<T> where T : IComparable<T>
  {
    T obj; // internal field - the instance of type T

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

    // Properties to access to the internal field
    public T Obj
    {
      get { return obj; }
      set { obj = value; }
    }

    // Demo method that outputs the result of comparing
    // the current value of obj with 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");
    }
  }

  // A class suitable for comparing instances in generic classes.
  // The class defines a certain number. The class implements the IComparable<T> interface
  class Number : IComparable<Number>
  {
    // Internal field
    double num = 0;

    // Constructor
    public Number(double num)
    {
      this.num = num;
    }

    // Inner field access property
    public double Num
    {
      get { return num; }
      set { num = value; }
    }

    // Implementation of the CompareTo() method from the IComparable<Number> interface
    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. Class Number. Create and compare 2 instances of type Number
      Number num1 = new Number(33);
      Number num2 = new Number(40);
      Console.WriteLine("num1.CompareTo(num2) == {0}", num1.CompareTo(num2));

      // 2. Instantiating a generic class with a placeholder int
      MyGenClass<int> obj1 = new MyGenClass<int>(45);
      MyGenClass<int> obj2 = new MyGenClass<int>(37);

      // Call a comparison method for the int placeholder
      obj1.GreateThen(39);
      obj2.GreateThen(100);

      // 3. Instantiating a generic class with the Number placeholder type
      Number num3 = new Number(23.8);
      Number num4 = new Number(23.8);
      MyGenClass<Number> obj3 = new MyGenClass<Number>(num3);

      // Call the comparison method
      obj3.GreateThen(num4); // obj == other.obj

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

 

5. Comparison of instances of the Number class in a generic structure. An example of implementing an interface IEquatable<T>

This example demonstrates a comparison of objects of type T in generic structures. The following elements are declared:

  • the generic structure MyGenStruct<T>, in which the parameter of type T is bounded to the IEquatable<T> interface;
  • a Number class that implements the IEquatable<T> interface. In the main() function, the class is passed to the MyGenStruct<T> structure as a type argument.

 

using System;

namespace ConsoleApp19
{
  // A structure in which a parameter of type T implements the IEquatable<T> interface
  struct MyGenStruct<T> where T : IEquatable<T>
  {
    T obj; // internal object

    // Constructor
    public MyGenStruct(T _obj)
    {
      obj = _obj;
    }

    // The property to access to obj field
    public T Obj
    {
      get { return obj; }
      set { obj = value; }
    }

    // Demo method that compares objects of type T
    public void CompareMethod(T other)
    {
      // Calling the Equals() method implemented in type T
      if (obj.Equals(other))
      {
        Console.WriteLine("obj == other.obj");
      }
      else
      {
        Console.WriteLine("obj != other.obj");
      }
    }
  }

  // A class serving as a placeholder type
  class Number : IEquatable<Number>
  {
    // Internal field
    double num;

    // Constructor
    public Number(double _num)
    {
      num = _num;
    }

    // The property to access to the num field
    public double Num
    {
      get { return num; }
      set { num = value; }
    }

    // Implementation of Equals() method from IEquatable<T> interface
    public bool Equals(Number other)
    {
      return num == other.num;
    }

    // Overriding the Equals(Object) method from the Object class
    public override bool Equals(object obj)
    {
      // Comparison of current data with the obj
      if (obj is Number)
        return Equals((Number)obj);
      return false;
    }

    // Overriding the GetHashCode() method from the Object class
    public override int GetHashCode()
    {
      return num.GetHashCode();
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      // Declare two numbers
      Number num1 = new Number(2.8);
      Number num2 = new Number(3.5);

      // Declare an instance of the class MyGenClass<T>
      MyGenStruct<Number> obj1 = new MyGenStruct<Number>(num1);

      // Compare instance data with num2
      obj1.CompareMethod(num2);

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

Program execution result

obj != other.obj
Ok!

 

6. Comparing objects of generic type T in generic methods

If in some non-generalized class (structure) a generalized method is implemented that receives the type T as a parameter, then in this method it is possible to compare objects of type T. In this case, the requirements for a method are the same as for a class or structure. A generic method must specify constraints on a parameter of type T. Constraints must be one of the IComparable<T> or IEquatable<T> interfaces.

In addition, the type-parameter T itself must implement one of the interfaces IComparable<T> or IEquatable<T>.

The syntax for declaring a method in which you want to compare objects of type T can be something like the following

return_type MethodName<T>(parameters)
    where T : IComparable<T>
    where T : IEquatable<T>
{
    // Actions to be taken
    // here you can call the CompareTo() or Equals() methods
    // in an object of type T
    // ...
}

here

  • MethodName – the name of method;
  • T – a type parameter that implements the IComparable<T> or IEquatable<T> interfaces. That is, this type implements the CompareTo() method;
  • return_type – the type returned by the method. Type T may also appear here;
  • parameters – the parameters that the method receives.

In the above code, one of the two bounds on the type T

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

may be absent. It all depends on the task solved.

A generic method can be either an instance method or a static method.

 

7. An example of sorting instances of type Point<T>. Implementing the IComparable<T> and IEquatable<T> Interfaces in a shared static method

The example declares two classes:

  • class Point – describes a point on the coordinate plane. The class implements the IComparable<Point> and IEquatable<Point> interfaces so that objects of this class can be compared;
  • the SortMethods class, which implements the generic static SortInsertion<T>() method. The method implements the insertion sort algorithm for sorting instances of the Point class.

 

using System;

namespace ConsoleApp19
{
  // A class describing a point on a coordinate plane.
  // To enable comparison, the class implements
  // the IComparable<T> and IEquatable<T> interfaces.
  class Point : IComparable<Point>, IEquatable<Point>
  {
    // Internal fields - coordinates of a point
    private double x, y;

    // Constructor
    public Point(double x, double y)
    {
      this.x = x;
      this.y = y;
    }

    // Property LengthOrigin() - returns the distance from the current point to the origin
    public double LengthOrigin
    {
      get { return Math.Sqrt(x * x + y * y); }
    }

    // Implementation of the CompareTo() method of the IComparable<Point> interface.
    // The distance from the point to the origin is compared:
    // - if the distance for the current instance is greater than the distance pt, 1 is returned,
    // - if the distance for the current instance is less than the distance pt, -1 is returned;
    // - if the distances are equal, 0 is returned.
    public int CompareTo(Point pt)
    {
      if (LengthOrigin > pt.LengthOrigin)
        return 1;
      if (LengthOrigin < pt.LengthOrigin)
        return -1;
      return 0;
    }

    // Implementing the Equals() Method of the IComparable<T> Interface
    public bool Equals(Point pt)
    {
      return LengthOrigin == pt.LengthOrigin;
    }

    // Overriding the Equals () method of the Object class,
    // needed for compatibility with the new Equals(Point) method
    public override bool Equals(object obj)
    {
      if (obj is Point)
        return Equals((Point)obj);
      return false;
    }

    // Override the GetHashCode () method,
    // required for compatibility with the new Equals(Point) method
    public override int GetHashCode()
    {
      return base.GetHashCode();
    }

    // Override ToString() method of Object class
    public override string ToString()
    {
      return base.ToString();
    }

    // A method that displays information about the current point.
    // The method is used for testing.
    public void ShowPoint()
    {
      // Display the coordinates of the current point and the distance to the origin
      Console.WriteLine("({0}, {1}) => {2:f2}", x, y, LengthOrigin);
    }
  }

  // A class containing a generic static insertion sort method
  // for objects of generic type T
  class SortMethods
  {
    // The static method of insertion sorting.
    // Parameters:
    // - array - the array to be sorted in ascending order.
    // The method returns a sorted 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);

      // Create an array of points
      Point[] AP =
      {
        new Point(3,5),
        new Point(2,7),
        new Point(4,6),
        new Point(-3,1)
      };

      // Print the list of points before sorting
      foreach (Point p in AP)
        p.ShowPoint();

      // Sort the array, call the static method SortInsertion()
      Point[] AP2 = SortMethods.SortInsertion<Point>(AP);

      // Print the list of points after sorting
      Console.WriteLine("--------------");
      Console.WriteLine("After sorting:");

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

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

Program execution result

(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!

 


Related topics