JavaRush /Курсы /Swift SELF /Сравнимость объектов: Comparable

Сравнимость объектов: Comparable

Swift SELF
19 уровень , 1 лекция
Открыта

1. После равенства появляется следующий вопрос: кто раньше

В прошлой лекции мы говорили о равенстве. Это был вопрос «одно и то же значение или нет». Но в реальном коде очень быстро появляется другая задача. Нужно понять, что меньше, что больше, что должно стоять раньше, а что позже.

На самом деле вы уже и это делали много раз. Когда писали score > 0, age >= 18, x < y, вы пользовались идеей порядка. Просто раньше это выглядело как обычные операторы в условии, а теперь мы посмотрим на них как на общую способность типов.

let a = 7
let b = 12

print(a == b) // false
print(a < b)  // true
print(a > b)  // false

Здесь особенно полезно увидеть разницу между равенством и порядком. a == b спрашивает «это одно и то же число?». a < b спрашивает «левое значение меньше правого?». Это не один и тот же вопрос, хотя внешне всё выглядит как «ну мы же просто что-то сравнили».

Точно так же это работает и со строками:

let first = "apple"
let second = "banana"

print(first == second) // false
print(first < second)  // true

Так что в голове полезно держать два разных слоя. Равенство отвечает за одинаковость. Порядок отвечает за расположение значений на линии «меньше → больше» или «раньше → позже».

2. У этой способности есть имя: Comparable

Теперь можно назвать то, чем вы уже пользовались. В Swift способность участвовать в сравнении через <, >, <= и >= имеет официальное имя — протокол Comparable.

В стандартной библиотеке он объявлен так:

public protocol Comparable: Equatable {
    static func < (lhs: Self, rhs: Self) -> Bool
}

На первый взгляд запись выглядит немного технически, но внутри скрывается очень простая идея.

Она говорит буквально следующее: тип, который поддерживает Comparable, умеет отвечать на вопрос «левое значение меньше правого?». Для этого язык вызывает специальную функцию сравнения <. В неё передаются два значения одного и того же типа (lhs и rhs), а результат — логическое значение Bool.

Если перевести это на обычный язык, получается знакомая картина: когда вы пишете

a < b

язык просто вызывает функцию сравнения для типа этих значений и получает true или false.

Полезно заметить и другую деталь в объявлении протокола. Comparable наследуется от Equatable. Это означает, что любой тип, который имеет естественный порядок, автоматически должен уметь и сравниваться на равенство.

Это логично: если мы умеем сказать, что одно значение меньше другого, мы должны уметь и ответить на вопрос, равны ли они.

Как и в прошлой лекции, пока не нужно углубляться в синтаксис протоколов. До этой темы мы ещё дойдём позже. Сейчас достаточно простой рабочей модели: если тип имеет понятный естественный порядок, язык связывает эту способность с Comparable.

Именно поэтому числа (Int, Double) и строки (String) спокойно участвуют в сравнениях через < и >. У них уже есть определённый порядок. Просто раньше вы пользовались этим, не задумываясь о том, как эта способность формально описана в языке.

3. Какие базовые типы уже имеют естественный порядок

Самые понятные примеры — числа. Для Int естественный порядок буквально совпадает со школьным: меньшее число стоит раньше, большее — позже.

let x = -3
let y = 2

print(x < y) // true
print(y > x) // true

Для Double идея та же самая. Числа с плавающей точкой тоже можно упорядочивать как числа.

let p = 0.5
let q = 0.75

print(p < q) // true

Для строк порядок тоже есть, но он уже не «числовой», а лексикографический. Грубо говоря, Swift сравнивает строки слева направо.

let a = "apple"
let b = "banana"

print(a < b) // true

Здесь сразу появляется важная ловушка. Строка "10" — это не число десять. Это строка из символов "1" и "0". Поэтому строковое сравнение и числовое сравнение — не одно и то же.

print("10" < "2") // true

На первый взгляд результат кажется странным, но для строк он логичен. Swift смотрит на первые символы. "1" идёт раньше "2", значит и строка "10" считается меньшей, чем "2". Это не ошибка языка. Это ошибка ожиданий, если мы забыли, что работаем со строками, а не с числами.

Именно поэтому так важно видеть связь между типом данных и тем, какой порядок для него считается естественным. У Int и Double это числовой порядок. У String — строковый порядок. У Bool стандартного порядка нет.

4. Где этот порядок уже работает в стандартной библиотеке

Как только у типа есть естественный порядок, стандартная библиотека может строить на нём полезные операции. Самые простые примеры — min() и max() у массива.

let numbers = [4, 1, 9, 2]

