C#/C# 인터페이스

IComparable/IComparer/IEquatable : 비교와 관련된 인터페이스

로파이 2021. 11. 13. 23:34

IComparable / IComparable<T>

선후 관계 / 대소 비교 등을 위한 인터페이스를 제공한다.

어떤 클래스의 인스턴스로 이루어진 컬렉션의 정렬을 위해 사용된다. 

public interface IComparable<in T>
{
    int CompareTo([AllowNull] T other);
}

CompareTo : 해당 메서드는 int 형을 반환하는데 다음의 선후 관계를 의미한다.

  • 0보다 작음 : 해당 인스턴스(Caller)가 other 보다 선의 관계를 가지며 정렬 시 other보다 앞에 배치된다.
  • 0 : other와 동등한 관계이다.
  • 0보다 큼 : 해당 인스턴스가 other 보다 후의 관계를 가지며 정렬 시 other보다 뒤에 배치된다.

예제

    public class Temperature : IComparable
    {
        protected double temperatureF;

        public int CompareTo(object obj)
        {
            if (obj == null)
                return 1;

            Temperature otherTemperature = obj as Temperature;
            if (otherTemperature != null)
                return this.temperatureF.CompareTo(otherTemperature.temperatureF);
            else
                throw new ArgumentException("Object is not a Temperature");
        }

        public double Fahrenheit { get { return this.temperatureF; } set { this.temperatureF = value; } }

        public double Celcius { get { return (this.temperatureF - 32) * (5.0 / 9); }  set { this.temperatureF = (value * 9.0 / 5) + 32; }  }
    }

 

IComparable<T>를 구현할 경우 int CompareTo(T other)를 구현해야한다.

제너릭 버전의 IComparable<T>를 구현하는 경우 해당 CompareTo 활용도가 높으므로 연산자를 오버로딩 하는 것이 좋다.

    public class Temperature : IComparable<Temperature>
    {
        public int CompareTo(Temperature other)
        {
            // If other is not a valid object reference, this instance is greater.
            if (other == null) return 1;

            // The temperature comparison depends on the comparison of
            // the underlying Double values.
            return m_value.CompareTo(other.m_value);
        }

        public static bool operator >(Temperature operand1, Temperature operand2)
        {
            return operand1.CompareTo(operand2) > 0;
        }
        public static bool operator <(Temperature operand1, Temperature operand2)
        {
            return operand1.CompareTo(operand2) < 0;
        }
        public static bool operator >=(Temperature operand1, Temperature operand2)
        {
            return operand1.CompareTo(operand2) >= 0;
        }
        public static bool operator <=(Temperature operand1, Temperature operand2)
        {
            return operand1.CompareTo(operand2) <= 0;
        }

        protected double m_value = 0.0;

        public double Celsius
        {
            get
            {
                return m_value - 273.15;
            }
        }

        public double Kelvin
        {
            get
            {
                return m_value;
            }
            set
            {
                if (value < 0.0)
                {
                    throw new ArgumentException("Temperature cannot be less than absolute zero.");
                }
                else
                {
                    m_value = value;
                }
            }
        }

        public Temperature(double kelvins)
        {
            this.Kelvin = kelvins;
        }
    }

 

Comparer 인터페이스

Comparer, Comparer<T>: Compare 비교 연산을 하는 클래스에 대한 인터페이스

public interface IComparer<in T>
{
    int Compare([AllowNull] T x, [AllowNull] T y);
}

두 인자를 비교하여 선 후 관계를 반환하여야한다. 어떤 클래스의 비교를 IComparer를 상속받은 클래스의 인스턴스를 이용하여 위임할 수 있을 것이다.

    public class TempComparer : IComparer<Temperature>
    {
        public int Compare(Temperature left, Temperature right)
        {
            return left.Celsius.CompareTo(right.Celsius);
        }
    }
    
    // IComparable<Temperature>를 구현한 Temperature 클래스 안에서
    static private TempComparer Comp = new TempComparer();
    public int CompareTo(Temperature other)
    {
        if (other == null) return 1;

        return Comp.Compare(this, other);
    }

 

