1. 入门
如果你有过一个袜子抽屉,想找一双配对的袜子,那你肯定遇到过排序的问题。当一切都乱七八糟的时候,找东西就像打副本一样。在编程里,集合也是同样的道理。
排序就是把集合里的元素按照某个标准(比如字母顺序、数值、日期等等)排个顺序。这很重要,因为:
- 给用户展示数据(没人喜欢乱糟糟的界面)。
- 方便查找(比如二分查找只能在排好序的集合里用)。
- 对比、报表、导出和其他操作。
百万级问题:“如果我只有五个元素,还需要排序吗?”——理论上可以不排,但一旦五变成五百、五千,不自动化就不行了。
.NET里排序主要有两种方式:
- 直接改原始集合(比如用List<T>的Sort方法排序)。
- 新建一个排好序的集合副本(比如先克隆数组再排序)。
用.Sort()方法排序List
Sort()方法是List<T>类自带的,因为它实现了IList<T>接口,支持按索引访问和元素顺序调整。
例子——数字升序排序:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
var numbers = new List<int> { 5, 2, 9, 1, 5, 6 };
numbers.Sort();
Console.WriteLine("升序排序:");
foreach(var number in numbers)
{
Console.Write($"{number} "); // 1 2 5 5 6 9
}
}
}
这里是“原地排序”:原始Listnumbers被改了,元素顺序变了。
字符串排序
字符串排序也很简单:
var words = new List<string> { "橙子", "苹果", "香蕉", "梨" };
words.Sort();
Console.WriteLine(string.Join(", ", words)); // 橙子, 香蕉, 梨, 苹果
有趣的是:默认字符串排序是按Unicode顺序来的,不是“人类”字母表顺序(多语言应用要注意)。
2. 自定义排序规则
有时候默认排序不够用。比如你想按用户年龄或注册日期排序,而不是名字。
用lambda表达式排序(Sort(Comparison<T>)方法)
Sort方法可以传一个自定义的比较函数,来指定排序规则。
按用户年龄排序:
class User
{
public string Name { get; set; }
public int Age { get; set; }
}
// ...
var users = new List<User>
{
new User { Name = "艾丽莎", Age = 30 },
new User { Name = "鲍勃", Age = 25 },
new User { Name = "伊娃", Age = 35 }
};
users.Sort((u1, u2) => u1.Age.CompareTo(u2.Age));
foreach (var user in users)
{
Console.WriteLine($"{user.Name}: {user.Age}");
}
// 输出:
// 鲍勃: 25
// 艾丽莎: 30
// 伊娃: 35
这魔法怎么实现的?lambda(u1, u2) => u1.Age.CompareTo(u2.Age)返回负数表示u1更小,正数表示更大,0表示一样大。
用IComparer<T>接口
有时候你想把排序规则单独写成一个类,比如有很多集合要用,或者逻辑很复杂。
class UserAgeComparer : IComparer<User>
{
public int Compare(User x, User y)
{
return x.Age.CompareTo(y.Age);
}
}
// ...
var users = new List<User>{ /* ... */ };
users.Sort(new UserAgeComparer()); // 现在按年龄排序
这样很方便,尤其是需要在不同地方复用排序规则时。
手动多字段排序
如果有些用户年龄一样,但你想在同龄人里再按名字排:
users.Sort((u1, u2) => {
int ageCompare = u1.Age.CompareTo(u2.Age);
if (ageCompare != 0)
return ageCompare;
else
return u1.Name.CompareTo(u2.Name);
});
现在先按年龄,再按名字排序。
3. 排序集合副本(不改原始集合)
如果你不想动原始List,排序前先复制一份:
var copy = new List<int>(numbers);
copy.Sort();
自定义对象也一样:
var usersCopy = new List<User>(users);
usersCopy.Sort((a, b) => a.Age.CompareTo(b.Age));
4. 数组排序
数组(T[])排序也很简单:
int[] numbers = { 4, 2, 9, 7 };
Array.Sort(numbers); // 原数组被改
自定义排序:
Array.Sort(numbers, (a, b) => b.CompareTo(a)); // 降序排序
注意Array.Sort会改原数组,不会返回新数组。如果想保留原始数据,先复制一份:
int[] oldNumbers = { 3, 2, 1 };
int[] copy = (int[])oldNumbers.Clone();
Array.Sort(copy);
5. 字典“排序”(Dictionary<TKey, TValue>)
Dictionary<TKey, TValue>本质上是无序集合(遍历时顺序不保证)。但有时候你想要“排好序的键值对”。
想要排好序的键或值,可以新建List,把键值对复制进去,然后按你想要的方式排序:
var dict = new Dictionary<string, int>
{
{ "苹果", 2 },
{ "橙子", 5 },
{ "梨", 3 }
};
// 按键排序:
var keyValueList = new List<KeyValuePair<string, int>>(dict);
keyValueList.Sort((a, b) => a.Key.CompareTo(b.Key));
foreach (var kv in keyValueList)
{
Console.WriteLine($"{kv.Key}: {kv.Value}");
}
// 按值排序:
keyValueList.Sort((a, b) => a.Value.CompareTo(b.Value));
foreach (var kv in keyValueList)
{
Console.WriteLine($"{kv.Key}: {kv.Value}");
}
如果只想要排好序的键或值:
var sortedKeys = new List<string>(dict.Keys);
sortedKeys.Sort();
var sortedValues = new List<int>(dict.Values);
sortedValues.Sort();
但要记住,Dictionary结构本身不会变——你只是拿到了排好序的遍历结果。等.NET 9出来会有OrderedDictionary<TKey, TValue>,它会记住元素顺序。虽然不常用,但有时候确实需要。
6. 有用的小细节
一定要实现比较接口吗?
很简单:如果你想让你的对象“知道”怎么比较自己(比如按日期或名字),就实现IComparable<T>接口。
class Product : IComparable<Product>
{
public string Name { get; set; }
public decimal Price { get; set; }
public int CompareTo(Product other)
{
return Price.CompareTo(other.Price);
}
}
现在你可以这样用:
var products = new List<Product> { /* ... */ };
products.Sort(); // 按价格排序
如果默认比较不合适——用IComparer<T>或者lambda,像上面那样。
按字母排序用户列表
比如你有个用户列表:
List<string> users = new List<string> { "维克多", "安娜", "叶卡捷琳娜", "鲍里斯" };
users.Sort(); // 现在users排好序了:安娜, 鲍里斯, 维克多, 叶卡捷琳娜
按紧急程度分配任务
class Task
{
public string Title { get; set; }
public int Priority { get; set; } // 1 - 紧急, 2 - 重要, 3 - 可以拖延
}
var todo = new List<Task>
{
new Task { Title = "做作业", Priority = 2 },
new Task { Title = "买面包", Priority = 1 },
new Task { Title = "追剧", Priority = 3 }
};
todo.Sort((a, b) => a.Priority.CompareTo(b.Priority));
foreach (var task in todo)
Console.WriteLine($"{task.Priority}: {task.Title}");
排序方式对比
| 集合 | 是否原地修改? | 排序方法 | 能自定义规则吗? |
|---|---|---|---|
|
是 | |
可以:lambda或IComparer |
|
是 | |
可以:lambda或IComparer |
|
否 | —(新建List再排序) | 可以:lambda或IComparer |
7. 常见错误和排序细节
原地排序——会改原始集合!如果原始数据不能动——提前复制一份。
试图排序不可变集合(比如ReadOnlyCollection<T>)会报运行时错误。
不同文化下字符串比较:字符串排序(尤其是有西里尔字母、变音符号等)在不同地区可能不一样。想要靠谱的排序,用Comparer.Create(...)指定文化。
字典排序不会改结构——你拿到的总是新序列(或新List)。
复杂排序用IComparer<T>接口写你的逻辑。
下面是个错误排序的例子(踩坑警告):
var numbers = new List<int> { 1, 2, 3 };
var sorted = numbers.Sort(); // 错误:Sort()返回void!
正确做法:
numbers.Sort(); // 原地改numbers
// 如果要新集合:
var sorted = new List<int>(numbers);
sorted.Sort();
GO TO FULL VERSION