Лекция 11. Обобщения (Generics)
1. Введение
Обобщения (Generics) — одна из самых мощных возможностей Swift. Они позволяют писать гибкий, переиспользуемый код, который работает с любыми типами, сохраняя полную типобезопасность на этапе компиляции. Большая часть стандартной библиотеки Swift построена на обобщениях: Array<Element>, Dictionary<Key, Value>, Optional<Wrapped>. В этой лекции мы разберём, зачем нужны обобщения, как их создавать и применять, сравним подход Swift с аннотациями типов в Python.
2. Проблема дублирования кода
Представим, что нужно написать функцию для обмена двух значений. Без обобщений — отдельная функция для каждого типа:
func swapInts(_ a: inout Int, _ b: inout Int) { let temp = a; a = b; b = temp}func swapDoubles(_ a: inout Double, _ b: inout Double) { let temp = a; a = b; b = temp}func swapStrings(_ a: inout String, _ b: inout String) { let temp = a; a = b; b = temp}Все три функции идентичны по логике — отличается только тип. Это нарушение принципа DRY.
3. Зачем нужны Generics
Обобщения решают эту проблему — вы пишете один экземпляр функции, а конкретный тип подставляется при вызове. Преимущества: переиспользуемость (один код для всех типов), типобезопасность (компилятор контролирует совместимость) и читаемость (меньше кода).
В Python функция изначально работает с любыми типами, но ошибки обнаруживаются в runtime:
def swap(a, b): return b, ax, y = swap(1, "hello") # Python не запрещает — ошибка проявится позжеSwift не допустит это: если параметры должны быть одного типа, компилятор гарантирует.
4. Обобщённые функции
Обобщённая функция объявляется с параметром типа в угловых скобках:
func swapValues<T>(_ a: inout T, _ b: inout T) { let temp = a a = b b = temp}
var x = 10, y = 20swapValues(&x, &y)print(x, y) // 20 10
var s1 = "привет", s2 = "мир"swapValues(&s1, &s2)print(s1, s2) // мир приветT — параметр типа, заменяемый конкретным типом при вызове. Компилятор выводит T автоматически. Значения разных типов — ошибка:
var a = 5var b = "текст"// swapValues(&a, &b) // Ошибка: Int и String — разные типы5. Параметры типа: соглашения об именах
Параметр типа — заполнитель в угловых скобках. Можно использовать несколько через запятую:
| Имя | Когда используется |
|---|---|
T, U, V | Общие параметры без конкретного смысла |
Element | Элемент коллекции |
Key, Value | Ключ и значение словаря |
Wrapped | Обёрнутое значение (Optional) |
Success, Failure | Результат операции (Result) |
Если параметр имеет смысловую нагрузку — дайте осмысленное имя:
func makePair<First, Second>(_ a: First, _ b: Second) -> (First, Second) { return (a, b)}let pair = makePair("Swift", 5) // ("Swift", 5)6. Обобщённые типы
Обобщёнными могут быть структуры, классы и перечисления. Классический пример — стек:
struct Stack<Element> { private var items: [Element] = []
mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element? { items.isEmpty ? nil : items.removeLast() } func peek() -> Element? { items.last } var isEmpty: Bool { items.isEmpty } var count: Int { items.count }}
var intStack = Stack<Int>()intStack.push(1); intStack.push(2)print(intStack.pop()!) // 2
var stringStack = Stack<String>()stringStack.push("один")print(stringStack.peek()!) // одинТип Element фиксируется при создании. Нельзя положить String в Stack<Int>.
Сравнение с Python:
from typing import TypeVar, GenericT = TypeVar("T")
class Stack(Generic[T]): def __init__(self): self._items: list[T] = [] def push(self, item: T) -> None: self._items.append(item)В Python Generic[T] — подсказка для mypy, интерпретатор её не проверяет.
7. Расширение обобщённых типов
При расширении обобщённого типа не нужно повторно указывать параметр — он уже доступен:
extension Stack { func toArray() -> [Element] { return items }
func map<U>(_ transform: (Element) -> U) -> Stack<U> { var newStack = Stack<U>() for item in items { newStack.push(transform(item)) } return newStack }}
var numbers = Stack<Int>()numbers.push(1); numbers.push(2); numbers.push(3)let strings = numbers.map { "Число: \($0)" }print(strings.toArray()) // ["Число: 1", "Число: 2", "Число: 3"]Метод map вводит собственный параметр U, отличный от Element.
8. Ограничения типов (type constraints)
Иногда функция должна работать не с любым типом, а с поддерживающими определённые операции:
func findIndex<T: Equatable>(of value: T, in array: [T]) -> Int? { for (index, item) in array.enumerated() { if item == value { return index } } return nil}
print(findIndex(of: 3, in: [1, 2, 3, 4])) // Optional(2)print(findIndex(of: "мир", in: ["привет", "мир"])) // Optional(1)<T: Equatable> — «T соответствует протоколу Equatable». Без этого компилятор не позволит ==. Несколько ограничений — через &:
func findMin<T: Comparable & CustomStringConvertible>(_ a: T, _ b: T) -> T { print("Сравниваем \(a) и \(b)") return a < b ? a : b}9. Ограничения через where-условия
Для сложных ограничений — where. Оно позволяет формулировать условия на параметры и ассоциированные типы:
func allItemsMatch<C1: Container, C2: Container>( _ c1: C1, _ c2: C2) -> Bool where C1.Item == C2.Item, C1.Item: Equatable { guard c1.count == c2.count else { return false } for i in 0..<c1.count { if c1[i] != c2[i] { return false } } return true}where также работает в расширениях:
extension Stack where Element: Equatable { func contains(_ item: Element) -> Bool { return items.contains(item) }}
var nums = Stack<Int>()nums.push(10); nums.push(20)print(nums.contains(10)) // trueМетод contains доступен только для стеков с Equatable-элементами.
10. Ассоциированные типы в протоколах (associatedtype)
Обобщённые протоколы в Swift создаются через ассоциированные типы:
protocol Container { associatedtype Item var count: Int { get } mutating func append(_ item: Item) subscript(i: Int) -> Item { get }}associatedtype Item определяется при реализации протокола:
struct AnyContainer<T>: Container { typealias Item = T private var items: [T] = [] var count: Int { items.count }
mutating func append(_ item: T) { items.append(item) } subscript(i: Int) -> T { return items[i] }}
var c = AnyContainer<String>()c.append("Swift")c.append("Generics")print(c[0], c.count) // Swift 211. Ограничения ассоциированных типов
Ассоциированные типы можно ограничивать протоколами:
protocol SortableContainer { associatedtype Item: Comparable var count: Int { get } mutating func append(_ item: Item) func sorted() -> [Item]}
struct NumberContainer: SortableContainer { private var items: [Int] = [] var count: Int { items.count } mutating func append(_ item: Int) { items.append(item) } func sorted() -> [Int] { items.sorted() }}
var nc = NumberContainer()nc.append(5); nc.append(1); nc.append(3)print(nc.sorted()) // [1, 3, 5]12. Обобщённые подиндексы (subscripts)
Подиндексы тоже могут быть обобщёнными:
extension AnyContainer { subscript<Indices: Sequence>(indices: Indices) -> [T] where Indices.Element == Int { var result: [T] = [] for index in indices { result.append(self[index]) } return result }}
var words = AnyContainer<String>()words.append("Обобщения"); words.append("в"); words.append("Swift")print(words[[0, 2]]) // ["Обобщения", "Swift"]13. Стандартные обобщённые типы Swift
Array<Element> и Dictionary<Key, Value>
let numbers: [Int] = [1, 2, 3] // Array<Int>let scores: [String: Int] = ["Анна": 95] // Dictionary<String, Int>Optional<Wrapped>
// enum Optional<Wrapped> { case none; case some(Wrapped) }let value: Int? = 42 // Optional<Int>Result<Success, Failure>
enum NetworkError: Error { case notFound }
func fetchData(url: String) -> Result<String, NetworkError> { url.contains("valid") ? .success("Данные") : .failure(.notFound)}
switch fetchData(url: "https://valid.example.com") {case .success(let data): print(data)case .failure(let error): print("Ошибка: \(error)")}14. Opaque types: some Protocol
Непрозрачные типы (Swift 5.1+) скрывают конкретный тип, указывая лишь протокол:
func makeCollection() -> some Collection { return [1, 2, 3]}print(makeCollection().count) // 3some Collection — компилятор знает конкретный тип, он фиксирован. any Collection — тип стирается, есть накладные расходы. Подробнее — в отдельной лекции.
15. Сравнение с Python
В Python обобщения реализованы через typing для статических анализаторов:
from typing import TypeVar, SequenceT = TypeVar("T")
def find_index(value: T, items: Sequence[T]) -> int | None: for i, item in enumerate(items): if item == value: return i return NoneКлючевые отличия:
| Аспект | Swift | Python |
|---|---|---|
| Проверка типов | Компилятор (compile-time) | Анализаторы (опционально) |
| Runtime-проверка | Да | Нет, аннотации игнорируются |
| Ограничения типов | <T: Protocol>, where | TypeVar(bound=...) |
| Обобщённые протоколы | associatedtype | Generic[T] |
| Обязательность | Типы обязательны | Аннотации опциональны |
В Swift обобщения — фундаментальная часть языка. В Python — инструмент для документации.
16. Практический пример: обобщённый кэш
Объединим концепции — кэш с ограничением по размеру:
struct Cache<Key: Hashable, Value> { private var storage: [Key: Value] = [:] private var order: [Key] = [] private let maxSize: Int
init(maxSize: Int) { self.maxSize = maxSize }
mutating func set(_ key: Key, _ value: Value) { if storage[key] != nil { order.removeAll { $0 == key } } else if storage.count >= maxSize { storage.removeValue(forKey: order.removeFirst()) } storage[key] = value order.append(key) }
func get(_ key: Key) -> Value? { storage[key] }}
var cache = Cache<Int, String>(maxSize: 2)cache.set(1, "один"); cache.set(2, "два")cache.set(3, "три") // "один" вытесняетсяprint(cache.get(1)) // nilprint(cache.get(3)) // Optional("три")17. Упражнения
Упражнение 1. Напишите обобщённую функцию filterItems<T>, принимающую массив [T] и замыкание (T) -> Bool, возвращающую массив элементов, удовлетворяющих предикату.
Упражнение 2. Создайте структуру Pair<A, B> с двумя значениями. Добавьте метод swapped() -> Pair<B, A>.
Упражнение 3. Реализуйте протокол Summable с associatedtype Element: Numeric и методом sum() -> Element. Реализуйте для структуры с массивом чисел.
Упражнение 4. Напишите removeDuplicates<T: Hashable>(from: [T]) -> [T] — массив без дубликатов с сохранением порядка.
Упражнение 5. Расширьте Stack<Element>: добавьте filter, а через where Element: Comparable — метод min() -> Element?.
18. Вопросы для самопроверки
- Какую проблему решают обобщения в Swift?
- Что такое параметр типа и как он указывается?
- Может ли обобщённая функция иметь несколько параметров типа?
- Чем
Stack<Element>отличается от структуры, работающей сAny? - Как ограничить параметр типа протоколом
Equatable? - В чём разница между
<T: Equatable>иwhere T: Equatable? - Что такое
associatedtypeи когда он используется? - Почему для ключей
DictionaryтребуетсяKey: Hashable? - Чем
some Protocolотличается отany Protocol? - Как обобщения Swift отличаются от
typing.Genericв Python?
19. Итоги
В этой лекции мы изучили обобщения (Generics) — механизм написания типобезопасного переиспользуемого кода. Рассмотрели обобщённые функции и типы, параметры типа, ограничения (<T: Protocol>, where), ассоциированные типы в протоколах, обобщённые подиндексы, стандартные обобщённые типы Swift и opaque types. Сравнили подход Swift с typing.Generic в Python.
Обобщения — фундаментальная концепция Swift, без которой невозможно писать качественный, масштабируемый код.