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

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

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

public struct Student:IComparable
    {
        
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方法来实现比较操作符的重载。例如:

  

public struct Student:IComparable
    {
        
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接口:

 

public struct Student:IComparable
    {
        
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 可以为排序提供一个可选的排序依据,这可以用于一些没有给你提供排序依据的类型上,提供你自己的排序依据。

 

 

public class program
    {
        
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方法。例如:

static void Main()
        {
            Student stu 
= new Student("PeterLau");
            Teacher tea 
= new Teacher();
            IComparable cstu 
= stu;
            cstu.CompareTo(tea);
     }

 

public struct Student:IComparable
    {
        
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类型操作。例如:

posted @ 2009-09-18 13:57  PeterLau  阅读(428)  评论(0)    收藏  举报