Item 26 用IComparable和IComparer实现对象的顺序关系
当你创建自己的类型时,应该定义类型的顺序关系,以便在集合中描述对象的存储及排序。.net框架为我们定义了两个接口用于实现对象的比较顺序关系,分别是:IComparable和IComparer。IComparable接口用于定义类型的自然顺序(据我理解也就是对象的默认顺序关系),IComparer接口用于定义其他可选的顺序关系。你可以在实现这两个接口时,定义并实现自己的比较操作符(>,<,>=,<=),这样可以避免.net运行时采用默认比较关系的低效问题。当你实现了这两个接口,.net框架核心可以通过你的接口实现来对类型的对象进行比较和排序,这样在客户端就能获得更高的比较和排序的效率。
IComparable接口只有一个方法,就是CompareTo(object right),这个方法沿用了C语言的strcmp函数的实现规则,当调用方法的对象小于参数时,返回值就小于0,当调用方法的对象大于参数时,返回值就大于0,若相等,则返回0。很明显,CompareTo方法的参数为object类型,所以在实现CompareTo方法时需要检查参数的运行时类型。例如:
{
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
public Student(string name)
{
_name = name;
}
int IComparable.CompareTo(object right)
{
if (!(right is Student))
{
throw new ArgumentException("right is not a studnet");
}
Student stu = (Student)right;
return CompareTo(stu);
}
public int CompareTo(Student right)
{
_name.CompareTo(right._name);
}
}
通过在Student结构体里重载CompareTo方法,并且将原来的CompareTo方法实现为一个隐式接口实现(去掉public,加上IComparer.CompareTo),现在IComparer.CompareTo方法只能通过IComparer接口类型的对象来调用,这时客户端通过Student类型的对象就只能调用类型安全的CompareTo方法了,那种无意的将非Student类型作为参数的调用就会产生编译错误,让客户在编译时就发现错误并纠正,而不是把错误留在运行时抛出异常。而且,这时通过编译时类型为Student类型的对象调用CompareTo方法时就不会有装箱和拆箱的新能损失了。例如:
这时就不会出现编译错误,只会在运行时抛出异常。
上面我们提到在实现了IComparable接口后,可以重载比较关系操作符,c#语言提供了这种机制,我们应该使用类型安全的CompareTo方法来实现比较操作符的重载。例如:
{
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
public Student(string name)
{
_name = name;
}
int IComparable.CompareTo(object right)
{
if (!(right is Student))
{
throw new ArgumentException("right is not a studnet");
}
Student stu = (Student)right;
return CompareTo(stu);
}
public int CompareTo(Student right)
{
return _name.CompareTo(right._name);
}
public static bool operator <(Student left, Student right)
{
return left.CompareTo(right) < 0;
}
public static bool operator >(Student left, Student right)
{
return left.CompareTo(right) > 0;
}
public static bool operator <=(Student left, Student right)
{
return left.CompareTo(right) <= 0;
}
public static bool operator >=(Student left, Student right)
{
return left.CompareTo(right) >= 0;
}
}
当我们定义的Student类实现了IComparable接口并重载了比较操作符之后,于所有的Student都是按照名字进行排序的,这时如果期末总评成绩出来了,我们要根据成绩进行排序,这时在保留原来默认的排序规则基础之上,通过实现IComparer接口来添加这样一个新的排序规则,如果你可以访问Student类的源代码,那么可以通过在类型内部提供一个私有的嵌套类,实现IComparer接口:
{
private string _name;
private int _score;
private static ScoreComparer _scoreCompare;
public string Name
{
get { return _name; }
set { _name = value; }
}
public int Score
{
get{return _score;}
set{_score=value;}
}
public IComparer ScoreCompare
{
get
{
if (_scoreCompare == null)
{
_scoreCompare = new ScoreComparer();
}
return _scoreCompare;
}
}
private class ScoreComparer : IComparer
{
int IComparer.Compare(object left, object right)
{
if (!(left is Student))
{
throw new ArgumentException("left is not a student");
}
if (!(right is Student))
{
throw new ArgumentException("right is not a stuent");
}
Student leftStu = (Student)left;
Student rightStu = (Student)right;
return leftStu._score.CompareTo(rightStu._score);
}
}
}
通过在Student结构体里添加了一个实现了ICompare接口的私有嵌套类ScoreComparer,在该类里实现了IComparer.Compare方法,该方法实现了通过成绩实现对象的排序。该类实现了ScoreComparer类的一个单件模式,客户端只能通过那个静态属性获得ScoreComparer对象,并且只存在一个该对象。大家可能会奇怪,为什么没有在ScroeComparer类里重载Compare方法,提供一个类型安全的版本,我个人认为,由于这毕竟不是首选的排序规则,所以在客户端的比较之中用得不如默认排序规则那么频繁,所以我们将ScoreComparer类定义为private,不能通过客户端访问该类型,所以就无法提供类型安全的Compare方法的版本,而且我们确实没有必要将这个嵌套类暴露给客户端。
IComparable 和IComparer接口为类型的排序提供了标准的机制,IComparable 应该在大多数自然排序下使用。当你实现IComparable接口时,你应该为类型排序重载一致的比较操作符(<, >, <=, >=)。IComparable.CompareTo()使用的是System.Object做为参数,同样你也要重载一个类型安全的CompareTo()方法。IComparer 可以为排序提供一个可选的排序依据,这可以用于一些没有给你提供排序依据的类型上,提供你自己的排序依据。
{
static void Main()
{
Student stu = new Student("PeterLau");
Teacher tea = new Teacher();
stu.CompareTo(tea);
//Error 1 The best overloaded method match for 'Delegate.Student.CompareTo(Delegate.Student)' has some invalid arguments
//Error 2 Argument '1': cannot convert from 'Delegate.Teacher' to 'Delegate.Student'
}
}
上例出现两个编译错误,正是由于在Student结构体里重载了ComparerTo方法,使得任何非Student类型或Student的子类型的参数都无法通过编译,若要通过编译,只能将stu的声明类型转换为Icomparable类型,然后再调用CompareTo方法。例如:
{
Student stu = new Student("PeterLau");
Teacher tea = new Teacher();
IComparable cstu = stu;
cstu.CompareTo(tea);
}
{
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
public Student(string name)
{
_name = name;
}
public int CompareTo(object right)
{
if (!(right is Student))
{
throw new ArgumentException("right is not a studnet");
}
Student stu = (Student)right;
return _name.CompareTo(stu._name);
}
}
显然,这样一个CompareTo方法的实现是有缺点的。首先,方法接受的参数是object类型,那么在客户端可以用任何类型的对象作为参数来调用CompareTo方法,不会产生编译错误,只会在运行时抛出异常,很明显,能在编译时解决的问题,为什么要留到运行时解决呢?然后,由于Student类型是值类型,如果提供正确的参数类型,那么还要经过一次装箱和两次拆箱才能完成比较(参数传入时,进行一次装箱,类型判断和转换时各进行一次拆箱),这对性能来讲确实是一个损失。所以,我们必须找一个可选的方法,虽然我们无法改变CompareTo方法的定义,但是也没必要让客户端在一个弱类型上忍受性能损失。可以在Student类里重载CompareTo(Student right)方法,使其只针对Student类型操作。例如:

浙公网安备 33010602011771号