$ sudo teach IT
Модуль 8 · Функции

Урок 8.11 — Сортировка и сравнение

Array.Sort, IComparable, LINQ OrderBy и сортировка сложных объектов.

Когда у тебя есть список чисел, его легко отсортировать — числа сравниваются естественно. Но что, если нужно отсортировать список людей по возрасту, затем по фамилии, или список заказов по сумме? C# предоставляет гибкие механизмы сортировки: от простого Array.Sort() до мощных LINQ-методов с делегатами.

🔢

Array.Sort() и List.Sort()

Самый простой способ отсортировать массив или список — использовать встроенные методы Array.Sort() и List.Sort(). Они работают «на месте» (in-place), изменяя исходную коллекцию.

C# · Array.Sort и List.Sort
// Массивы
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 — объекты равны.
  • Положительное число — текущий объект больше другого (должен идти позже).
C# · IComparable<T>
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> позволяет определить отдельный класс-компаратор для каждой стратегии сравнения.

C# · 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 методы, которые не изменяют исходную коллекцию, а возвращают новую отсортированную последовательность.

C# · OrderBy / OrderByDescending
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().

C# · 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.

C# · Сортировка по вложенным свойствам
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, возвращает новую последовательность).

C# · Reverse
// 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: Какой интерфейс нужно реализовать для «естественной» сортировки класса?

A) IEnumerable<T>
B) IDisposable
C) IComparable<T>
D) IComparer<T>

Правильный ответ: C

Вопрос 2: Возвращает ли OrderBy() новую коллекцию или изменяет старую?

A) Изменяет старую
B) Возвращает новую последовательность
C) Может делать и то, и другое
D) Выдаёт ошибку

Правильный ответ: B

Вопрос 3: Как сделать сортировку сначала по городу, затем по имени?

A) OrderBy(city).ThenBy(name)
B) SortBy(city).ThenBy(name)
C) OrderBy(city, name)
D) OrderBy(city).ThenBy(name) — правильно

Правильный ответ: D

Вопрос 4: Что вернёт метод CompareTo() если объекты равны?

A) 1
B) -1
C) 0
D) true

Правильный ответ: C

Вопрос 5: Какой интерфейс позволяет определить несколько разных способов сравнения для одного класса?

A) IComparable<T>
B) IComparer<T>
C) IComparable<T> + IComparer<T>
D) ISortable<T>

Правильный ответ: B

🧪

Задание

Сортировка сотрудников

Создай консольное приложение для управления списком сотрудников:

  1. Класс Employee со свойствами: Name, Department, Salary (decimal), HireDate (DateTime).
  2. Создай список из 10 сотрудников (разные отделы, зарплаты, даты).
  3. Реализуй IComparable<Employee> для сортировки по зарплате по умолчанию.
  4. Создай NameComparer : IComparer<Employee> (по имени).
  5. Создай HireDateComparer : IComparer<Employee> (по дате).
  6. Выведи сотрудников, отсортированных: сначала по отделу, затем по зарплате (убывание), затем по имени.
  7. Используй LINQ OrderBy, ThenBy, ThenByDescending.
  8. Дополнительно: используй Reverse() и делегат Comparison<T>.

Цель

Отработать все способы сортировки: IComparable, IComparer, LINQ, делегаты.

Сложность

Средняя

📌

Что важно запомнить

1️⃣
Array.Sort() и List.Sort() — простые методы для сортировки на месте. Работают для примитивов и классов с IComparable.
2️⃣
IComparable<T> — естественный порядок (метод CompareTo). IComparer<T> — отдельные классы для разных стратегий.
3️⃣
LINQ: OrderBy() / OrderByDescending() — не изменяют исходную коллекцию, возвращают новую последовательность.
4️⃣
ThenBy() / ThenByDescending() — для многоуровневой сортировки. Сначала OrderBy, потом ThenBy сколько угодно раз.
5️⃣
Reverse() — обратный порядок (in-place или LINQ). Comparison<T> — делегат для быстрой сортировки без отдельных классов.

Тест: 8.11: Сортировка и сравнение

3 вопроса

Сортировка

Premium