Перейти к содержимому

Лекция 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, a
x, 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 = 20
swapValues(&x, &y)
print(x, y) // 20 10
var s1 = "привет", s2 = "мир"
swapValues(&s1, &s2)
print(s1, s2) // мир привет

Tпараметр типа, заменяемый конкретным типом при вызове. Компилятор выводит T автоматически. Значения разных типов — ошибка:

var a = 5
var 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, Generic
T = 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 2

11. Ограничения ассоциированных типов

Ассоциированные типы можно ограничивать протоколами:

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) // 3

some Collection — компилятор знает конкретный тип, он фиксирован. any Collection — тип стирается, есть накладные расходы. Подробнее — в отдельной лекции.


15. Сравнение с Python

В Python обобщения реализованы через typing для статических анализаторов:

from typing import TypeVar, Sequence
T = 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

Ключевые отличия:

АспектSwiftPython
Проверка типовКомпилятор (compile-time)Анализаторы (опционально)
Runtime-проверкаДаНет, аннотации игнорируются
Ограничения типов<T: Protocol>, whereTypeVar(bound=...)
Обобщённые протоколыassociatedtypeGeneric[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)) // nil
print(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. Вопросы для самопроверки

  1. Какую проблему решают обобщения в Swift?
  2. Что такое параметр типа и как он указывается?
  3. Может ли обобщённая функция иметь несколько параметров типа?
  4. Чем Stack<Element> отличается от структуры, работающей с Any?
  5. Как ограничить параметр типа протоколом Equatable?
  6. В чём разница между <T: Equatable> и where T: Equatable?
  7. Что такое associatedtype и когда он используется?
  8. Почему для ключей Dictionary требуется Key: Hashable?
  9. Чем some Protocol отличается от any Protocol?
  10. Как обобщения Swift отличаются от typing.Generic в Python?

19. Итоги

В этой лекции мы изучили обобщения (Generics) — механизм написания типобезопасного переиспользуемого кода. Рассмотрели обобщённые функции и типы, параметры типа, ограничения (<T: Protocol>, where), ассоциированные типы в протоколах, обобщённые подиндексы, стандартные обобщённые типы Swift и opaque types. Сравнили подход Swift с typing.Generic в Python.

Обобщения — фундаментальная концепция Swift, без которой невозможно писать качественный, масштабируемый код.