java icomparer_Item 26 用IComparable和IComparer实现对象的顺序关系

当你创建自己的类型时,应该定义类型的顺序关系,以便在集合中描述对象的存储及排序。.net框架为我们定义了两个接口用于实现对象的比较顺序关系,分别是:IComparable和IComparer。IComparable接口用于定义类型的自然顺序(据我理解也就是对象的默认顺序关系),IComparer接口用于定义其他可选的顺序关系。你可以在实现这两个接口时,定义并实现自己的比较操作符(>,=,<=),这样可以避免.net运行时采用默认比较关系的低效问题。当你实现了这两个接口,.net框架核心可以通过你的接口实现来对类型的对象进行比较和排序,这样在客户端就能获得更高的比较和排序的效率。

IComparable接口只有一个方法,就是CompareTo(object right),这个方法沿用了C语言的strcmp函数的实现规则,当调用方法的对象小于参数时,返回值就小于0,当调用方法的对象大于参数时,返回值就大于0,若相等,则返回0。很明显,CompareTo方法的参数为object类型,所以在实现CompareTo方法时需要检查参数的运行时类型。例如:

publicstructStudent:IComparable

{privatestring_name;publicstringName

{get{return_name; }set{ _name=value; }

}publicStudent(stringname)

{

_name=name;

}intIComparable.CompareTo(objectright)

{if(!(rightisStudent))

{thrownewArgumentException("right is not a studnet");

}

Student stu=(Student)right;returnCompareTo(stu);

}publicintCompareTo(Student right)

{

_name.CompareTo(right._name);

}

}

通过在Student结构体里重载CompareTo方法,并且将原来的CompareTo方法实现为一个隐式接口实现(去掉public,加上IComparer.CompareTo),现在IComparer.CompareTo方法只能通过IComparer接口类型的对象来调用,这时客户端通过Student类型的对象就只能调用类型安全的CompareTo方法了,那种无意的将非Student类型作为参数的调用就会产生编译错误,让客户在编译时就发现错误并纠正,而不是把错误留在运行时抛出异常。而且,这时通过编译时类型为Student类型的对象调用CompareTo方法时就不会有装箱和拆箱的新能损失了。例如:

这时就不会出现编译错误,只会在运行时抛出异常。

上面我们提到在实现了IComparable接口后,可以重载比较关系操作符,c#语言提供了这种机制,我们应该使用类型安全的CompareTo方法来实现比较操作符的重载。例如:

publicstructStudent:IComparable

{privatestring_name;publicstringName

{get{return_name; }set{ _name=value; }

}publicStudent(stringname)

{

_name=name;

}intIComparable.CompareTo(objectright)

{if(!(rightisStudent))

{thrownewArgumentException("right is not a studnet");

}

Student stu=(Student)right;returnCompareTo(stu);

}publicintCompareTo(Student right)

{return_name.CompareTo(right._name);

}publicstaticbooloperator

{returnleft.CompareTo(right)<0;

}publicstaticbooloperator>(Student left, Student right)

{returnleft.CompareTo(right)>0;

}publicstaticbooloperator<=(Student left, Student right)

{returnleft.CompareTo(right)<=0;

}publicstaticbooloperator>=(Student left, Student right)

{returnleft.CompareTo(right)>=0;

}

}

当我们定义的Student类实现了IComparable接口并重载了比较操作符之后,于所有的Student都是按照名字进行排序的,这时如果期末总评成绩出来了,我们要根据成绩进行排序,这时在保留原来默认的排序规则基础之上,通过实现IComparer接口来添加这样一个新的排序规则,如果你可以访问Student类的源代码,那么可以通过在类型内部提供一个私有的嵌套类,实现IComparer接口:

publicstructStudent:IComparable

{privatestring_name;privateint_score;privatestaticScoreComparer _scoreCompare;publicstringName

{get{return_name; }set{ _name=value; }

}publicintScore

{get{return_score;}set{_score=value;}

}publicIComparer ScoreCompare

{get{if(_scoreCompare==null)

{

_scoreCompare=newScoreComparer();

}return_scoreCompare;

}

}privateclassScoreComparer : IComparer

{intIComparer.Compare(objectleft,objectright)

{if(!(leftisStudent))

{thrownewArgumentException("left is not a student");

}if(!(rightisStudent))

{thrownewArgumentException("right is not a stuent");

}

Student leftStu=(Student)left;

Student rightStu=(Student)right;returnleftStu._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 可以为排序提供一个可选的排序依据,这可以用于一些没有给你提供排序依据的类型上,提供你自己的排序依据。

publicclassprogram

{staticvoidMain()

{

Student stu=newStudent("PeterLau");

Teacher tea=newTeacher();

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方法。例如:

staticvoidMain()

{

Student stu=newStudent("PeterLau");

Teacher tea=newTeacher();

IComparable cstu=stu;

cstu.CompareTo(tea);

}

publicstructStudent:IComparable

{privatestring_name;publicstringName

{get{return_name; }set{ _name=value; }

}publicStudent(stringname)

{

_name=name;

}publicintCompareTo(objectright)

{if(!(rightisStudent))

{thrownewArgumentException("right is not a studnet");

}

Student stu=(Student)right;return_name.CompareTo(stu._name);

}

}

显然,这样一个CompareTo方法的实现是有缺点的。首先,方法接受的参数是object类型,那么在客户端可以用任何类型的对象作为参数来调用CompareTo方法,不会产生编译错误,只会在运行时抛出异常,很明显,能在编译时解决的问题,为什么要留到运行时解决呢?然后,由于Student类型是值类型,如果提供正确的参数类型,那么还要经过一次装箱和两次拆箱才能完成比较(参数传入时,进行一次装箱,类型判断和转换时各进行一次拆箱),这对性能来讲确实是一个损失。所以,我们必须找一个可选的方法,虽然我们无法改变CompareTo方法的定义,但是也没必要让客户端在一个弱类型上忍受性能损失。可以在Student类里重载CompareTo(Student right)方法,使其只针对Student类型操作。例如:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值