print(numbers.min() as Any) // Optional(1)
print(numbers.max() as Any) // Optional(9)

Почему здесь возвращается Optional, вы уже должны чувствовать интуитивно. У пустого массива нет минимального и максимального элемента. Значит, честный результат — либо значение, либо nil.

let empty: [Int] = []

print(empty.min() as Any) // nil
print(empty.max() as Any) // nil

Тот же естественный порядок используется и в сортировке. В следующей лекции мы будем разбирать сортировку подробно, но уже сейчас полезно увидеть связь. Массив строк можно отсортировать «по умолчанию» именно потому, что String имеет естественный порядок.

let titles = ["Dune", "1984", "Brave New World"]
print(titles.sorted()) // ["1984", "Brave New World", "Dune"]

То есть Comparable — это не декоративное название. Это основа для целого семейства операций: взять минимум, взять максимум, отсортировать данные, поддерживать порядок в алгоритмах и дальше, чуть позже, делать бинарный поиск.

5. Строгий и нестрогий порядок — это не одно и то же

Операторы порядка выглядят похожими, но между ними есть важная логическая разница. Операторы < и > задают строгий порядок. Операторы <= и >= разрешают ситуацию равенства.

Это видно на простом примере:

let x = 10
let y = 10

print(x < y)  // false
print(x <= y) // true

Когда вы спрашиваете «меньше ли x, чем y», ответ false, потому что значения равны. Но когда вы спрашиваете «меньше или равно ли», ответ уже true.

Почему это важно не только теоретически? Потому что в алгоритмах и в правилах сортировки обычно нужен именно строгий ответ на вопрос «кто должен стоять раньше». Если вместо этого дать нестрогое правило, вы легко попадёте в ситуацию, где два равных элемента как будто оба «подходят быть раньше друг друга». Для обычного if это может быть терпимо. Для сортировки и поиска — уже опасно.

Поэтому полезная привычка звучит так: когда вы описываете порядок, думайте в терминах строгого сравнения, а не «ну пусть и равенство тоже туда входит».

6. Optional<Int> не имеет естественного порядка по умолчанию

Здесь очень полезно сравнить нынешнюю лекцию с прошлой. В прошлой лекции Optional<Int> прекрасно участвовал в равенстве. Можно было спросить, равен ли он nil, или равны ли два опционала между собой.

С порядком всё уже не так просто. Как сравнивать nil и число? nil меньше нуля? Больше любого числа? Всегда в начале? Всегда в конце? Для разных задач ответы могут быть разными.

Поэтому у Optional нет одного универсального естественного порядка, который Swift мог бы навязать всем случаям.

let a: Int? = 3
let b: Int? = nil

// print(a < b) // так нельзя

Правильный подход — сначала распаковать значения, а потом уже сравнивать обычные Int.

let a: Int? = 3
let b: Int? = 10

if let a, let b {
    print(a < b) // true
} else {
    print("Есть nil, нужно отдельно решить, что с ним делать")
}

Это очень полезный контраст. Для равенства Optional можно сравнить автоматически, потому что модель проста: nil или значение. Для порядка универсальной модели нет. Поэтому порядок требует явного решения с вашей стороны.

7. Сравнение по нескольким полям

В реальных задачах одно поле редко бывает единственным критерием. Обычно есть главный ключ и дополнительный. Например, книги можно сравнивать сначала по году, а если год совпал — по названию. Людей можно сравнивать сначала по фамилии, а потом по имени.

Пока мы ещё не дошли до struct, поэтому удобно использовать кортежи. Пусть книга выглядит так: (title: String, year: Int).

let a = (title: "Clean Code", year: 2008)
let b = (title: "The Pragmatic Programmer", year: 1999)

Если правило такое: раньше та книга, у которой год меньше, а при одинаковом годе — та, у которой название лексикографически меньше, то вручную это можно записать так:

func isEarlierBook(
    _ left: (title: String, year: Int),
    _ right: (title: String, year: Int)
) -> Bool {
    if left.year != right.year {
        return left.year < right.year
    }
    return left.title < right.title
}

Здесь хорошо видно, как равенство и порядок работают вместе. Сначала мы проверяем, есть ли ничья по главному полю. Если ничьей нет, сравниваем годы. Если есть, подключаем запасной ключ.

Проверим:

let b1 = (title: "A", year: 2020)
let b2 = (title: "B", year: 2020)

print(isEarlierBook(b1, b2)) // true
print(isEarlierBook(b2, b1)) // false

А теперь приятный трюк. Поскольку Int и String уже имеют естественный порядок, можно сравнивать не сами книги напрямую, а кортеж ключей.

