1. Введение
Если у вас когда-либо был ящик с носками, в котором вы пытались найти пару, вы уже сталкивались с проблемой сортировки. Когда всё лежит в беспорядке, поиск нужной вещи превращается в квест. В программировании с коллекциями — та же история.
Сортировка — это процесс упорядочивания элементов коллекции по определённому критерию (например, по алфавиту, по значению, дате и т. д.). Это важно для:
- Отображения данных пользователю (никто не любит хаос).
- Упрощения поиска (например, бинарный поиск возможен только в отсортированных коллекциях).
- Сравнения, отчётов, экспорта и других операций.
Вопрос на миллион: "А если у меня всего пять элементов, я могу не сортировать?" — Теоретически, да, но как только пять превращаются в пятьсот или пять тысяч, без автоматизации не обойтись.
В .NET есть два главных подхода для сортировки:
- Изменение исходной коллекции (например, сортировка List<T> методом Sort).
- Создание новой отсортированной копии коллекции (например, клонирование массива, а затем сортировка).
Сортировка списков с помощью метода .Sort()
Метод 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
}
}
}
Здесь происходит сортировка "на месте": исходный список numbers меняется, элементы в нем переставляются.
Сортировка строк
Сортировка отлично работает и для строк:
var words = new List<string> { "апельсин", "яблоко", "банан", "груша" };
words.Sort();
Console.WriteLine(string.Join(", ", words)); // апельсин, банан, груша, яблоко
Интересный факт: по умолчанию строковая сортировка идет по Unicode-порядку, а не по "человеческому" алфавиту (особенно для разных языков, учтите это при многоязычных приложениях).
2. Сортировка по собственным правилам
Иногда сортировка "по умолчанию" не подходит. Допустим, вы хотите отсортировать пользователей не по имени, а по возрасту или дате регистрации.
Сортировка с помощью лямбда-выражения (метод 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
Как работает эта магия? Лямбда (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. Сортировка копии коллекции (без изменения исходной)
Если вы не хотите менять исходный список, скопируйте его перед сортировкой:
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> — это неупорядоченная коллекция по своей сути (т. е. не гарантирует порядок при перечислении). Но иногда хочется получить "отсортированные пары".
Чтобы получить отсортированные ключи или значения, создайте список, скопируйте туда пары, и отсортируйте нужным образом:
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> или передавайте лямбду, как показано выше.
Сортировка списка пользователей по алфавиту
Допустим, у нас в приложении есть список пользователей:
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}");
Сравнение способов сортировки
| Коллекция | Изменяется ли на месте? | Метод для сортировки | Можно ли задавать правило? |
|---|---|---|---|
|
Да | |
Да: лямбда или IComparer |
|
Да | |
Да: лямбда или IComparer |
|
Нет | — (создаём список и сортируем) | Да: через лямбду или IComparer |
7. Типичные ошибки и нюансы сортировки
Сортировка на месте — изменяет исходную коллекцию! Если исходные данные не должны меняться — копируйте их заранее.
Попытка сортировать неизменяемую коллекцию (например, ReadOnlyCollection<T>) приведёт к ошибке выполнения.
Сравнение строк по-разному на разных культурах: сортировка строк (особенно с кириллицей, умляутами и пр.) может отличаться на различных локалях. Для корректной сортировки используйте Comparer.Create(...) с нужной культурой.
Сортировка словарей не изменяет их структуру — всегда возвращается новая последовательность пар (или новый список).
Для сложного порядка используйте интерфейс 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();
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