🍎 Swift

Funções Lambda e Estilo Funcional em Swift: Um Guia Detalhado

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты — шеф-повар в ресторане. У тебя есть повара, которые умеют готовить разные блюда. Но иногда к тебе приходит гость и говорит: «Я хочу, чтобы вы приготовили мне что-то новое, чего нет в меню, прямо сейчас, на моих глазах». Ты не можешь нанять нового повара ради одного заказа, но ты можешь сказать своему помощнику: «Возьми этот кусок мяса и просто обжарь его с солью — никаких сложных рецептов, сделай это прямо здесь».

В программировании лямбда-функция — это именно такой «повар на один заказ». Это функция, у которой нет имени. Она создаётся на лету, прямо в том месте, где она нужна, и чаще всего используется один раз. В Swift такие функции называются замыканиями (closures), и это одна из самых мощных фич языка.

Зачем это нужно? Представь, что у тебя есть массив чисел, и ты хочешь отсортировать его по убыванию. Ты можешь написать отдельную функцию для сравнения, а можешь написать лямбду прямо внутри вызова сортировки — это короче, понятнее и не засоряет код лишними именами.

Лямбда-функция — это функция без имени, которую можно передать как аргумент другой функции или сохранить в переменную. В Swift она называется замыканием (closure).

Функциональный стиль — это подход, при котором мы используем функции как строительные блоки: передаём их, возвращаем из других функций, комбинируем. Лямбды — это кирпичики этого стиля.

Теория: как это работает изнутри

Чтобы понять, как работают лямбды, нужно заглянуть «под капот» Swift. Когда ты пишешь обычную функцию:

func add(_ a: Int, _ b: Int) -> Int {
    return a + b
}

Компилятор создаёт для неё специальную область в памяти, даёт ей имя и делает так, чтобы её можно было вызвать из любого места программы. Всё просто.

Лямбда работает иначе. Она — анонимная. У неё нет имени, но есть тело и захваченные переменные. Самое интересное — это захват (capture). Лямбда может «запомнить» переменные из окружающего контекста и использовать их внутри себя, даже если этот контекст уже закончил работу.

Пример:

func makeMultiplier(factor: Int) -> () -> Int {
    var total = 0
    let closure = {
        total += factor
        return total
    }
    return closure
}

let multiplyBy5 = makeMultiplier(factor: 5)
print(multiplyBy5()) // 5
print(multiplyBy5()) // 10

Здесь лямбда «захватила» переменную total и параметр factor. Даже после того, как функция makeMultiplier завершилась, лямбда продолжает жить и помнить эти значения. Это называется замыканием — потому что она «замыкает» в себе внешние переменные.

Внутри Swift хранит лямбду как структуру, которая содержит:

  • Указатель на код (тело функции)
  • Копии или ссылки на захваченные переменные

Это делает лямбды немного «тяжелее» обычных функций, но даёт невероятную гибкость.

Важно: по умолчанию Swift захватывает переменные по значению (копирует их). Если нужно менять переменную внутри лямбды, используй ключевое слово inout или захватывай её как ссылку с помощью списка захвата [weak self].

Синтаксис в Swift: все формы и варианты

У лямбд в Swift есть несколько форм записи. Чем больше ты знаешь, тем гибче твой код.

Полная форма

let fullClosure: (Int, Int) -> Int = { (a: Int, b: Int) -> Int in
    return a + b
}

Здесь всё явно: тип параметров, тип возвращаемого значения, ключевое слово in отделяет заголовок от тела.

Сокращённая форма (вывод типов)

let shortClosure: (Int, Int) -> Int = { a, b in
    return a + b
}

Типы выводятся из контекста — мы уже указали тип переменной, поэтому внутри можно опустить объявления.

Неявный return

let implicitReturn: (Int, Int) -> Int = { a, b in
    a + b
}

Если тело лямбды состоит из одного выражения, return можно опустить.

Сокращённые имена параметров

let shorthand: (Int, Int) -> Int = { $0 + $1 }

