Effective C# 20.以 IComparable<T> 與 IComparer<T> 實作排序關係 Effective C# 20.以 IComparable<T> 與 IComparer<T> 實作排序關係 (Implement Ordering Relations with IComparable<T> and IComparer<T>)

Published on Thursday, October 17, 2024

這個做法在教你如何用 IComparable 與 IComparer 介面來實作排序的功能。

首先準備一下測試資料:

void Main()
{
	Car[] arrayOfCars = new Car[6]
	{
		 new Car("Ford",1992),
		 new Car("Fiat",1988),
		 new Car("Buick",1932),
		 new Car("Ford",1932),
		 new Car("Dodge",1999),
		 new Car("Honda",1977)
	};
}

public class Car
{
	public int year { get; set; }
	public string make { get; set; }

	public Car(string Make, int Year)
	{
		make = Make;
		year = Year;
	}
}

目前有 IComparable 與 IComparable 兩種版本,建議是直接實作泛型版本就好,IComparable 介面會進行額外的拆裝箱所以除了要兼容 就版本否則只實作 IComparable 就好,接下來在 Car 類別實作這兩個介面試試。

public class Car : IComparable<Car>, IComparable
{
	public int year { get; set; }
	public string make { get; set; }

	public Car(string Make, int Year)
	{
		make = Make;
		year = Year;
	}

	public int CompareTo(object obj)
	{
		if (!(obj is Car))
			throw new ArgumentException("Argument is not a Car", "obj");
		Car other = (Car)obj;
		return this.CompareTo(other);
	}

	public int CompareTo(Car other)
	{
		return String.Compare(this.make, other.make);
	}
}

這裡可以用 Linq 的 OrderBy 方法進行測試,目前會使用預設的 Comparer<T>.Default 最後會取得 IComparable<T> 也就是上面寫的 Car 類別的 CompareTo 方法來判斷兩個元素的優先順序,所以結果會輸出由名稱排序。

void Main()
{
	Car[] arrayOfCars = new Car[6]
	{
		 new Car("Ford",1992),
		 new Car("Fiat",1988),
		 new Car("Buick",1932),
		 new Car("Ford",1932),
		 new Car("Dodge",1999),
		 new Car("Honda",1977)
	};
	
	var sorted = arrayOfCars.OrderBy(x => x);

	foreach (var stuff in sorted)
	{
		Console.WriteLine(stuff.make);
	}
}

Buick
Dodge
Fiat
Ford
Ford
Honda

也可以多載常用的運算子讓可讀性更加提升。

public class Car : IComparable<Car>, IComparable
{
	public int year { get; set; }
	public string make { get; set; }

	public Car(string Make, int Year)
	{
		make = Make;
		year = Year;
	}

	public int CompareTo(object obj)
	{
		if (!(obj is Car))
			throw new ArgumentException("Argument is not a Car", "obj");
		Car other = (Car)obj;
		return this.CompareTo(other);
	}

	public int CompareTo(Car other)
	{
		return String.Compare(this.make, other.make);
	}

	public static bool operator <(Car left, Car right) => 
		left.CompareTo(right) < 0;
	public static bool operator <=(Car left, Car right) =>
		left.CompareTo(right) <= 0;
	public static bool operator >(Car left, Car right) =>
		left.CompareTo(right) > 0;
	public static bool operator >=(Car left, Car right) =>
		left.CompareTo(right) >= 0;
}

我們就可以直接比較兩個值的大小,例如下面檢查 Buick 小於 Fiat 結果返回 True。

Console.WriteLine(arrayOfCars[2] < arrayOfCars[1]);
True

目前都是用字串名稱排序方法來進行排序,可以看到我們的測試資料裡面有包含年份,接下來可以試試寫一個根據年份排序的方法。 要做到這樣的效果主要是透過實作 IComparer<T> 介面。

可以在之前的 Car 類別裡面寫一個 Nested Cass YearComparer 並且使用年份來進行比較。

private static Lazy<YearComparer> yearComp = new Lazy<YearComparer>(() => new YearComparer());
public static IComparer<Car> YearCompare => yearComp.Value;
public static Comparison<Car> CompareByYear => (left, right) => left.year.CompareTo(right.year);
	
private class YearComparer : IComparer<Car>
{
	//  IComparer<Customer> Members
	int IComparer<Car>.Compare(Car left, Car right) =>
		left.year.CompareTo(right.year);
}

最後可以利用靜態成員的特性取得專用的 YearComparer 就能看到結果會優先用年份來排序了。

var sorted = arrayOfCars.OrderBy(x => x, Car.YearCompare);

foreach (var stuff in sorted)
{
	Console.WriteLine(stuff.make);
}

不過由於現在 Linq 已經支援 OrderBy, ThenBy, OrderByDescending, ThenByDescending 這些好用的方法, 所以現在簡單的排序工作可以直接利用 Linq 就好,有一些特殊的排序在自行實作 IComparer<T> 就好。

var sorted = arrayOfCars
	.OrderBy(x => x.year)
	.ThenBy(x => x.make);

foreach (var stuff in sorted)
{
	Console.WriteLine(stuff.make);
}

Summary

這個做法主要在教如何使用 IComparable 與 IComparer 這兩個介面,在舊版本的 C# 都是透過這兩個介面來處理排序的關係, 但是現在已經有更好用的 Linq 擴充方法,所以簡單的工作透過 Linq 就好,複雜的排序在自己實作 IComparer<T> 就好。