LINQ基礎 - IComparable vs IComparer And IEquatable vs IEqualityComparer

LINQ基礎 - IComparable vs IComparer And IEquatable vs IEqualityComparer

原始文章將記錄於此
https://github.com/s0920832252/LinQ-Note

這四個介面通常是作用在參考型別.

  • 介面 IEquatable 和 IEqualityComparer 是用來檢查兩個參考型別是否相等.
    • 在取 Union , SequenceEqual , ToDictionary 等等的方法 , 我們可能會使用到它. 因為我們需要判兩個參考型別的物件是否相等.
  • 介面 IComparable 和 IComparer 是用來判斷兩個對象誰大誰小.
    • IComparable 和 IComparer 通常用於對一群參考型別的物件進行排序.
  • 在 Linq 方法中實際上會用到的是 IEqualityComparer 以及 IComparer
    • 另外兩個是順便 :crystal_ball:

IEquatable

IEquatable 是在 C# 2.0 加入的. 它的主要用途是提供實作此介面的類別物件具有判斷自己與另一個與自己相同類別的物件是否相等的能力. 所以實現 IEquatable 介面的類別都必須實作 Equals 方法. bool Equals(T other);

class Employee : IEquatable<Employee>
{
     public int Id { get; set; }
     public string Name { get; set; }
     public bool Equals(Employee other)
     {
          return (Id == other.Id && Name == other.Name);
     }       
     // 可以不 override 
     public override bool Equals(object obj)
     {
          Employee employee = (Employee)obj;
          return (Id == employee.Id && Name == employee.Name);
     }
     // 可以不 override 
     public override int GetHashCode()
     {
           return Id.GetHashCode() ^ Name.GetHashCode();
     }
}

static void Main(string[] args)
{
     Employee ob1 = new Employee();
     Employee ob2 = new Employee();
     Console.WriteLine(ob1.Equals(ob2)); // true

     // 如果沒 override bool Equals(object obj) , 
     // 會使用Object.Equals - 依據記憶體位址是否相同(回傳 false)
     Employee a = new Employee();
     object b = new Employee();
     Console.WriteLine(a.Equals(b)); // true
     
     List<Employee> employees = new List<Employee> { 
         new Employee { Id = 1, Name = "王" 
     }};
     List<Employee> employees2 = new List<Employee> { 
         new Employee { Id = 3, Name = "綠" } , 
         new Employee { Id = 1, Name = "王" } 
     };
     foreach (var emp in employees.Union(employees2))
     {
          Console.Write(" " + emp.Name); // 輸出 : 王 綠
          // 若是沒有實作 bool Equals(Employee other) 會因為記憶體位址不同 , 
          // 被認為是不同實體 , 而輸出 : 王 綠 王
     }
}

微軟的文件建議若是我們實作 IEquatable 介面 , 則順便 override Equals() & GetHashCode(). 以避免未預期的結果發生.


IEqualityComparer

IEqualityComparer是在 C# 2.0 加入的. 實作該介面的類別物件具有判斷某相同類別的兩個物件是否相等的能力. 實作IEquatable 的類別物件自身就能夠判斷自己是否與另外一個物件相等. 而實作 IEqualityComparer 的類別物件則是專門負責比較的比較器 , 通常是以不希望更改原類別為前提 , 但又希望可以比較該類別是否相等的狀況下使用.

IEqualityComparer 必須實作下列兩個方法

  1. bool Equals(T x, T y) :
    • 判斷兩物件相等的邏輯
  2. int GetHashCode(T obj) :
    • 使用 hash 的方式加快比較相等的速度(在 key 相同的情況才會再進去 Equals去判斷兩物件是否相等)
class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
}

class EmployeeComparer : IEqualityComparer<Employee>
{
    public bool Equals(Employee x, Employee y)
    {
        return x.Name.Equals(y.Name) && x.Id.Equals(y.Id);
    }

    public int GetHashCode(Employee obj)
    {
        return obj.Name.GetHashCode() ^ obj.Id.GetHashCode();
    }
}

static void Main(string[] args)
{
    List<Employee> employees = new List<Employee> { 
                                  new Employee { Id = 1, Name = "王" } 
                               };
    List<Employee> employees2 = new List<Employee> { 
                                  new Employee { Id = 2, Name = "綠" }, 
                                  new Employee { Id = 1, Name = "王" } 
                                };
    foreach (var emp in employees.Union(employees2, new EmployeeComparer()))
    {
        Console.Write(" " + emp.Name);
        // 輸出 王 綠
        // 若沒有使用 EmployeeComparer 
        // 則輸出 王 綠 王
    }
    Console.ReadKey();
}

IComparable

IComparable 必須實現 CompareTo 方法

int CompareTo(Object obj) // 比較邏輯
  1. 當呼叫物件 < 參數物件obj , 回傳負數
  2. 當呼叫物件 > 參數物件obj , 回傳正數
  3. 當呼叫物件 = 參數物件obj , 回傳零
