我们都知道,在开发中如果想把某个类型的对象放入集合中执行排序和搜索功能,就需要定义出来对象与对象之间的关系。那么你知道怎么样定义对象关系才是正确的吗?下面就听我一一道来。
零、讲解
在 .NET 中有两个接口可以用来定义关系,即 IComparable 和 IComparer 。前者用来规定某类型的给对象之间所具备的自然顺序,后者用来表示另一种排序机制可以有需要提供排序功能的类型来实现。 IComparable 接口只有一个方法 CompareTo ,该方法遵循如下的惯例:如果本对象小于另一个受测对象,就返回小于 0 的值,如果相等就返回 0 ,如果大于受测对象就返回大于 0 的值。这里需要注意的是在新的 .NET API 中大部分都使用了 IComparable ,而在一些老的 API 中使用的依然是不带泛型的 IComparable 接口,所以我们在实现 IComparable 的时候就必须实现 IComparable 。并且由于 IComparable 的CompareTo方法需要一个 object 类型的参数,因此我们需要检查传入参数的运行期类型,就是说每次进行对比前我们要判断它是否是指定的类型,如果不是就抛出异常反之继续执行后续代码。下面的代码就同时实现了 IComparable 和 IComparable。
public class User:IComparable<User>,IComparable { private readonly string name; public User(string name) { this.name=name; } //实现 IComparable<T> 中的 CompareTo 方法 public int CompareTo(User user) => name.CompareTo(user.name); //实现 IComparable 中的 CompareTo 方法 int IComparable.CompareTo(object obj) { if(!(obj is User)) { throw new ArgumentException("传入的参数不是 User 类型!"); } User user=(User)obj; return this.CompareTo(user); } }
在上述代码中我们看到了一个奇怪的代码行 int IComparable.CompareTo(object obj)
,明确的限定了这个方法只能通过 IComparable 来调用,这就说明了它是专门留给老版本 API 使用的。现在大部分开发人员都不怎么喜欢非泛型的 IComparable ,主要是因为它要检查传入参数的运行期类型,并且每次作比较的时候有很大的可能性会触发装箱和拆箱操作,我们都知道装箱和拆箱操作是一个很费时的事情。而且因为 IComparable.CompareTo 的对比次数为 nlog(n) 次,因此每次进行比较时基本上会执行装箱和拆箱操作,这样的话要执行三次。因此对于大数据的排序对比的耗时将是非常恐怖的。
到这里一定有读者会问:IComparable.CompareTo 缺点这么大为什么还要实现它呢?其实原因很简单,首先为了保证向后兼容,当我门需要和老版本的 API 进行交互的时候这一点是非常重要的,其次有些时候开发人员只能/必须调用 IComparable.CompareTo 。我们在实现 IComparable 的时候必须限定这个版本相关的方法只能通过 IComparable 形式的引用来调用,同时还要提供强类型的公共重载版本用来提升程序执行效率,还能防止开发人员用错 CompareTo 方法。
到这里我们的代码并没有完成,我们还需要利用 CompareTo 方法重载关系运算符:
public class User:IComparable<User>,IComparable { private readonly string name; public User(string name) { this.name=name; } //实现 IComparable<T> 中的 CompareTo 方法 public int CompareTo(User user) => name.CompareTo(user.name); //实现 IComparable 中的 CompareTo 方法 int IComparable.CompareTo(object obj) { if(!(obj is User)) { throw new ArgumentException("传入的参数不是 User 类型!"); } User user=(User)obj; return this.CompareTo(user); } #region 重载运算符 public static bool operator < (User user1,User user2) { return user1.CompareTo(user2) < 0; } public static bool operator > (User user1,User user2) { return user1.CompareTo(user2) > 0; } public static bool operator <= (User user1,User user2) { return user1.CompareTo(user2) <= 0; } public static bool operator >= (User user1,User user2) { return user1.CompareTo(user2) >= 0; } #endregion }
上面的代码只是针对 name 进行了排序,那么如果我们需要按照 age 进行排序怎么办?难道我们要删掉 name 替换成 age 吗?当然不是,我们可以利用 Comparison形式的委托实现,这样我们就可以按照其他指标进行排列。具体的用法是在 User 类中增加一个静态属性,并且采用其他指标来定义对象与对象之间的顺序。
public static Comparison<User> CompareByAge=>(user1,user2)=>user1.age.CompareTo(user2.age);
Tip:大部分基础教程的书上会讲解利用嵌套类的方式实现按照另一种方式排序,这个我只建议大家了解即可,因为这是针对 .NET 1.X 的接口来说的。目前几乎没有任何公司、个人、机构在使用 .NET 1.X了,因此本文不进行讲解。
一、总结
在我们自定义类型的时候,IComparable 和 IComparer 是定义排序关系的标准,大部分排序基本上都可以通过 IComparable 来实现,但是我们需要主义的时这个时候我们必须重载运算符 <、>、<=、>= 这样才能产生出与 IComparable 相协调的结果。