$0 — первый параметр, $1 — второй и так далее. Это самая короткая форма.

Замыкание как последний аргумент (trailing closure)

let numbers = [3, 1, 4, 1, 5]
let sorted = numbers.sorted { $0 > $1 }
print(sorted) // [5, 4, 3, 1, 1]

Если лямбда — последний аргумент функции, её можно вынести за скобки. Это делает код чище.

Замыкание, возвращающее Void

let sayHello: () -> Void = {
    print("Привет, мир!")
}
sayHello()

Если функция ничего не возвращает, тип возврата — Void (или пустые скобки ()).

Замыкание с захватом (capture list)

var counter = 0
let increment = { [counter] in
    // Здесь counter — это копия на момент создания замыкания
    print(counter)
}
counter = 10
increment() // Выведет 0, а не 10

Список захвата в квадратных скобках позволяет контролировать, как захватываются переменные: по значению или по ссылке.

Примеры кода — от простого к сложному

Пример 1: Простейшая лямбда в переменной

// Создаём лямбду, которая принимает строку и возвращает её в верхнем регистре
let toUpper = { (text: String) -> String in
    return text.uppercased()
}

// Вызываем её как обычную функцию
let result = toUpper("hello")
print(result) // HELLO

Объяснение: Мы создали замыкание и сохранили его в константу toUpper. Тип этой константы — (String) -> String. Теперь мы можем вызывать её как функцию.

Пример 2: Лямбда как аргумент функции

// Функция, которая применяет переданное действие к числу
func applyOperation(to number: Int, operation: (Int) -> Int) -> Int {
    return operation(number)
}

// Передаём лямбду напрямую
let doubled = applyOperation(to: 5, operation: { $0 * 2 })
print(doubled) // 10

// Используем trailing closure
let squared = applyOperation(to: 4) { $0 * $0 }
print(squared) // 16

Объяснение: Функция applyOperation принимает число и замыкание. Мы передаём лямбду, которая умножает число на 2. Во втором вызове используем trailing closure — скобки опущены.

Пример 3: Сортировка массива

let students = ["Иван", "Анна", "Пётр", "Елена"]

// Сортируем по убыванию длины имени
let sortedStudents = students.sorted { $0.count > $1.count }
print(sortedStudents) // ["Елена", "Иван", "Пётр", "Анна"]

// Сортируем по алфавиту, но в обратном порядке
let reversedAlphabet = students.sorted { $0 > $1 }
print(reversedAlphabet) // ["Пётр", "Иван", "Елена", "Анна"]

Объяснение: Метод sorted принимает замыкание, которое сравнивает два элемента. Мы используем сокращённые имена параметров $0 и $1.

Пример 4: Фильтрация массива

let scores = [45, 78, 92, 33, 67, 88]

// Оставляем только оценки выше 70
let passingScores = scores.filter { $0 > 70 }
print(passingScores) // [78, 92, 88]

// Оставляем только чётные числа
let evenScores = scores.filter { $0.isMultiple(of: 2) }
print(evenScores) // [78, 92, 88]

Объяснение: filter возвращает новый массив, содержащий только те элементы, для которых замыкание вернуло true.

Пример 5: Преобразование массива (map)

let prices = [100, 200, 300, 400]

// Добавляем налог 20% к каждой цене
let withTax = prices.map { $0 * 120 / 100 }
print(withTax) // [120, 240, 360, 480]

// Преобразуем числа в строки с символом валюты
let priceStrings = prices.map { "($0) ₽" }
print(priceStrings) // ["100 ₽", "200 ₽", "300 ₽", "400 ₽"]

Объяснение: map применяет замыкание к каждому элементу массива и возвращает новый массив с преобразованными значениями.

Пример 6: Свёртка (reduce)

let numbers = [1, 2, 3, 4, 5]

// Суммируем все числа, начиная с 0
let sum = numbers.reduce(0) { $0 + $1 }
print(sum) // 15

