Search code examples
c#c++icomparablegeneric-method

C# generic compare method [analogue of C++ template]


Both C++ and C# support generics. However, I don't see any way to rewrite a simple C++ function template that compares any two args (arg1 > arg2 ?) into a singe C# generic method:

C++

template<typename T>
int compare(const T & st1, const T & st2) {
    if (std::less<T>() (st1, st2)) 
        return -1;
    return 1;
}

works both with int, std::string, std::vectoretc.

compare(33, 4);         // 1

std::vector<int> v1{ 1,0 }, v2{ 1,0,0 };
compare(v1, v2);        // -1

std::vector<std::string> vs1{ "hi", "bob" }, vs2{ "hi", "ben" };
compare(vs1, vs2);      // 1

C#

   class Demo
    {
        public static int Compare<T>(T v1, T v2) where T : IComparable<T>
        {
            if (v1.CompareTo(v2) < 0)
                return -1;
            return 1;
        }
    }

doesn't work with, say C# Lists<>:

List<int> v1 = new List<int> { 1, 2 };
List<int> v2 = new List<int> { 3, 4 };
Console.WriteLine($"Compare(v1, v2): {Compare(v1, v2)}");

error: no implicit reference conversion from 'System.Collections.Generic.List' to 'System.IComparable>'

Is the only way to make it work with both integral types and collections in C# to overload each time?

public static int Compare<T>(List<T> v1, List<T> v2) where T : IComparable<T>
{
    for (int i = 0; i < v1.Count; i++)
    {
        if (v1[i].CompareTo(v2[i]) < 0)
            return -1;
    }
    return 1;
}

Solution

  • The immediate cause of the error is that List<int> doesn't implement IComparer<List<int>> and this fact doesn't meet method's specification:

    public static int Compare<T>(T v1, T v2) where T : IComparable<T>
    

    Since T must implement IComparable<T>.

    I suggest something like this (quick and in some case a dirty solution):

    public static int Compare<T>(T v1, T v2, IComparer<T> comparer = null) {
      if (null == comparer)              // If we don't have tailored comparer
        comparer = Comparer<T>.Default;  // Try default one
    
      // If we don't know how to compare - throw exception
      if (null == comparer)
        throw new ArgumentNullException("comparer", 
          $"Type {typeof(T).Name} doesn't have default comparer; comparer must not be null.");
    
      // Taken from the question: 
      // if (v1.CompareTo(v2) < 0)
      //          return -1;
      //      return 1;
      // You, probably, may want just  
      // return comparer.Compare(v1, v2);
      return comparer.Compare(v1, v2) < 0 ? -1 : 1;
    }
    

    So you can put, in a simple case

    int result = Compare(15, 25); // Comparer<int>.Default will be used
    

    In a complex case with no default comparer you have to implement it:

    public class MyComparer<T> : IComparer<IEnumerable<T>> {
      public int Compare(IEnumerable<T> x, IEnumerable<T> y) {
        if (Object.ReferenceEquals(x, y))
          return 0;
        else if (null == x)
          return -1;
        else if (null == y)
          return 1;
    
        Comparer<T> comparer = Comparer<T>.Default;
    
        using (var en_x = x.GetEnumerator()) {
          using (var en_y = y.GetEnumerator()) {
            if (!en_x.MoveNext()) 
              if (!en_y.MoveNext())
                return 0;
              else
                return 1;
            else if (en_y.MoveNext())
              return -1;
    
            if (comparer != null) {
              int result = comparer.Compare(en_x.Current, en_y.Current);
    
              if (result != 0)
                return result;
            }
          }
        }
    
        return 0;
      }
    }
    

    And provide the comparer

    List<int> v1 = new List<int> { 1, 2 };
    List<int> v2 = new List<int> { 3, 4 };
    
    int another result = Compare(v1, v2, new MyComparer<int>());