Comparison 대리자

Comparer<T>의 Compare 메서드와 같은 시그니쳐를 갖는 대리자 형식이 Comparison<T>로 정의되어있다.

public delegate int Comparison<in T>(T x, T y);

대리자를 이용하여 CompareTo를 구현한다. 

static private Comparison<Temperature> CompDelegate =
    (Temperature left, Temperature right) => { return left.Celsius.CompareTo(right.Celsius); };

public int CompareTo(Temperature other)
{
    if (other == null) return 1;

    return CompDelegate(this, other);
}

 

IEquatable<T>

두 인스턴스가 동등한 지 판단하는 bool Equals(T other)를 구현해야한다.

public interface IEquatable<T>
{
    bool Equals([AllowNull] T other);
}

IEquatable<T> 인터페이스는 제너릭 컬렉션에서 자주 사용되는데 (Dictionary<TKey, TValue>, List<T>, LinkedList<T>), Contains, IndexOf, LastIndexOf, Remove등 에서 두 요소가 같은지 판별하기 위해 해당 인터페이스가 사용된다.

 

IComparable<T>와 차이점

IComparable<T>의 경우 원소를 정렬하기 위해 사용되는 인터페이스 이며 int를 반환하고 IEqutable은 두 인스턴스가 동등한 지 판별하기 때문에 bool를 반환한다.

 

IEqutable<T>를 구현한다면, T 타입 클래스가 반드시 Object.Equals(Object)와 Object.GetHashCode()를 오버라이드하여 행동이 Equals<T>와 동일하게 동작하도록 해야한다.

 

T가 Value-Type 인 경우

bool Equals(T) / bool Equals(Object) / int GetHashCode()를 정의 혹은 재정의해주고 operator ==와 != 연산자 오버로딩을 하여 Equals와 같은 방식으로 동작하게 해준다.

public struct Data : IEquatable<Data>
    {
        public DateTime Date { get; set; }
        public int Value { get; set; }

        public static bool operator==(Data lhs, Data rhs)
        {
            return lhs.Equals(rhs);
        }

        public static bool operator!=(Data lhs, Data rhs)
        {
            return !(lhs == rhs);
        }

        public bool Equals(Data rhs)
        {
            return Date.Year == rhs.Date.Year && Date.Month == rhs.Date.Month && Date.Day == rhs.Date.Day
                    && Value == rhs.Value;
        }

        public override bool Equals(object rhs)
        {
            if (rhs == null || !(rhs is Data))
                return false;

            return Equals((Data)rhs);
        }

        public override int GetHashCode()
        {
            return Date.GetHashCode() ^ Value.GetHashCode();
        }
    }

T가 Reference-Type 인 경우

bool Equals(T) / bool Equals(Object) / int GetHashCode()를 정의 혹은 재정의해주나 연산자 오버로딩은 하지 않는다.

==가 보통 참조의 같음을 비교하는 데 사용하고 내부적인 값의 같음으로 사용하는 것이 드물기 때문이다.

 

    public class Product : IEquatable<Product>
    {
        public int Index { get; set; }
        public string Tag { get; set; }

        public override bool Equals(Object other)
        {
            return Equals(other as Product);
        }

        public override int GetHashCode()
        {
            return Index.GetHashCode() ^ Tag.GetHashCode();
        }

        public bool Equals([AllowNull] Product other)
        {
            if (Object.ReferenceEquals(this, other))
                return true;

            if (Object.ReferenceEquals(null, other))
                return false;
			
            // Product 파생 클래스가 있다면 정확한 타입 비교 필요
            // if (this.GetType() != other.GetType())
            //  return false;
            
            return Index == other.Index && Tag == other.Tag;
        }
    }