1. Почему generic-функция без ограничений почти ничего не может
Если смотреть на generics без ограничений, T напоминает сотрудника, которого вы наняли, но забыли спросить резюме. Он точно «человек» (ну, или «тип»), но умеет ли он читать, писать, сравнивать числа — неизвестно. Поэтому компилятор ведёт себя строго: «не доказано — значит нельзя».
Это не занудство ради занудства. Это тот самый принцип Swift: лучше честная ошибка компиляции сегодня, чем загадочный баг у пользователя завтра. И ограничения как раз превращают «тип-загадку» в «тип с договором».
Пример: generic без ограничений умеет принять и вернуть T, но не умеет использовать операции, которых может не существовать.
import Foundation
func maxOf<T>(_ a: T, _ b: T) -> T {
// return a > b ? a : b // ❌ не компилируется: компилятор не знает, что у T есть ">"
return a // это компилируется: вернуть T можно
}
И это логично: «больше/меньше» существует не для всех типов. Например, «что больше: URL или DateFormatter?» — вопрос философский, не технический.
2. Constraint как переключатель возможностей: T:Comparable
Сравнение и сортировка — одна из самых частых причин, по которой мы вообще вспоминаем про generics. В прикладном коде постоянно возникает задача: выбрать максимум, отсортировать список, зажать значение в диапазон, найти границу. И хочется сделать это один раз красиво, а не писать «версии под Int, Double, String».
Протокол Comparable — это и есть «пропуск» в мир порядка. Если тип T соответствует Comparable, значит его значения можно сравнивать и сортировать (в терминах Swift — есть естественный порядок).
Пример: maxOf начинает «иметь право» использовать >.
import Foundation
func maxOf<T: Comparable>(_ a: T, _ b: T) -> T {
return a > b ? a : b
}
print(maxOf(10, 3)) // 10
print(maxOf("b", "a")) // b
Обратите внимание на приятную магию: одна функция работает для Int и для String. Это не магия на самом деле — это контракт. Int и String уже реализуют Comparable, поэтому подходят.
Теперь важное наблюдение, которое экономит много нервов: constraint — это не «украшение» сигнатуры. Он буквально отвечает на вопрос «какие операции разрешены внутри функции».
Чтобы это держать в голове, можно представлять такой мини-конвейер:
flowchart LR
A["Вы вызываете функцию"] --> B["Компилятор выводит T"]
B --> C["Проверяет: T соответствует Comparable?"]
C -->|да| D["Разрешает >, <, sorted() и т.п."]
C -->|нет| E["Ошибка компиляции: 'нельзя сравнивать'"]
3. Constraint T:Hashable: зачем он нужен Set и ключам Dictionary
Если Comparable отвечает за «порядок», то Hashable — за «быстрый поиск» и «уникальность». В Swift (и вообще в большинстве языков) Set и ключи Dictionary строятся на хешировании: мы быстро вычисляем «корзинку» (bucket), куда положить элемент, и из-за этого проверка «есть ли такой элемент» обычно работает очень быстро.
Именно поэтому Set<Element> требует, чтобы Element был Hashable. Это не прихоть: если элемент не умеет хешироваться, то «корзинки» нет, и структура данных теряет смысл.
Пример: generic-функция для поиска дубликатов через Set обязана требовать Hashable.
import Foundation
func hasDuplicates<T: Hashable>(_ items: [T]) -> Bool {
var seen = Set<T>()
for item in items {
if seen.contains(item) { return true }
seen.insert(item)
}
return false
}
print(hasDuplicates([1, 2, 2, 3])) // true
print(hasDuplicates(["a", "b", "c"])) // false
Тут очень «честная» связь: как только внутри вы написали Set<T>(), вы автоматически обязаны сказать в сигнатуре: «ребята, T должен быть Hashable». И это прямой пример того, как ограничения документируют код лучше любого комментария.
4. Comparable, Hashable, Equatable: выбираем минимально достаточный контракт
Ограничения — это ещё и дизайн API. Если вы требуете от типа больше, чем нужно, вы ограничиваете пользователей функции. Это как сказать: «Чтобы зайти в библиотеку, предъявите паспорт, водительские права и диплом магистра по философии». Формально можно, но зачем?
Есть полезное правило: требуйте минимально достаточный контракт. То есть ровно то, что нужно для операций внутри.
Ниже — маленькая таблица «операция → какой протокол нужен». Это не «надо выучить наизусть», но удобно как шпаргалка.
| Что вы хотите сделать в функции | Что вам нужно от T | Как пишется constraint |
|---|---|---|
| Сравнить на равенство == | |
|
| Сравнить по порядку >, <, сортировать | |
|
| Положить в Set, сделать ключом Dictionary, быстро проверять contains | |
|
Небольшая тонкость: Comparable логически включает идею равенства (потому что порядок обычно предполагает сравнимость), но на уровне мышления лучше так: если вам нужен только ==, не просите Comparable.
Пример: «не завышаем требования»: для == достаточно Equatable.
import Foundation
func areEqual<T: Equatable>(_ a: T, _ b: T) -> Bool {
return a == b
}
print(areEqual(1, 1)) // true
print(areEqual("a", "b")) // false
Если бы мы написали T:Comparable, код работал бы тоже, но мы бы искусственно запретили типы, которые равны (Equatable), но не упорядочены (Comparable).
5. Комбинируем ограничения и применяем в коде
Несколько ограничений сразу: T: Hashable & Comparable
Иногда вам нужно и «уникальность», и «сортировка». Например, вы хотите удалить повторы, а потом вывести результат в красивом порядке. Тогда ограничения можно комбинировать: T: Hashable & Comparable.
Это читается как: «тип T должен соответствовать обоим протоколам». И да, это именно композиция протоколов на уровне constraints, а не какая-то отдельная магия.
Пример: «уникальные + отсортированные».
import Foundation
func uniqueSorted<T: Hashable & Comparable>(_ items: [T]) -> [T] {
return Array(Set(items)).sorted()
}
print(uniqueSorted([3, 1, 3, 2])) // [1, 2, 3]
Здесь очень наглядно, почему нужны оба протокола. Set требует Hashable, а .sorted() требует Comparable. И сигнатура это честно отражает.
Встраиваем в консольное приложение: мини‑библиотека без копипасты
Сейчас мы сделаем шаг в сторону учебного приложения (условный CLI, который работает с книгами). Не будем усложнять архитектуру и придумывать 12 слоёв абстракций — мы пока учимся пользоваться generics как инструментом, а не строить «библиотеку для библиотек».
Представим, что в main.swift у нас уже есть простейшая модель книги: id и title. Мы хотим уметь печатать отчёты: уникальные ID, уникальные названия, «топ» по алфавиту и так далее.
Мини-модель и тестовые данные (всё ещё максимально учебно):
import Foundation
struct Book {
let id: Int
let title: String
}
let books = [
Book(id: 2, title: "Swift Basics"),
Book(id: 1, title: "Algorithms"),
Book(id: 2, title: "Swift Basics")
]
Теперь задача: вывести список уникальных ID книг. Int — Hashable, поэтому мы можем сделать функцию, которая работает для любых Hashable элементов, не только для Int.
Универсальная функция «уникальные элементы» через Set:
import Foundation
func uniqueItems<T: Hashable>(_ items: [T]) -> [T] {
return Array(Set(items))
}
let ids = books.map { $0.id }
print(uniqueItems(ids)) // порядок не гарантирован, пример: [2, 1]
Обратите внимание на комментарий про порядок: Set не обязан сохранять порядок вставки. Это не баг, это контракт структуры данных. Если вам важно сохранять порядок — это отдельный разговор (и отдельный алгоритм), и мы не будем забегать вперёд.
Теперь сделаем похожую функцию, но уже для красивого отчёта: «уникальные элементы, отсортированные».
«Уникальные + отсортированные» (и снова generics):
import Foundation
func uniqueSortedItems<T: Hashable & Comparable>(_ items: [T]) -> [T] {
return Array(Set(items)).sorted()
}
let titles = books.map { $0.title }
print(uniqueSortedItems(titles)) // ["Algorithms", "Swift Basics"]
В рамках CLI это уже выглядит как полезный строительный блок: вы можете так печатать уникальные теги, уникальные авторы, уникальные ID читателей — что угодно, лишь бы тип был Hashable (и Comparable, если хотите сортировку).
Как читать сигнатуры: Comparable и Hashable в реальном Swift
Когда вы начинаете активно использовать стандартную библиотеку, вы замечаете: constraints там везде. И это хорошо: вы буквально читаете сигнатуру и понимаете, «что требуется» и «что гарантируется».
Например, в Swift Evolution (документах развития языка) можно встретить структуры, которые прямо объявляются как generic по Comparable типу. У RangeSet (тип, представляющий набор диапазонов) параметр Bound обязан быть Comparable, потому что диапазоны без порядка не имеют смысла: надо уметь сравнивать границы диапазонов. В описании API это выглядит как RangeSet<Bound: Comparable>.
Ещё один наглядный пример — generic typealias, где constraint вынужден «повторяться» ради ясности: если внутри вы используете Dictionary<T, ...>, то T обязан быть Hashable, иначе ключи словаря невозможны. Именно поэтому встречаются записи вроде typealias DictionaryOfStrings<T: Hashable> = Dictionary<T, String>.
И маленький факт для расширения кругозора: в обсуждениях языка отдельно поднимается тема, что базовые протоколы вроде Equatable, Comparable, Hashable — суперважны, потому что они открывают двери к огромному числу операций стандартной библиотеки: поиск, сортировки, множества и т.д.
В учебном коде это ощущается очень просто: как только тип «стал Hashable», он внезапно начинает жить полной жизнью в Set и Dictionary.
6. Типичные ошибки
Ошибка №1: пытаться использовать < или sorted() на T без T:Comparable.
Это одна из самых частых «первых» ошибок при generics: вы написали красивую функцию maxOf<T>, а внутри — a > b. Компилятор честно говорит: «я не знаю, что такое > для T». Исправление всегда одно и то же: понять, какая операция нужна, и добавить минимально достаточный constraint.
Ошибка №2: требовать Hashable, когда на самом деле достаточно Equatable.
Если вы делаете линейный поиск, сравниваете соседние элементы или проверяете равенство двух значений, вам может быть достаточно Equatable. Hashable — более «сильное» требование: далеко не каждый тип обязан быть корректно хешируемым. Завышенные constraints делают ваш код менее универсальным, хотя вы вроде бы пишете generics ради универсальности.
Ошибка №3: забывать, что Set не гарантирует порядок, и удивляться «случайному» выводу.
Очень по-человечески ожидать, что Set вернёт элементы в том же порядке, в котором вы их вставили. Но Set — не про порядок, а про уникальность и быстрый contains. Поэтому, когда вы делаете Array(Set(items)), порядок может быть любым. Если ваш отчёт должен быть стабильным и красивым, следующий логичный шаг — отсортировать (а значит — добавить Comparable), как мы сделали в uniqueSortedItems.
Ошибка №4: добавлять T: Hashable & Comparable «на всякий случай».
Иногда хочется написать «самый мощный» constraint и больше никогда не думать. Но это снижает ценность generics: вы начинаете требовать от типа слишком много. Хороший стиль — сначала написать тело функции, увидеть, какие операции реально используются, и только потом сформулировать constraints как честный контракт.
Ошибка №5: путать constraint с приведением типов и ожидать, что as? Comparable решит проблему.
Constraint — это условие на этапе компиляции, а не «попробуем в рантайме». Если функция объявлена как func f<T>(_ x: T), то внутри неё компилятор не позволит обращаться к возможностям Comparable, даже если в конкретном вызове вы передали Int. В generics «разрешение на операцию» даётся именно через constraints в сигнатуре, а не через хитрые попытки уговорить рантайм.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