Что такое обобщения (Generics) в Swift и для чего они нужны?

Ответ

Generics (обобщения) — это механизм Swift для написания гибкого, переиспользуемого кода, который может работать с любым типом, сохраняя при этом безопасность типов (type safety).

Основная цель: Устранить дублирование кода за счет абстрагирования от конкретных типов данных.

Пример 1: Обобщенная функция

// Функция меняет местами значения ЛЮБОГО одного типа.
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

var int1 = 10, int2 = 20
swapTwoValues(&int1, &int2) // T выводится как Int

var string1 = "hello", string2 = "world"
swapTwoValues(&string1, &string2) // T выводится как String

Пример 2: Обобщенный тип (Generic Type)

// Стек, который может хранить элементы ЛЮБОГО одного типа.
struct Stack<Element> {
    private var items: [Element] = []

    mutating func push(_ item: Element) {
        items.append(item)
    }

    mutating func pop() -> Element? {
        return items.popLast()
    }
}

var intStack = Stack<Int>()
intStack.push(5)

var stringStack = Stack<String>()
stringStack.push("Swift")

Ключевые преимущества:

  1. Type Safety: Компилятор следит за корректностью типов, ошибка intStack.push("text") будет поймана на этапе компиляции.
  2. Устранение дублирования: Не нужно писать отдельные функции swapInts, swapStrings и т.д.
  3. Оптимизация: Код с дженериками компилируется в нативный код для конкретных типов, минимизируя накладные расходы времени выполнения.

Где используются повсеместно:

  • Стандартные коллекции: Array<Element>, Dictionary<Key, Value>.
  • Опциональные типы: Optional<Wrapped> (это Wrapped?).
  • Асинхронные API: Future<Output, Failure> в Combine, Task<Success, Failure> в Concurrency.

Ответ 18+ 🔞

Давай разберём эту тему, а то я вижу, у тебя глаза уже стекленеют от слова «дженерики». Не бойся, тут всё проще, чем кажется, если не забивать себе голову академическим бредом.

Представь себе, блядь, обычную коробку. Коробка для обуви. Ты можешь положить в неё только ботинки, и всё, нахуй. А теперь представь коробку-трансформер, которая смотрит, что ты в неё суёшь, и подстраивается. Сунул носки — она стала коробкой для носков. Сунул гвозди — стала коробкой для гвоздей. Вот это и есть Generics, ёпта! Универсальная хуйня, которая работает с чем угодно, но при этом не превращается в помойку, где носки с гвоздями в одной куче.

Зачем это, спросишь? Да чтобы не писать одно и то же сто раз, как последний лох! Раньше, в каменном веке, приходилось для целых чисел писать одну функцию swapInts, для строк — другую swapStrings. Овердохуища кода, а суть-то одна — поменять два значения местами! Ну ёбаный в рот, да как так-то?

Пример первый: функция-универсал. Смотри, как красота:

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

Видишь эту букву T? Это не хуй с горы, а placeholder, типа «сюда подставят любой тип, который понадобится». Компилятор, хитрая жопа, сам догадается. Ты вызываешь функцию с целыми числами — T становится Int. Вызываешь со строками — T становится String. И никакого шаманства с приведением типов, всё чётко и безопасно. Попробуй передать туда число и строку — получишь по ебалу от компилятора на этапе сборки, а не в рантайме, когда всё уже ебнулось.

Пример второй: своя структура. Допустим, тебе нужен стек. Ну, структура данных «последний зашёл — первый вышел», как стопка тарелок. Раньше пришлось бы городить IntStack, StringStack, DogStack... Пиздец, да и только.

struct Stack<Element> {
    private var items: [Element] = []

    mutating func push(_ item: Element) {
        items.append(item)
    }

    mutating func pop() -> Element? {
        return items.popLast()
    }
}

Вот видишь? Вместо конкретного типа мы говорим Element. «Элемент, сука, любой!». Создаёшь стек для целых чисел — Stack<Int>(), и всё внутри автоматически становится про Int. Создаёшь для строк — Stack<String>(), и всё работает со строками. Красота, блядь! Никакого дублирования кода, одна структура на все случаи жизни.

А в чём, собственно, профит, спросит дотошный читатель?

  1. Безопасность, ёпта! Компилятор следит за тобой, как ястреб. Не даст тебе в intStack запихнуть строку. Ошибка всплывёт сразу, когда ты пишешь код, а не когда твое приложение уже на прод ебнулось в три часа ночи.
  2. Переиспользование, блядь! Написал один раз — используй везде. Не надо изобретать велосипед для каждого типа данных.
  3. Производительность. Всё это магия компилируется в конкретный код для конкретных типов. Никаких проверок типов в рантайме, всё быстро, как угорелое.

Да они везде, эти дженерики! Ты ими уже пользуешься, сам того не зная.

  • Массив [Int] — это сахар для Array<Int>.
  • Опциональная строка String? — это Optional<String>.
  • Всякие модные асинхронные штуки вроде Task или Future — тоже сплошные дженерики.

Так что не пугайся. Это не какая-то высшая математика, а просто удобный инструмент, чтобы не делать лишнюю работу и не стрелять себе в ногу. Освоишь — будешь как рыба в воде, а код станет чище и надёжнее.