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

Практика 12. Практика к лекции 11

Практика 12. Обобщения (Generics)

Цель: освоить обобщённое программирование в Swift — обобщённые функции, обобщённые типы, ограничения типов, ассоциированные типы, opaque-типы (some Protocol) и обобщённые расширения — через серию упражнений от базовых к проектным. Все задания выполняются на Linux в командной строке.

Рекомендации по выполнению:

  • Создавайте файлы с расширением .swift и запускайте командой swift file.swift.
  • Каждое задание можно оформлять в отдельном файле или объединять логически близкие в один.
  • Добавляйте print-вызовы для демонстрации работы каждого задания.
  • Сравнивайте обобщения Swift с typing.Generic и TypeVar в Python.

A. Разминка — обобщённые функции

  1. Напишите обобщённую функцию filterItems<T>(_ items: [T], predicate: (T) -> Bool) -> [T], принимающую массив и замыкание-предикат, возвращающую массив элементов, удовлетворяющих условию.

Продемонстрируйте работу с разными типами:

let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let evens = filterItems(numbers) { $0 % 2 == 0 }
print(evens) // [2, 4, 6, 8, 10]
let names = ["Алиса", "Борис", "Ал", "Вера", "Алексей"]
let longNames = filterItems(names) { $0.count > 3 }
print(longNames) // ["Алиса", "Борис", "Вера", "Алексей"]
  1. Напишите обобщённую функцию swapValues<T>(_ a: inout T, _ b: inout T), меняющую значения двух переменных местами. Напишите также findIndex<T: Equatable>(of value: T, in array: [T]) -> Int?, возвращающую индекс первого вхождения элемента.

Пример ожидаемого поведения:

var x = 10, y = 20
swapValues(&x, &y)
print(x, y) // 20 10
let idx = findIndex(of: "Swift", in: ["Python", "Swift", "Go"])
print(idx!) // 1

B. Обобщённые типы

  1. Создайте структуру Pair<A, B> с двумя свойствами first: A и second: B. Добавьте:
    • Метод swapped() -> Pair<B, A> — возвращает пару с переставленными элементами.
    • Метод map<C, D>(_ transformFirst: (A) -> C, _ transformSecond: (B) -> D) -> Pair<C, D> — трансформирует оба элемента.

Пример ожидаемого поведения:

let pair = Pair(first: "Swift", second: 5)
print(pair.first) // Swift
print(pair.second) // 5
let swapped = pair.swapped()
print(swapped.first) // 5
print(swapped.second) // Swift
let mapped = pair.map({ $0.uppercased() }, { $0 * 10 })
print(mapped.first) // SWIFT
print(mapped.second) // 50
  1. Создайте обобщённую структуру Stack<Element> с операциями push, pop, peek, isEmpty, count. Реализуйте внутреннее хранение на массиве.

Пример ожидаемого поведения:

var intStack = Stack<Int>()
intStack.push(10)
intStack.push(20)
intStack.push(30)
print(intStack.pop()!) // 30
print(intStack.peek()!) // 20
print(intStack.count) // 2
var strStack = Stack<String>()
strStack.push("hello")
strStack.push("world")
print(strStack.pop()!) // world

C. Ограничения типов

  1. Напишите функцию removeDuplicates<T: Hashable>(from array: [T]) -> [T], возвращающую массив без дубликатов с сохранением исходного порядка элементов.

Пример ожидаемого поведения:

let nums = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
print(removeDuplicates(from: nums)) // [3, 1, 4, 5, 9, 2, 6]
let words = ["кот", "пёс", "кот", "рыба", "пёс"]
print(removeDuplicates(from: words)) // ["кот", "пёс", "рыба"]
  1. Напишите обобщённую функцию mostFrequent<T: Hashable>(in array: [T]) -> (element: T, count: Int)?, возвращающую элемент, встречающийся чаще всего, и его количество. Если массив пуст — вернуть nil.

Пример ожидаемого поведения:

let data = [1, 3, 2, 3, 1, 3, 2]
if let result = mostFrequent(in: data) {
print("\(result.element) встречается \(result.count) раз")
// 3 встречается 3 раз
}

D. Ассоциированные типы

  1. Реализуйте протокол Summable с:
    • associatedtype Element: Numeric
    • Свойство items: [Element] { get }
    • Метод sum() -> Element

Реализуйте соответствие для:

  • struct IntCollection: SummableElement = Int)
  • struct DoubleCollection: SummableElement = Double)