// Создаём строку из чисел через запятую
let csv = numbers.reduce("") { $0.isEmpty ? "($1)" : "($0), ($1)" }
print(csv) // "1, 2, 3, 4, 5"

Объяснение: reduce последовательно применяет замыкание к каждому элементу, накапливая результат. Первый аргумент — начальное значение.

Пример 7: Замыкание, возвращающее замыкание

// Функция, которая возвращает замыкание — приветствие на разных языках
func makeGreeter(language: String) -> (String) -> String {
    switch language {
    case "ru":
        return { name in "Привет, (name)!" }
    case "en":
        return { name in "Hello, (name)!" }
    default:
        return { name in "Hi, (name)!" }
    }
}

let russianGreeter = makeGreeter(language: "ru")
print(russianGreeter("Иван")) // Привет, Иван!

let englishGreeter = makeGreeter(language: "en")
print(englishGreeter("John")) // Hello, John!

Объяснение: Функция возвращает разные замыкания в зависимости от переданного языка. Каждое замыкание «запоминает» язык, для которого оно создано.

Пример 8: Асинхронное выполнение с замыканием

// Функция, имитирующая загрузку данных с задержкой
func fetchData(completion: (String) -> Void) {
    DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) {
        let data = "Загруженные данные"
        completion(data)
    }
}

print("Начинаем загрузку...")
fetchData { result in
    print("Получено: (result)")
}
print("Загрузка запущена, ждём...")

// Вывод:
// Начинаем загрузку...
// Загрузка запущена, ждём...
// (через 2 секунды)
// Получено: Загруженные данные

Объяснение: Замыкание completion вызывается после завершения асинхронной операции. Это основа работы с сетевыми запросами и таймерами в Swift.

Сравнение с Python: чем похоже и чем отличается

Характеристика Swift (замыкания) Python (lambda)
Синтаксис { $0 + $1 } lambda x, y: x + y
Многострочность Да, можно писать несколько строк Нет, только одно выражение
Захват переменных Да, с контролем через capture list Да, но может быть неожиданным
Типизация Строгая, статическая Динамическая
Trailing closure Да, можно выносить за скобки Нет аналога
Использование Повсеместно: сортировка, асинхронность, UI В основном с map, filter, sorted
Производительность Оптимизируется компилятором (inline) Медленнее из-за динамической природы

Главное отличие: В Swift замыкания — это полноценные функции, которые могут быть многострочными и иметь сложную логику. В Python лямбды ограничены одним выражением. Если нужно больше строк — приходится писать обычную функцию.

Сходство: И там, и там лямбды используются для передачи поведения в функции высшего порядка (map, filter, reduce).

Типичные ошибки и подводные камни

1. Захват переменных по ссылке вместо значения

var numbers = [1, 2, 3]
let closure = {
    print(numbers)
}
numbers.append(4)
closure() // Выведет [1, 2, 3, 4], а не [1, 2, 3]

Проблема: Замыкание захватило numbers по ссылке, поэтому изменения видны внутри.

Решение: Используй список захвата, если нужно скопировать значение на момент создания.

2. Сильные ссылочные циклы (retain cycles)

class ViewController {
    var handler: (() -> Void)?
    
    func setup() {
        handler = {
            // self захвачен сильно — цикл!
            self.view.backgroundColor = .red
        }
    }
}

Проблема: Объект self держит замыкание, а замыкание держит self — никто не освобождается.

Решение: Используй [weak self] или [unowned self].

handler = { [weak self] in
    self?.view.backgroundColor = .red
}

3. Путаница с синтаксисом

// Ошибка: забыли in
let badClosure = { a, b
    return a + b
}

// Правильно:
let goodClosure = { a, b in
    return a + b
}

4. Использование замыкания до его определения

// Ошибка: замыкание ещё не определено
let result = myClosure(5) // myClosure не существует

let myClosure = { $0 * 2 }

Решение: Определяй замыкание до его использования.

5. Слишком сложные замыкания

