Effective C# Item 26: Implement Ordering Relations with IComparable and IComparer
通过IComparable和IComparer接口来实现排序关系
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型,因此我们在使用的时候需要对参数的类型进行检查:
public struct Customer : IComparable

{
private readonly string _name;
public string Name

{
get

{
return _name;
}
}

public Customer(string name)

{
_name = name;
}


IComparable 成员#region IComparable 成员

public int CompareTo(object right)

{

if (!(right is Customer))

{
throw new ArgumentException("Argument not a customer", "right");
}
Customer rightCustomer = (Customer)right;
return _name.CompareTo(rightCustomer.Name);
}

#endregion
}
IComparable接口有一些不尽人意之处。我们必须检查输入参数的类型,因为我们不能确定使用者会传递哪种类型的对像。另外在对值类型进行操作的时候还会因为boxing和unboxing而降低执行效率。当我们使用IComparable对集合进行排序时需要进行N*log(n)次比较。对于一个大小为1000的集合来说,需要进行7000次左右的比较,在某些条件下这相当于20000多次boxing和unboxing。在这种情况下我们必须寻找更好的比较途径。虽然我们不能修改IComparable.CompareTo()的定义,但是我们还是可以通过重写CompareTo方法来解决这个问题:
public struct Customer : IComparable

{
private readonly string _name;
public string Name

{
get

{
return _name;
}
}

public Customer(string name)

{
_name = name;
}


IComparable 成员#region IComparable 成员

int IComparable.CompareTo(object right)

{

if (!(right is Customer))

{
throw new ArgumentException("Argument not a customer", "right");
}
Customer rightCustomer = (Customer)right;
return _name.CompareTo(rightCustomer.Name);
}

#endregion

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()方法:
public struct Customer : IComparable

{
private readonly string _name;
public string Name

{
get

{
return _name;
}
}

public Customer(string name)

{
_name = name;
}


IComparable 成员#region IComparable 成员

int IComparable.CompareTo(object right)

{

if (!(right is Customer))

{
throw new ArgumentException("Argument not a customer", "right");
}
Customer rightCustomer = (Customer)right;
return _name.CompareTo(rightCustomer.Name);
}

#endregion

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接口内部创建一个私有的类,并将其通过静态属性暴露出来:
public struct Customer : IComparable

{
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 成员#region IComparable 成员

int IComparable.CompareTo(object right)

{

if (!(right is Customer))

{
throw new ArgumentException("Argument not a customer", "right");
}
Customer rightCustomer = (Customer)right;
return _name.CompareTo(rightCustomer.Name);
}

#endregion

public int CompareTo(Customer right)

{
return _name.CompareTo(right.Name);
}

private class RevenueComparer : IComparer

{


IComparer 成员#region IComparer 成员

int IComparer.Compare(object left, object right)

{
if (!(left is Customer))

{
throw new ArgumentException("Argument not a customer", "left");
}
if (!(right is Customer))

{
throw new ArgumentException("Argument not a customer", "right");
}
Customer leftCustomer = (Customer)left;
Customer rightCustomer = (Customer)right;
return leftCustomer._revenue.CompareTo(rightCustomer._revenue);
}

#endregion
}
}
现在我们就可以通过收入来对用户排序了:
Customer c1 = new Customer();
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著
回到目录
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通