Пример ожидаемого поведения:

let ints = IntCollection(items: [1, 2, 3, 4, 5])
print(ints.sum()) // 15
let doubles = DoubleCollection(items: [1.5, 2.5, 3.0])
print(doubles.sum()) // 7.0
  1. Создайте протокол Container с:
    • associatedtype Item
    • mutating func append(_ item: Item)
    • var count: Int { get }
    • subscript(i: Int) -> Item { get }

Реализуйте struct Queue<T>: Container (FIFO-очередь) с операциями enqueue (через append), dequeue() -> T? и доступом по индексу.

Пример ожидаемого поведения:

var queue = Queue<String>()
queue.append("первый")
queue.append("второй")
queue.append("третий")
print(queue.count) // 3
print(queue[0]) // первый
print(queue.dequeue()!) // первый
print(queue.count) // 2

E. Обобщённые расширения

  1. Расширьте Stack<Element> из задания 4, добавив:
    • Метод filter(_ predicate: (Element) -> Bool) -> Stack<Element> — возвращает новый стек с элементами, удовлетворяющими предикату.
    • Метод map<U>(_ transform: (Element) -> U) -> Stack<U> — трансформирует элементы стека.
    • Через условное расширение where Element: Comparable — метод min() -> Element?, возвращающий минимальный элемент.
    • Через условное расширение where Element: Numeric — метод sum() -> Element.
    • Через условное расширение where Element: CustomStringConvertible — метод prettyPrint(), выводящий элементы стека в формате [bottom] el1 | el2 | el3 [top].

Пример ожидаемого поведения:

var stack = Stack<Int>()
stack.push(5)
stack.push(2)
stack.push(8)
stack.push(1)
let filtered = stack.filter { $0 > 3 }
print(filtered.pop()!) // 8
print(filtered.pop()!) // 5
print(stack.min()!) // 1
print(stack.sum()) // 16
stack.prettyPrint() // [bottom] 5 | 2 | 8 | 1 [top]
let mapped = stack.map { Double($0) * 1.5 }
print(mapped.pop()!) // 1.5

F. Мини-проект

  1. «Обобщённый кэш с политикой вытеснения» — создайте обобщённую структуру Cache<Key: Hashable, Value>:

Требования:

  • Инициализация с capacity: Int (максимальное количество элементов).
  • Метод set(_ key: Key, value: Value) — добавляет элемент; если кэш полон, удаляет самый старый элемент (FIFO).
  • Метод get(_ key: Key) -> Value? — возвращает значение или nil.
  • Метод remove(_ key: Key) — удаляет элемент.
  • Вычисляемое свойство count: Int.
  • Метод allKeys() -> [Key] — возвращает все ключи в порядке добавления.

Добавьте условное расширение where Value: CustomStringConvertible с методом dump(), выводящим содержимое кэша в формате key: value.

Пример ожидаемого поведения:

var cache = Cache<String, Int>(capacity: 3)
cache.set("a", value: 1)
cache.set("b", value: 2)
cache.set("c", value: 3)
print(cache.get("b")!) // 2
print(cache.count) // 3
cache.set("d", value: 4) // "a" вытеснен (FIFO)
print(cache.get("a")) // nil
print(cache.allKeys()) // ["b", "c", "d"]
cache.dump()
// b: 2
// c: 3
// d: 4

Дополнительно к мини-проекту: создайте протокол EvictionPolicy с associatedtype Key: Hashable и методами recordAccess(_ key: Key) и evict() -> Key?. Реализуйте FIFOPolicy и LRUPolicy. Параметризуйте Cache политикой вытеснения.


G. Критерии оценивания

  • Корректность и полнота реализации: 0–5 баллов
  • Грамотное использование обобщённых функций и типов: 0–4 балла
  • Правильное применение ограничений типов, ассоциированных типов и условных расширений: 0–4 балла
  • Читаемость кода и наличие демонстрационных примеров: 0–3 балла

Максимум: 16 баллов. Бонус до +2 за дополнительные задания.


H. Дополнительно (по желанию)

  • Реализуйте обобщённый BinarySearchTree<Element: Comparable> с операциями insert, contains, inOrderTraversal.
  • Создайте функцию с opaque return type: func makeCollection() -> some Collection и объясните, чем some Collection отличается от any Collection.
  • Сравните обобщения Swift с typing.Generic[T] в Python — напишите аналог Stack<Element> на Python с аннотациями типов и обсудите разницу в гарантиях компилятора.