func isEarlierBook2(
    _ left: (title: String, year: Int),
    _ right: (title: String, year: Int)
) -> Bool {
    (left.year, left.title) < (right.year, right.title)
}

Это работает, потому что кортеж сравнивается поэлементно слева направо. Сначала сравниваются годы. Если они равны, сравниваются названия. И снова видно главное: всё строится на уже знакомых базовых типах. Int умеет в порядок. String умеет в порядок. Значит, и комбинация этих ключей может использовать тот же принцип.

Логику такого сравнения удобно представлять как маленькое дерево решений:

flowchart TD
    A[Сравниваем две книги] --> B{Годы равны?}
    B -- нет --> C[Сравниваем year]
    B -- да --> D[Сравниваем title]

Чем полезен этот образ? Тем, что вы видите не «магическую строчку с кортежами», а обычную человеческую логику: сначала главный критерий, потом запасной.

8. Мини-пример: выбрать самую раннюю книгу без сортировки

До полноценной сортировки мы дойдём в следующей лекции. Но порядок уже можно использовать в простом алгоритме выбора лучшего значения за один проход.

Пусть у нас есть список книг в виде кортежей. Мы хотим найти самую раннюю книгу по правилу «меньший год, а при равном годе — меньшее название».

var books: [(title: String, year: Int)] = [
    (title: "Clean Code", year: 2008),
    (title: "The Pragmatic Programmer", year: 1999),
    (title: "Refactoring", year: 1999),
]

Сделаем функцию, которая вернёт либо лучшую книгу, либо nil, если список пустой:

func earliestBook(
    in books: [(title: String, year: Int)]
) -> (title: String, year: Int)? {
    guard var best = books.first else { return nil }

    for book in books.dropFirst() {
        if (book.year, book.title) < (best.year, best.title) {
            best = book
        }
    }

    return best
}

Проверка:

let best = earliestBook(in: books)
print(best as Any) // Optional((title: "Refactoring", year: 1999))

Здесь уже видно, зачем нам нужен естественный порядок. Мы не просто умеем сказать, что два значения равны или не равны. Мы умеем однозначно выбрать, какое из них «идёт раньше». А это уже основа для сортировки, поиска минимума, выбора лучшего варианта и, чуть позже, бинарного поиска.

9. Типичные ошибки

Ошибка №1: путать == и < как будто это одно и то же «сравнение».
Когда вы пишете условия, важно проговаривать вопрос, на который отвечает оператор. == — это «одно и то же?», а < — «кто раньше/меньше?». Если в голове вопрос один, а оператор другой, код будет выглядеть правдоподобно, но делать не то. Особенно часто это проявляется в «ничьих», когда нужен тай‑брейк.

Ошибка №2: использовать <= там, где нужен строгий порядок.
В задачах, где вы определяете «кто идёт раньше», <= почти всегда ухудшает ситуацию: два разных объекта могут оказаться “раньше или равны”, и вы потеряете однозначность. На маленьких примерах это может не проявляться, но в алгоритмах приводит к странным результатам и сложноуловимым багам.

Ошибка №3: ожидать от строк числового порядка.
Сортировка/сравнение строк — лексикографическое. Поэтому "10" < "2" может оказаться true, и это не ошибка Swift, а ошибка ожиданий. Если вы храните числа как строки, то для числового порядка сначала нужно преобразовать к Int, а уже потом сравнивать.

Ошибка №4: пытаться сравнивать Optional через < «как-нибудь».
С Optional порядок не задан по умолчанию, и это правильная строгость языка. Если вы пытаетесь сравнивать Int?, распакуйте значения и явно решите, что делать с nil: пропускать, считать “самым маленьким”, считать “самым большим” или вообще прекращать операцию. Автоматическая магия тут только навредит.

Ошибка №5: делать мульти‑ключевое сравнение без «ничьей».
Типичный баг: вы сравнили только по главному ключу (например, год), а если годы равны — возвращаете false и думаете, что всё нормально. В итоге у вас появляются значения, которые «не меньше и не больше» друг друга, но при этом не равны. Для некоторых сценариев это терпимо, но чаще ломает ожидания. Тай‑брейк (второй ключ) — не роскошь, а способ сделать порядок полноценным и предсказуемым.

1
Задача
Swift SELF, 19 уровень, 1 лекция
Недоступна
Кто впереди
Кто впереди
1
Задача
Swift SELF, 19 уровень, 1 лекция
Недоступна
Два взгляда на номер
Два взгляда на номер
1
Задача
Swift SELF, 19 уровень, 1 лекция
Недоступна
Самая ранняя запись
Самая ранняя запись
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