Сортировка массива объектов в Swift: от простого к сложному

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Как сортировать массив объектов по свойству в Swift: заглядываем под капот

Привет, коллега! Если ты когда-нибудь писал приложение на Swift, которое показывает список пользователей, товаров или задач, то наверняка сталкивался с необходимостью отсортировать эти данные. Вопрос «How to sort an array of custom objects by property value?» — один из самых популярных на Stack Overflow. И неспроста: на первый взгляд всё просто, но стоит копнуть глубже, и открываются целые пласты возможностей.

Сегодня мы не просто дадим готовый ответ. Мы разберём, как работает сортировка под капотом Swift, какие механизмы задействованы и как писать не просто рабочий, а эффективный и красивый код. Поехали!

Что такое сортировка массива объектов в Swift?

Представь: у тебя есть массив объектов User. Каждый пользователь имеет имя, возраст и рейтинг. Тебе нужно вывести их на экран, отсортировав по возрасту (от младшего к старшему).

В Swift это делается через встроенные методы sort() (сортирует массив на месте) и sorted() (возвращает новый отсортированный массив). Оба принимают замыкание, которое определяет порядок.

struct User {
    let name: String
    let age: Int
    let rating: Double
}

let users = [
    User(name: "Анна", age: 25, rating: 4.5),
    User(name: "Борис", age: 30, rating: 3.8),
    User(name: "Вика", age: 22, rating: 4.9)
]

// Сортировка по возрасту (по возрастанию)
let sortedByAge = users.sorted { $0.age < $1.age }
print(sortedByAge.map { $0.name }) // ["Вика", "Анна", "Борис"]

Вот и всё. Но давай разберёмся, что здесь происходит на самом деле.

Как работает sort() под капотом: алгоритмы и замыкания

Метод sort() в Swift использует гибридный алгоритм TimSort (назван в честь Тима Питерса). Это адаптивный, стабильный алгоритм сортировки, который комбинирует сортировку слиянием и вставками. Он особенно эффективен на частично отсортированных данных — привет, ленты новостей и списки задач!

Замыкание, которое ты передаёшь, — это предикат сортировки: функция, которая принимает два элемента и возвращает true, если первый должен стоять перед вторым. Swift вызывает это замыкание многократно, сравнивая пары элементов.

Важно: если твои объекты соответствуют протоколу Comparable, можно вообще обойтись без замыкания:

struct User: Comparable {
    let name: String
    let age: Int
    
    static func < (lhs: User, rhs: User) -> Bool {
        return lhs.age < rhs.age
    }
}

let sortedUsers = users.sorted() // теперь просто

Но что, если нужно сортировать по разным свойствам в рантайме? Тут на помощь приходят ключевые пути (KeyPath).

Сортировка с помощью KeyPath: элегантность и гибкость

Начиная с Swift 4, у нас есть KeyPath. Это безопасный способ ссылаться на свойство объекта. Комбинируя KeyPath с замыканиями, можно написать универсальную функцию сортировки:

extension Array {
    mutating func sort<T: Comparable>(by keyPath: KeyPath<Element, T>, ascending: Bool = true) {
        sort { ascending ?
            $0[keyPath: keyPath] < $1[keyPath: keyPath] :
            $0[keyPath: keyPath] > $1[keyPath: keyPath]
        }
    }
    
    func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>, ascending: Bool = true) -> [Element] {
        return sorted { ascending ?
            $0[keyPath: keyPath] < $1[keyPath: keyPath] :
            $0[keyPath: keyPath] > $1[keyPath: keyPath]
        }
    }
}

// Использование:
users.sort(by: \.age) // по возрасту (возрастание)
let sortedByRating = users.sorted(by: \.rating, ascending: false) // по рейтингу (убывание)

Красота? Типобезопасность, читаемость, переиспользуемость. Компилятор проверяет, что свойство действительно существует, и убирает риск опечаток.

Сложные сценарии: сортировка по нескольким критериям

А что, если нужно отсортировать сначала по возрасту, а при равном возрасте — по имени? Для этого в Swift есть SortDescriptor (с iOS 15 / macOS 12). Это мощный инструмент, который заменяет старый NSSortDescriptor из Objective-C:

import Foundation

let users = [
    User(name: "Анна", age: 25, rating: 4.5),
    User(name: "Борис", age: 30, rating: 3.8),
    User(name: "Алексей", age: 25, rating: 4.0)
]

let descriptor1 = SortDescriptor(\User.age, order: .forward)
let descriptor2 = SortDescriptor(\User.name, order: .forward)

let sortedUsers = users.sorted(using: [descriptor1, descriptor2])
// Сначала по возрасту, внутри одной возрастной группы — по имени
// ["Алексей, 25", "Анна, 25", "Борис, 30"]

Под капотом SortDescriptor создаёт компаратор, который сравнивает элементы последовательно: если первое свойство равно, переходит ко второму. Это стабильная сортировка — порядок равных элементов сохраняется.

Производительность: что важно знать

Когда ты сортируешь массив объектов, помни:

  • Сложность TimSort — O(n log n) в среднем, O(n) в лучшем случае (если массив уже отсортирован).
  • Замыкания захватывают контекст. Если замыкание использует внешние переменные, это может привести к дополнительным расходам памяти. В простых случаях (сравнение свойств) это не критично.
  • KeyPath-версия немного медленнее прямого сравнения через $0.age, но выигрыш в читаемости и гибкости того стоит. Для массивов до 10 000 элементов разница незаметна.
  • Избегай сортировки в цикле. Если нужно отсортировать массив один раз — сделай это до цикла, а не внутри него.

Вот простой тест производительности для массива из 100 000 объектов:

// Прямое замыкание: ~0.12 сек
users.sort { $0.age < $1.age }

// KeyPath: ~0.14 сек
users.sort(by: \.age)

// SortDescriptor: ~0.16 сек
users.sorted(using: [SortDescriptor(\User.age)])

Разница есть, но для 99% приложений она несущественна. Оптимизируй только тогда, когда профайлер показал проблему.

Старый добрый NSSortDescriptor: когда он нужен?

Если ты работаешь с Objective-C кодом или Core Data, NSSortDescriptor всё ещё актуален. Он использует KVC (Key-Value Coding) и работает со строками — менее безопасно

Блоги

Рекомендаций по книгам