Урок 8.11 — Сортировка и сравнение
Array.Sort, IComparable, LINQ OrderBy и сортировка сложных объектов.
Когда у тебя есть список чисел, его легко отсортировать — числа сравниваются естественно. Но что, если нужно отсортировать список людей по возрасту, затем по фамилии, или список заказов по сумме? C# предоставляет гибкие механизмы сортировки: от простого Array.Sort() до мощных LINQ-методов с делегатами.
Array.Sort() и List.Sort()
Самый простой способ отсортировать массив или список — использовать встроенные методы Array.Sort() и List.Sort(). Они работают «на месте» (in-place), изменяя исходную коллекцию.
// Массивы
int[] numbers = { 5, 2, 8, 1, 9 };
Array.Sort(numbers);
Console.WriteLine(string.Join(", ", numbers)); // → 1, 2, 5, 8, 9
string[] names = { "Ольга", "Анна", "Иван", "Борис" };
Array.Sort(names);
Console.WriteLine(string.Join(", ", names)); // → Анна, Борис, Иван, Ольга
// Списки
List<int> list = new() { 9, 3, 7, 1, 4 };
list.Sort();
Console.WriteLine(string.Join(", ", list)); // → 1, 3, 4, 7, 9
// Сортировка в обратном порядке
Array.Sort(numbers);
Array.Reverse(numbers);
Console.WriteLine(string.Join(", ", numbers)); // → 9, 8, 5, 2, 1
// List.Reverse()
list.Reverse();
Console.WriteLine(string.Join(", ", list)); // → 9, 7, 4, 3, 1
Sort() использует «естественный» порядок: числа по возрастанию, строки — в алфавитном порядке. Для своих классов нужно реализовать IComparable или передать IComparer.IComparable<T> — естественный порядок
Если ты хочешь, чтобы твой класс можно было сортировать «из коробки» (методом Sort()), нужно реализовать интерфейс IComparable<T>. Он содержит единственный метод CompareTo(), который должен вернуть:
- Отрицательное число — текущий объект меньше другого (должен идти раньше).
- 0 — объекты равны.
- Положительное число — текущий объект больше другого (должен идти позже).
public class Person : IComparable<Person>
{
public string Name { get; set; }
public int Age { get; set; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
// Сравниваем по возрасту
public int CompareTo(Person? other)
{
if (other is null) return 1;
return Age.CompareTo(other.Age);
}
public override string ToString() => $"{Name} ({Age} лет)";
}
// Использование:
var people = new List<Person>
{
new Person("Анна", 25),
new Person("Иван", 30),
new Person("Ольга", 22),
new Person("Борис", 28)
};
people.Sort(); // использует CompareTo
people.ForEach(p => Console.WriteLine(p));
// Ольга (22 лет)
// Анна (25 лет)
// Борис (28 лет)
// Иван (30 лет)
IComparer<T> — разные способы сравнения
Иногда нужно сортировать один и тот же класс по-разному. IComparer<T> позволяет определить отдельный класс-компаратор для каждой стратегии сравнения.
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public override string ToString() => $"{Name} ({Age} лет)";
}
// Компаратор для сортировки по имени
public class NameComparer : IComparer<Person>
{
public int Compare(Person? x, Person? y)
{
if (x is null || y is null) return 0;
return string.Compare(x.Name, y.Name, StringComparison.Ordinal);
}
}
// Компаратор для сортировки по возрасту (по убыванию)
public class AgeDescendingComparer : IComparer<Person>
{
public int Compare(Person? x, Person? y)
{
if (x is null || y is null) return 0;
return y.Age.CompareTo(x.Age); // обратный порядок
}
}
// Использование:
var people = new List<Person> { /* ... */ };
people.Sort(new NameComparer()); // по имени
people.Sort(new AgeDescendingComparer()); // по возрасту (убыв.)
Сортировка с делегатом (Comparison<T>):
Не обязательно создавать отдельный класс. Можно передать делегат Comparison<T> прямо в Sort():
people.Sort((p1, p2) => p1.Age.CompareTo(p2.Age)); // по возрасту
people.Sort((p1, p2) => string.Compare(p1.Name, p2.Name)); // по имени
LINQ: OrderBy() и OrderByDescending()
LINQ-методы OrderBy() и OrderByDescending() — это extension методы, которые не изменяют исходную коллекцию, а возвращают новую отсортированную последовательность.
var people = new List<Person> { /* ... */ };
// Сортировка по возрастанию (OrderBy)
var sortedByAge = people.OrderBy(p => p.Age);
sortedByAge.ToList().ForEach(p => Console.WriteLine(p));
// Сортировка по убыванию (OrderByDescending)
var sortedByNameDesc = people.OrderByDescending(p => p.Name);
// Сортировка с селектором свойства
var sortedByName = people.OrderBy(p => p.Name);
// Можно передать компаратор
var sortedCustom = people.OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase);
List.Sort(), LINQ-методы не изменяют исходный список, а возвращают новую последовательность. Если нужно сохранить результат, вызови .ToList() или .ToArray().Многоуровневая сортировка: ThenBy()
Часто нужно сортировать по нескольким критериям: сначала по возрасту, затем по имени (для одинакового возраста). Для этого используется ThenBy() и ThenByDescending().
public class Product
{
public string Category { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public override string ToString() =>
$"{Category} | {Name} | {Price:C}";
}
var products = new List<Product>
{
new() { Category = "Книги", Name = "C# для начинающих", Price = 500 },
new() { Category = "Книги", Name = "Алгоритмы", Price = 800 },
new() { Category = "Электроника", Name = "Наушники", Price = 3000 },
new() { Category = "Книги", Name = "Паттерны", Price = 500 },
new() { Category = "Электроника", Name = "Клавиатура", Price = 2500 },
};
// Сначала по категории, затем по цене, затем по имени
var sorted = products
.OrderBy(p => p.Category) // 1. категория (А-Я)
.ThenBy(p => p.Price) // 2. цена (по возрастанию)
.ThenBy(p => p.Name); // 3. имя (А-Я)
// Вывод:
foreach (var p in sorted)
Console.WriteLine(p);
// Книги | C# для начинающих | 500,00₽
// Книги | Паттерны | 500,00₽
// Книги | Алгоритмы | 800,00₽
// Электроника | Клавиатура | 2 500,00₽
// Электроника | Наушники | 3 000,00₽
ThenBy() применяется только после OrderBy(). Можно сделать цепочку любой длины. Для обратного порядка используй ThenByDescending().Сортировка сложных объектов
В реальных проектах объекты часто содержат вложенные объекты. Сортировка по свойству вложенного объекта тоже проста с LINQ.
public class Order
{
public int Id { get; set; }
public Customer Customer { get; set; }
public decimal Total { get; set; }
public DateTime CreatedAt { get; set; }
}
public class Customer
{
public string Name { get; set; }
public string City { get; set; }
}
// Сортировка заказов по городу клиента, затем по сумме
var orders = GetOrders(); // List<Order>
var sortedOrders = orders
.OrderBy(o => o.Customer.City) // по городу
.ThenByDescending(o => o.Total) // по сумме (от большей к меньшей)
.ThenBy(o => o.CreatedAt); // по дате
// Сортировка с проверкой null
var safeSorted = orders
.Where(o => o.Customer is not null)
.OrderBy(o => o.Customer!.City ?? "Без города")
.ToList();
Reverse() — обратный порядок
Reverse() существует в двух вариантах: Array.Reverse() / List.Reverse() (изменяет коллекцию на месте) и Enumerable.Reverse() (LINQ, возвращает новую последовательность).
// In-place (изменяет исходный)
int[] arr = { 1, 2, 3, 4, 5 };
Array.Reverse(arr);
Console.WriteLine(string.Join(", ", arr)); // → 5, 4, 3, 2, 1
List<int> list = new() { 1, 2, 3, 4, 5 };
list.Reverse();
Console.WriteLine(string.Join(", ", list)); // → 5, 4, 3, 2, 1
// LINQ (не изменяет исходный)
int[] original = { 1, 2, 3, 4, 5 };
var reversed = original.Reverse(); // новый IEnumerable
Console.WriteLine(string.Join(", ", reversed)); // → 5, 4, 3, 2, 1
Console.WriteLine(string.Join(", ", original)); // → 1, 2, 3, 4, 5
Мини-тест
Вопрос 1: Какой интерфейс нужно реализовать для «естественной» сортировки класса?
Правильный ответ: C
Вопрос 2: Возвращает ли OrderBy() новую коллекцию или изменяет старую?
Правильный ответ: B
Вопрос 3: Как сделать сортировку сначала по городу, затем по имени?
Правильный ответ: D
Вопрос 4: Что вернёт метод CompareTo() если объекты равны?
Правильный ответ: C
Вопрос 5: Какой интерфейс позволяет определить несколько разных способов сравнения для одного класса?
Правильный ответ: B
Задание
Сортировка сотрудников
Создай консольное приложение для управления списком сотрудников:
- Класс
Employeeсо свойствами:Name,Department,Salary(decimal),HireDate(DateTime). - Создай список из 10 сотрудников (разные отделы, зарплаты, даты).
- Реализуй IComparable<Employee> для сортировки по зарплате по умолчанию.
- Создай
NameComparer : IComparer<Employee>(по имени). - Создай
HireDateComparer : IComparer<Employee>(по дате). - Выведи сотрудников, отсортированных: сначала по отделу, затем по зарплате (убывание), затем по имени.
- Используй LINQ OrderBy, ThenBy, ThenByDescending.
- Дополнительно: используй Reverse() и делегат Comparison<T>.
Цель
Отработать все способы сортировки: IComparable, IComparer, LINQ, делегаты.
Сложность
Средняя
Что важно запомнить
Тест: 8.11: Сортировка и сравнение
3 вопроса