Effective C# Item 26: Implement Ordering Relations with IComparable and IComparer
有时候我们需要为类定义排序关系以便在集合中对该类的对象进行排序和检索。在.Net Framework中定义了两种用来描述排序顺序关系的接口:IComparable和IComparer。IComparable接口描述了类型的通用排序比较方法,而IComparer描述的是两个对象之间的比较方法。在本节中主要讨论如何实现这种排序顺序关系。另外,我们可以通过实现自定义关系运算符来提供这些特殊的比较关系。
IComparable接口包含一个方法:CompareTo()。这个方法的历史可以追溯到C语言类库中的strcmp函数:如果当前对象小于比较对象则返回小于0的值,如果相等则返回0,如果当前对象大于比较对象则返回大于0的值。IComparable的参数为System.Object型,因此我们在使用的时候需要对参数的类型进行检查:
{
private readonly string _name;
public string Name
{
get
{
return _name;
}
}
public Customer(string name)
{
_name = name;
}
IComparable 成员
}
IComparable接口有一些不尽人意之处。我们必须检查输入参数的类型,因为我们不能确定使用者会传递哪种类型的对像。另外在对值类型进行操作的时候还会因为boxing和unboxing而降低执行效率。当我们使用IComparable对集合进行排序时需要进行N*log(n)次比较。对于一个大小为1000的集合来说,需要进行7000次左右的比较,在某些条件下这相当于20000多次boxing和unboxing。在这种情况下我们必须寻找更好的比较途径。虽然我们不能修改IComparable.CompareTo()的定义,但是我们还是可以通过重写CompareTo方法来解决这个问题:
{
private readonly string _name;
public string Name
{
get
{
return _name;
}
}
public Customer(string name)
{
_name = name;
}
IComparable 成员
public int CompareTo(Customer right)
{
return _name.CompareTo(right.Name);
}
}
现在IComparable.CompareTo()只是一个接口实现,只能通过IComparable接口被调用。用户使用的将是类型安全的比较。不正确的比较类型不能通过编译。不能通过编译的原因是参数类型和Customer.CompareTo(Customer right)方法不符。只有通过IComparable接口才会调用CompareTo(object right)方法。
当我们实现IComparable接口时,应当也提供一个强类型的重载。这样可以减少我们发生错误的可能性,而且比较两个已知类型对像的效率较高。不过这种重载对于集合的Sort()方法来说没有效果,因为它是通过接口来访问CompareTo()方法的。
我们可以再对Customer结构做一些修改。在C#中允许我们重载运算符。它们调用的是类型安全的CompareTo()方法:
{
private readonly string _name;
public string Name
{
get
{
return _name;
}
}
public Customer(string name)
{
_name = name;
}
IComparable 成员
public int CompareTo(Customer right)
{
return _name.CompareTo(right.Name);
}
public static bool operator <(Customer left, Customer right)
{
return left.CompareTo(right) < 0;
}
public static bool operator <=(Customer left, Customer right)
{
return left.CompareTo(right) <= 0;
}
public static bool operator >(Customer left, Customer right)
{
return left.CompareTo(right) > 0;
}
public static bool operator >=(Customer left, Customer right)
{
return left.CompareTo(right) >= 0;
}
}
上例中我们完成了以姓名对用户排序。现在我们又需要对用户的收入进行排序,而且不可以修改上例中对姓名排序的基本排序方式。为了解决这个问题,我们可以创建一个实现IComparer接口的类来达到目的。在.Net类库中凡是实现了IComparable接口的类型都通过IComparer接口进行了重载。我们可以为Customer接口内部创建一个私有的类,并将其通过静态属性暴露出来:
{
private readonly string _name;
public string Name
{
get
{
return _name;
}
set
{
_name = value;
}
}
private double _revenue;
public double Revenue
{
get
{
return _revenue;
}
set
{
_revenue = value;
}
}
private static RevenueComparer _revComp = null;
public static IComparer RevenueCompare
{
get
{
if (_revComp == null)
{
_revComp = new RevenueComparer();
}
return _revComp;
}
}
IComparable 成员
public int CompareTo(Customer right)
{
return _name.CompareTo(right.Name);
}
private class RevenueComparer : IComparer
{
IComparer 成员
}
}
现在我们就可以通过收入来对用户排序了:
Customer c2 = new Customer();
Customer.RevenueCompare.Compare(c1, c2);
另外我们还应当注意一下Equals()方法和==运算符。在排序时我们没有实现等于关系的必要。事实上,对于引用类型来说,判断大小依照的是对像的内容,而判断相等依照的是对像的地址。
IComparable和IComparer是为类提供顺序关系的基本机制。IComparable适用于大多数的排序需求。当我们实现它时,也应该重载比较关系运算符。IComparable.CompareTo()使用System.Object类型对像做为参数,为此我们可以提供一个特定类型参数的重载来提高效率,减少错误。如果一个类型并未给我们提供需要的排序机制,那么我们可以通过ICompare来达到目的。
译自 Effective C#:50 Specific Ways to Improve Your C# Bill Wagner著
回到目录