// Плохо: замыкание на 20 строк
let complex = { (data: [Int]) -> [Int] in
    // ... много кода ...
    // ... трудно читать ...
    return result
}

// Лучше: вынести в отдельную функцию
func processData(_ data: [Int]) -> [Int] {
    // ... много кода ...
    return result
}
let simple = processData

Лучшие практики и советы профессионалов

  1. Используй сокращённые имена ($0, $1) только для простых замыканий. Если замыкание сложное, дай параметрам осмысленные имена.
  2. Всегда используй trailing closure, если замыкание — последний аргумент. Это делает код чище.
  3. Избегай сильных ссылочных циклов. В замыканиях, которые живут дольше своего создателя, используй [weak self].
  4. Не делай замыкания слишком длинными. Если тело замыкания больше 3-5 строк, вынеси его в отдельную функцию.
  5. Используй typealias для сложных типов замыканий.
typealias CompletionHandler = (Bool, String?) -> Void

func fetchData(completion: CompletionHandler) {
    // ...
}
  1. Помни о производительности. Замыкания, которые не захватывают переменные, оптимизируются лучше.
  2. Используй @escaping для замыканий, которые вызываются после возврата из функции.
func performAsyncTask(completion: @escaping () -> Void) {
    DispatchQueue.global().async {
        completion()
    }
}

Вопросы для самопроверки

  1. Вопрос: Что такое замыкание (closure) в Swift?

    Ответ: Это анонимная функция, которая может захватывать переменные из окружающего контекста.

  2. Вопрос: Какой синтаксис у самой короткой формы замыкания, складывающей два числа?

    Ответ: { $0 + $1 }

  3. Вопрос: Что такое trailing closure?

    Ответ: Это синтаксис, при котором замыкание пишется после скобок вызова функции, если оно является последним аргументом.

  4. Вопрос: Как избежать сильного ссылочного цикла с замыканием?

    Ответ: Использовать список захвата с [weak self] или [unowned self].

  5. Вопрос: Чем отличается map от filter?

    Ответ: map преобразует каждый элемент и возвращает новый массив той же длины. filter возвращает только элементы, удовлетворяющие условию.

  6. Вопрос: Можно ли в Swift создать многострочную лямбду?

    Ответ: Да, в отличие от Python, замыкания в Swift могут содержать несколько строк кода.

  7. Вопрос: Что делает ключевое слово in в замыкании?

    Ответ: Оно отделяет заголовок (параметры и возвращаемый тип) от тела замыкания.

  8. Вопрос: Как захватить переменную по значению, а не по ссылке?

    Ответ: Использовать список захвата: { [variable] in ... }

  9. Вопрос: Что такое @escaping и когда его использовать?

    Ответ: Это атрибут, указывающий, что замыкание может быть вызвано после возврата из функции. Используется для асинхронных операций.

  10. Вопрос: Какой метод массива используется для накопления значения (например, суммы)?

    Ответ: reduce

Итог и что изучить дальше

Лямбда-функции (замыкания) — это не просто синтаксический сахар, а фундаментальная концепция современного программирования на Swift. Они позволяют писать более чистый, модульный и выразительный код. Ты научился:

  • Создавать замыкания в разных формах
  • Использовать их с функциями высшего порядка (map, filter, reduce)
  • Понимать захват переменных и управлять памятью
  • Избегать типичных ошибок

Что изучить дальше:

  1. Функциональное программирование в Swift: Immutability, чистые функции, композиция функций.
  2. Combine framework: Современная работа с асинхронными потоками данных, где замыкания используются повсеместно.
  3. Оператор @autoclosure: Позволяет создавать замыкания, которые выглядят как обычные выражения.
  4. Замыкания и ARC: Глубокое понимание управления памятью с замыканиями.
  5. Реактивное программирование: RxSwift или Combine — следующий шаг после освоения замыканий.
Помни: замыкания — это суперсила Swift. Используй их с умом, и твой код станет элегантным и эффективным. Практикуйся каждый день, и скоро ты будешь мыслить «замыканиями»!