C# 的 List<T> 提供了 Sort 方法用于对集合元素进行原地排序。如果元素类型实现了 IComparable 接口,可以直接调用无参的 Sort 方法;但在实际业务中,往往需要根据对象的某个属性、多个字段组合甚至动态条件来排序,这时就需要向 Sort 方法传入自定义的比较逻辑。List<T>.Sort 本身接受 Comparison<T> 委托或 IComparer<T> 接口对象,也可以借助 LINQ 的 OrderBy 生成新的有序序列。下面分别说明这几种方式的写法和适用场景。

一、用 Comparison<T> 委托实现内联条件排序
如果排序规则比较简单,并且只在当前方法中使用一次,最直接的方式是向 Sort 方法传入一个 Comparison<T> 委托。该委托的参数是两个同类型对象,返回一个整数表示相对顺序:小于0表示前者排在前面,等于0表示两者相等,大于0表示后者排在前面。借助 Lambda 表达式可以在一行内写出排序条件。
下面是一个按 Age 属性升序排列的示例:
List<Person> people = GetPeople(); people.Sort((x, y) => x.Age.CompareTo(y.Age));
如果需要降序,只需要交换比较对象:
people.Sort((x, y) => y.Age.CompareTo(x.Age));
对于多条件排序,可以在 Lambda 内部先比较第一个字段,不为0时直接返回,否则继续比较第二个字段。例如按年龄升序、年龄相同时按姓名降序:
people.Sort((x, y) =>
{
int ageResult = x.Age.CompareTo(y.Age);
if (ageResult != 0) return ageResult;
return string.Compare(y.Name, x.Name, StringComparison.Ordinal);
});
使用 Comparison<T> 的优点是代码紧凑、条件清晰,适合短小的排序规则;缺点是不够直观,当条件变多时 Lambda 体会迅速膨胀,且不容易在多个地方复用。另外需要注意的是,Sort 方法会直接修改原集合,如果不想影响原始数据,需要先复制一份再排序。
二、通过 IComparer<T> 接口封装可复用排序规则
当同一个排序规则需要在多个方法或类中重复使用时,实现 IComparer<T> 接口是更规范的做法。该接口只有一个 Compare 方法,返回值约定与 Comparison<T> 委托一致。常见的做法是定义一个比较器类,在 Compare 方法中编写完整的多字段比较逻辑。
先定义一个简单的 Person 类:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public DateTime? HireDate { get; set; }
}
然后实现一个按年龄降序排列的比较器:
public class AgeDescendingComparer : IComparer<Person>
{
public int Compare(Person x, Person y)
{
if (ReferenceEquals(x, y)) return 0;
if (x is null) return 1;
if (y is null) return -1;
return y.Age.CompareTo(x.Age);
}
}
使用时直接创建比较器实例传入 Sort 方法:
people.Sort(new AgeDescendingComparer());
如果需要多个字段组合,可以在比较器内部依次判断。例如先按 Name 升序,再按 Age 升序:
public class NameThenAgeComparer : IComparer<Person>
{
public int Compare(Person x, Person y)
{
int nameResult = string.Compare(x.Name, y.Name, StringComparison.Ordinal);
if (nameResult != 0) return nameResult;
return x.Age.CompareTo(y.Age);
}
}
相比 Comparison<T> 委托,IComparer<T> 更适合承载复杂且需要复用的规则。它的另一个优势是可以作为类的静态只读实例暴露出来,避免每次排序都重新分配对象。比如在 Person 类中定义一个静态字段 public static readonly IComparer<Person> AgeDescending = new AgeDescendingComparer();,调用方可以直接使用 people.Sort(Person.AgeDescending);
三、使用 LINQ 的 OrderBy 与 ThenBy 生成排序结果
如果不想修改原集合,而是希望生成一个排序后的新序列,可以使用 LINQ 的 OrderBy、OrderByDescending、ThenBy 和 ThenByDescending 方法。它们返回 IOrderedEnumerable<T>,不会改变原始 List<T> 的元素顺序,调用 ToList 后可以得到新的有序列表。
例如按年龄降序、姓名升序生成新列表:
List<Person> sortedPeople = people
.OrderByDescending(p => p.Age)
.ThenBy(p => p.Name)
.ToList();
LINQ 排序默认使用 Comparer<T>.Default 进行比较,对于字符串会采用当前区域性的排序规则。如果需要区分大小写或忽略大小写,可以传入 StringComparer 参数,例如 OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase)。这一扩展参数同样适用于 ThenBy。
从性能角度看,OrderBy 是延迟执行的,只有在真正遍历或调用 ToList 时才会执行排序,并且内部实现是稳定排序;而 List<T>.Sort 使用内省排序,属于不稳定排序,相等的元素在排序后相对顺序可能发生变化。如果业务要求保持原始顺序,优先选择 LINQ。不过 LINQ 会创建新的集合,内存开销比原地排序大,在处理大规模数据时需要权衡。
四、处理空值与自定义比较逻辑的细节
实际排序中经常会遇到属性为 null 的情况。直接调用 CompareTo 会抛出 NullReferenceException,因此在自定义比较逻辑中要先处理空值。通常的做法是规定 null 排在最后或最前,示例:
people.Sort((x, y) =>
{
if (x.HireDate is null && y.HireDate is null) return 0;
if (x.HireDate is null) return 1; // 空日期排最后
if (y.HireDate is null) return -1;
return x.HireDate.Value.CompareTo(y.HireDate.Value);
});
上面的代码将 null 的 HireDate 排在非空日期之后。如果希望空值排在最前,只需交换返回的 1 和 -1。对于字符串排序,string.Compare 本身允许传入 null,但要注意它会把 null 视为小于任何非空字符串,这可能不符合业务预期,因此最好显式判断。
当排序逻辑比较复杂且涉及多个计算字段时,可以考虑在排序前为每个元素预计算排序键,避免在比较器中重复计算。例如按某个派生属性排序时,可以构造一个匿名类型或元组列表,排序后再映射回原对象。这种方式能减少比较次数带来的开销,尤其是 List<T>.Sort 的复杂度为 O(n log n),比较器会被调用很多次。
另一个容易忽略的细节是字符串比较的区域性差异。默认的 string.Compare 重载受当前线程的 CultureInfo 影响,可能会导致在不同环境下排序结果不同。如果需要稳定的字节序比较,应显式传入 StringComparison.Ordinal。对于数字或日期等类型,直接使用其 CompareTo 方法即可。