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, уміє відповідати на запитання «ліве значення менше за праве?». Для цього Swift викликає спеціальну функцію порівняння <. До неї передаються два значення одного й того самого типу (lhs і rhs), а результатом є логічне значення Bool.
Якщо перекласти це звичайною мовою, виходить знайома картина: коли ви пишете
a < b
Swift просто викликає функцію порівняння для типу цих значень і отримує true або false.
Корисно помітити й іншу деталь в оголошенні протоколу. Comparable успадковується від Equatable. Це означає, що будь-який тип, який має природний порядок, автоматично повинен уміти й порівнюватися на рівність.
Це логічно: якщо ми вміємо сказати, що одне значення менше за інше, ми маємо вміти й відповісти на запитання, чи вони рівні.
Як і в минулій лекції, поки не потрібно занурюватися в синтаксис протоколів. До цієї теми ми ще дійдемо пізніше. Зараз достатньо простої робочої моделі: якщо тип має зрозумілий природний порядок, Swift пов’язує цю здатність із 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[Порівнюємо рік]
B -- так --> D[Порівнюємо назву]
Чим корисний цей образ? Тим, що ви бачите не «магічний рядок із кортежами», а звичайну людську логіку: спочатку головний критерій, потім запасний.
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 і думаєте, що все нормально. У результаті у вас зʼявляються значення, які «не менші й не більші» одне за одне, але при цьому не рівні. Для деяких сценаріїв це терпимо, але частіше ламає очікування. Тай-брейк, тобто другий ключ, — не розкіш, а спосіб зробити порядок повноцінним і передбачуваним.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