集合排序问题与ICompareble
对于C#最常见的集合List<T>,有时候需要进行排序,而List是直接有Sort方法,因此对于一个简单地整数集合排序,很简单:
List<int> ints = new List<int>() { 1, 3, 1, 4, 5, 9, 8 };
ints.ForEach(i => Console.Write(i+" - "));
ints.Sort();
Console.WriteLine();
ints.ForEach(i => Console.Write(i + " - "));
1 - 3 - 1 - 4 - 5 - 9 - 8 -
1 - 1 - 3 - 4 - 5 - 8 - 9 -
如果集合中是自定义数据,如下面的雇员类:
internal class Employee
{
public int ID { get; set; }
public string Name { get; set; }=String.Empty;
public string Gender { get; set; }=String.Empty ;
public int Salary { get; set; }
}
那么直接调用Sort则会出现问题,举例如下:
List<Employee> listEmployees = new List<Employee>
{
new Employee() { ID = 101, Name = "Pranaya", Gender = "Male", Salary = 5000 },
new Employee() { ID = 102, Name = "Priyanka", Gender = "Female", Salary = 7000 },
new Employee() { ID = 103, Name = "Anurag", Gender = "Male", Salary = 5500 },
new Employee() { ID = 104, Name = "Sambit", Gender = "Male", Salary = 6500 },
new Employee() { ID = 105, Name = "Hina", Gender = "Female", Salary = 6500 }
};
Console.WriteLine("Employees Before Sorting");
foreach (Employee employee in listEmployees)
{
Console.WriteLine("ID = {0}, Name = {1}, Gender = {2}, Salary = {3}",
employee.ID, employee.Name, employee.Gender, employee.Salary);
}
listEmployees.Sort();
Console.WriteLine("\nEmployees After Sorting");
foreach (Employee employee in listEmployees)
{
Console.WriteLine("ID = {0}, Name = {1}, Gender = {2}, Salary = {3}",
employee.ID, employee.Name, employee.Gender, employee.Salary);
}
运行时会报错:
出错的原因很简单,Sort方法不知道如何对Employee对象排序。那为什之前对List<int> 类型可以排序呢?
因为像int, double, string, decimal, char,这些基本类型都已经实现了IComparable接口,看一下int类型的定义:
所以我们要实现对Employee集合的排序,也应该让Employee实现这个接口。
添加接口ICompareble<T>之后,会提示要求实现CompareTo方法:
CompareTo方法有一个参数,跟自己的类型一样,返回一个Int类型ans,分别用正,0,负代表要比较的对象的关系:
ans>0:代表当前对象大于传入的对象
ans==0:代表两个对象相等
ans<0:代表当前对象小于传入对象
实现如下:
public int CompareTo(Employee? obj)
{
if (obj == null)
return 1;
if (this.Salary > obj.Salary)
{
return 1;
}
else if (this.Salary < obj.Salary)
{
return -1;
}
else
{
return 0;
}
}
运行结果:
Employees Before Sorting
ID = 101, Name = Pranaya, Gender = Male, Salary = 5000
ID = 102, Name = Priyanka, Gender = Female, Salary = 7000
ID = 103, Name = Anurag, Gender = Male, Salary = 5500
ID = 104, Name = Sambit, Gender = Male, Salary = 6500
ID = 105, Name = Hina, Gender = Female, Salary = 6500
Employees After Sorting
ID = 101, Name = Pranaya, Gender = Male, Salary = 5000
ID = 103, Name = Anurag, Gender = Male, Salary = 5500
ID = 104, Name = Sambit, Gender = Male, Salary = 6500
ID = 105, Name = Hina, Gender = Female, Salary = 6500
ID = 102, Name = Priyanka, Gender = Female, Salary = 7000
考虑到工资是一个Int类型,而此类型本身也实现了Comparable接口,因为可以简化比较函数,直接用改为:
public int CompareTo(Employee? obj)
{
if (obj == null)
return 1;
//if (this.Salary > obj.Salary)
//{
// return 1;
//}
//else if (this.Salary < obj.Salary)
//{
// return -1;
//}
//else
//{
// return 0;
//}
return Salary.CompareTo(obj.Salary);
}
到此为止,好像问题都解决了,但试想一下,我们实现的接口是按工资来排序,假设有时候要按照人名排序,有时候按性别排序咋办呢?一个接口只有一个实现,所以上面的方法不够灵活。
比较器类
List类的Sort方法有4个重载,其中一个是Sort(IComparer<T> comparer),也就是你可以直接传入一个实现ICompare<T>的对象,Sort方法会调用次对象的固定方法,这里我们将这个特殊的对象叫做比较器。
回到上面的问题,我们来写一个比较器实现用名字比较:
public class SortByName : IComparer<Employee>
{
int IComparer<Employee>.Compare(Employee? x, Employee? y)
{
return x!.Name.CompareTo(y!.Name);
}
}
在原来的代码后面再加上如下几行:
Console.WriteLine("\n Employees After Sorting by Name");
listEmployees.Sort(new SortByName());
foreach (Employee employee in listEmployees)
{
Console.WriteLine("ID = {0}, Name = {1}, Gender = {2}, Salary = {3}",
employee.ID, employee.Name, employee.Gender, employee.Salary);
}
运行即可看到列表按姓名字母排序了。
这样就将比较函数和原生的Employee类解耦,后面想安性别排序,也可以再实现一个类似的比较器。
使用Linq排序
熟悉linq的朋友都知道,前面将的两种排序方法有点繁琐(过时),很多复杂的集合操作都可以用linq轻松搞定。就算不熟悉Linq,熟悉SQL查询语句的,也能很快上手Linq。这里我就不介绍Linq基础知识了,大家可以看看我之前写的Linq文章。
这里简单展示一下Linq的魅力:
Console.WriteLine("\n Use Linq Sort");
var ans=listEmployees.OrderBy(x => x.ID).ToList();
ans.ForEach(employee => Console.WriteLine("ID = {0}, Name = {1}, Gender = {2}, Salary = {3}",
employee.ID, employee.Name, employee.Gender, employee.Salary));
Console.WriteLine("We got a new Copy");
listEmployees.ForEach(employee => Console.WriteLine("ID = {0}, Name = {1}, Gender = {2}, Salary = {3}",
employee.ID, employee.Name, employee.Gender, employee.Salary));
顺便要说的是,Linq返回的是一个新的List,不会影响排序前的List,这和Sort方法不一样。
使用代理Comparison<T>排序
Sort的另一个重载是:Sort(Comparison<T> comparison): 其中Comparison是一个delegate类型:
// 摘要:
// Represents the method that compares two objects of the same type.
//
// 参数:
// x:
// The first object to compare.
//
// y:
// The second object to compare.
//
// 类型参数:
// T:
// The type of the objects to compare.
//
// 返回结果:
// A signed integer that indicates the relative values of x and y, as shown in the
// following table.
// Value – Meaning
// Less than 0 –x is less than y.
// 0 –x equals y.
// Greater than 0 –x is greater than y.
public delegate int Comparison<in T>(T x, T y);
有了前面的介绍,我也不多说了,直接看代码:
listEmployees.Sort(CompareEmployees);
static int CompareEmployees(Employee e1,Employee e2)
{
return e1.Salary.CompareTo(e2.Salary);
}
所有用代理的地方基本上也可以用Lambda表达式:所以上面几行可以用下面一行代替:
listEmployees.Sort((e1, e2) => e1.ID.CompareTo(e2.ID));
显而易见,Lmbda表达式更简洁优雅,所以Delegate也过时了。
小结
从前面的讨论可以看出最简单的排序方法是利用Linq或者Lambda,另外两种会增加额外的代码量,但是我们应该了解其背后的原理,Linq和Lambda都是C#后面陆续推出的,自然会更能节省开发者时间。