class Employee : IComparable<Employee>
{
     public int Id { get; set; }
     public string Name { get; set; }

     public int CompareTo(Employee other)
     {
          return Id.CompareTo(other.Id);
     }
}

static void Main(string[] args)
{
     List<Employee> employees = new List<Employee> { 
             new Employee { Id = 3, Name = "王" }, 
             new Employee { Id = 1, Name = "綠" }, 
             new Employee { Id = 2, Name = "王" }, 
     };
     employees.Sort(); // 沒實作 IComparable 會跳例外XD
     foreach (var employee in employees)
     {
          Console.WriteLine($"{employee.Id} {employee.Name}");
     }
     Console.ReadKey();
}
輸出結果

Id = 1 Name = 綠
Id = 2 Name = 王
Id = 3 Name = 王


IComparer

IComparer 使用在參考型態的自定義排序. (在 C# 3.5 前 , 可能在 Sort 方法的時候會使用) 但在 LinQ 的 ThenBy , OrderBy …出來後 , 使用機率會少很多. LinQ 仍然有接受 IComparer 作為參數的方法.

int Compare(T x, T y);
  • 若 x < y 回傳 -1
  • 若 x == y 回傳 0
  • 若 x > y 回傳 1
class Employee
{
        public int Id { get; set; }
        public string Name { get; set; }
}

class EmployeeCompare : IComparer<Employee>
{
     public int Compare(Employee x, Employee y)
     {
          return x.Id.CompareTo(y.Id);
     }
}

static void Main(string[] args)
{
     List<Employee> employees = new List<Employee> { 
                                 new Employee { Id = 3, Name = "王" }, 
                                 new Employee { Id = 1, Name = "綠" }, 
                                 new Employee { Id = 2, Name = "王" } 
                                 };
     employees.Sort(new EmployeeCompare());
     foreach (var employee in employees)
     {
          Console.WriteLine($"{employee.Id} {employee.Name}");
     }
     Console.ReadKey();
}
輸出結果

與 LinQ 的關聯

一些 LinQ 的方法可能會需要知道兩個項目是否相等或者需要比較兩個項目的大小.

  • 需要知道兩個項目是否相等 ToDictionary , ToHashSet , ToLookup , Contains , Distinct , SequenceEqual , Except , GroupBy , GroupJoin , Intersect , Join , Union
  • 需要比較兩個項目的大小 OrderBy , OrderByDescending , ThenBy , ThenByDescending ,

參考

  1. Equals vs IEqualityComparer, IEquatable<T>, IComparable, IComparer
  2. Comparing Complex Type With ==, Equals, IEquatable and IComparable in C#
  3. IComparable, IComparer And IEquatable Interfaces In C#
  4. 使用原則(5) - IComparable<T>, IEquatable<T>
  5. What is the difference between IEqualityComparer<T> and IEquatable<T>?
  6. What’s the difference between IComparable & IEquatable interfaces?
  7. [C#] IComparable 和 IComparer
  8. Implementing IComparable and IEquatable safely and fully
  9. 談談C#的相等性(1)
  10. The Right Way to do Equality in C#

補充 : 製造泛型的 IEqualityComparer

class EqualityComparer<TSource> : IEqualityComparer<TSource> where TSource : class
{
     private PropertyInfo[] properties = null;
     public bool Equals(TSource x, TSource y)
     {
          if (properties == null)
               properties = x.GetType().GetProperties();
          return properties.Aggregate(true, (result, item) =>
          {
               return result && item.GetValue(x).Equals(item.GetValue(y));
          });
     }

     public int GetHashCode(TSource obj)
     {
          if (properties == null)
               properties = obj.GetType().GetProperties();
          return properties.Aggregate(0, (result, item) =>
          {
               return result ^ item.GetValue(obj).GetHashCode();
          });
     }
}

補充 : EqualityComparer<T>.Default 屬性

依照泛型來決定回傳哪一個已被微軟定義好的比較器.

  • 大部分方法預設是使用此方法去製造比較器.
  • 未來我們自定義方法或是類別的時候 , 可使用此方法去幫我們產生比較器
public static Dictionary<TKey, TElement> MyToDictionary<TSource, TKey, TElement>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, IEqualityComparer<TKey> comparer)
{
    if (source is null || keySelector is null || elementSelector is null)
    {
        throw new Exception("null exception");
    }

    // 若 comparer 為 null 使用微軟定義的比較器.
    var d = new Dictionary<TKey, TElement>(
                    comparer ?? EqualityComparer<TKey>.Default
                );
                
    foreach (var element in source)
    {
        d.Add(keySelector(element), elementSelector(element));
    }
    return d;
}

Thank you!

You can find me on

若有謬誤 , 煩請告知 , 新手發帖請多包涵

:100: :muscle: :tada: :sheep: