JavaRush /Курсы /Swift SELF /Захват переменных в Swift: что именно захватывается

Захват переменных в Swift: что именно захватывается

Swift SELF
16 уровень , 2 лекция
Открыта

1. Модель «функция с рюкзаком»

Когда вы впервые видите замыкания, они кажутся просто ещё одним способом написать маленькую функцию. Но довольно быстро вы замечаете странность: замыкание может использовать переменные “снаружи”, как будто у него есть маленький портал в прошлое. Это удобно, но иногда приводит к неожиданным результатам: “почему условие изменилось?” или “почему счётчик не сбросился?”. Чтобы не ловить такие баги, важно понять захват интуитивно.

Под захватом (capture) в Swift обычно понимают ситуацию, когда замыкание использует значения из внешней области видимости: переменные и константы, объявленные вокруг него. То есть замыкание не живёт в вакууме — оно “видит” окружение.

Интуитивная модель: замыкание как «функция с рюкзаком»

Представьте, что замыкание — это не только код, но и “рюкзак”, в который оно кладёт всё, что ему нужно из внешнего мира. Когда мы передаём замыкание куда-то или сохраняем в переменную, мы как бы переносим код + рюкзак вместе.

Можно изобразить это так:

[Внешняя область]
  var threshold = 10
  let f = { x in x > threshold }

          │
          ▼

f = (код: "x > threshold")
    + (окружение: "threshold")

И вот тут ключевой вопрос лекции: что именно лежит в “окружении”? Это копия числа 10? Это ссылка на переменную threshold? Это “что-то третье”?

Что именно захватывается: переменная, а не «снимок»

Обычно новичок думает так: “Ну раз threshold был 10, когда я написал замыкание, значит замыкание навсегда запомнит 10”. А Swift такой: “Ха. Хо-хо. Сейчас посмотрим”.

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

import Foundation

var threshold = 10

let isBig: (Int) -> Bool = { x in
    x > threshold
}

threshold = 20

print(isBig(15)) // false
print(isBig(25)) // true

Почему isBig(15) стало false? Потому что к моменту вызова threshold уже равен 20, и замыкание использует текущее значение.

Интуитивный вывод: замыкание чаще всего “помнит не число 10”, а саму переменную threshold (условно — “коробку”, внутри которой лежит значение). Мы поменяли содержимое коробки — замыкание увидело новое содержимое.

3. Захват let и var: стабильность и состояние

Захват let: проще всего, потому что значение не меняется

С let проще психологически: если значение не меняется, то и сюрпризов меньше. Вы один раз задали константу — и она всегда одинаковая, поэтому даже если замыкание “помнит переменную”, переменная неизменяемая, и результат стабилен.

import Foundation

let prefix = "BOOK-"

let makeCode: (Int) -> String = { number in
    "\(prefix)\(number)"
}

print(makeCode(7))  // BOOK-7
print(makeCode(42)) // BOOK-42

В реальном Swift есть важная деталь: захват неизменяемых значений, объявленных через let, можно рассматривать как “безопасный” и фактически “снимок значения”, потому что менять всё равно нечего. В материалах по развитию Swift это формулируется как идея “неизменяемые значения захватываются по значению (by-value)”.

Захват var: общая «коробка» для двух областей видимости

Теперь вернёмся к var, потому что именно он создаёт большинство “что вообще происходит” моментов. Замыкание может читать var, и это уже интересно. Но ещё веселее — оно может менять этот var, и переменная снаружи тоже увидит изменения.

import Foundation

var query = "swift"

let matchesQuery: (String) -> Bool = { title in
    title.lowercased().contains(query)
}

print(matchesQuery("Swift Basics")) // true

query = "ios"
print(matchesQuery("Swift Basics")) // false
print(matchesQuery("iOS Guide"))    // true

Это тот же эффект “коробки”: замыкание и внешний код смотрят на одну и ту же переменную. Поэтому изменение query снаружи меняет поведение замыкания.

Здесь появляется первое правило читаемости: если замыкание хранится где-то и будет вызываться позже, захват var превращает его поведение в “зависящее от текущего состояния программы”. Это может быть круто (например, фильтр с меняющимся режимом), но может быть и ловушкой (“почему фильтр внезапно стал другим?”).

Замыкание как состояние: генератор ID и память между вызовами

Окей, мы поняли, что замыкание может читать внешнюю переменную. Теперь следующий шаг: а что если замыкание само будет хранить внутреннее состояние между вызовами? Звучит как “маленький объект без класса” (классы будут позже, не торопимся), но по ощущениям — примерно так.

Сделаем генератор ID: каждый вызов возвращает следующее число.

import Foundation

func makeIDGenerator() -> () -> Int {
    var id = 0									// локальная переменная

    let nextID: () -> Int = {		// фактически объявили функцию внутри фукнции
        id += 1
        return id
    }

    return nextID								// вернули дочернюю функцию наружу, а она захватывает локальную переменную 
}

let gen = makeIDGenerator()
print(gen()) // 1
print(gen()) // 2
print(gen()) // 3

Что произошло? Переменная id была объявлена внутри makeIDGenerator(). Казалось бы, функция завершилась — переменная должна исчезнуть. Но мы вернули замыкание nextID, которое захватило id, и теперь оно живёт вместе с замыканием.

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

4. Захват и параметры: как не сделать код загадочным

Когда вы видите, что замыкание может “таскать” с собой внешние переменные, возникает соблазн: “А давайте вообще всё будем брать из внешней области — меньше параметров, красота”. Обычно это заканчивается тем, что код становится похож на детектив, в котором слишком много подозреваемых: непонятно, откуда пришло значение и кто его поменял.

Практичнее держать такую модель:

Что это по смыслу Лучше сделать… Почему
“Это входные данные для вычисления” параметром замыкания/функции так проще тестировать и читать
“Это настройка, одинаковая для многих вызовов” захватить let поведение стабильное, меньше шума
“Это режим, который может меняться” захватить var, но явно поведение зависит от состояния — это должно быть заметно
“Это накопление состояния (счётчик, статистика)” захватить var, но не прятать иначе следующий читатель кода будет плакать

Если сказать совсем по-человечески: параметры — это то, что вы хотите видеть в скобках, а захват — то, что вы готовы “держать в фоне”, потому что оно либо константное, либо очень осмысленно является состоянием.

5. Как «заморозить» значение при захвате

Иногда нужно ровно обратное тому, что мы видели в начале: вы хотите, чтобы замыкание использовало значение на момент создания, даже если внешняя переменная потом поменяется. Например: “создай фильтр по порогу 10” — и пусть он всегда будет про 10, даже если мы потом поменяли глобальную настройку.

Способ 1: локальная константа

Первый — самый понятный новичку: переложить текущее значение в let, а замыканию дать захватить эту константу.

import Foundation

var threshold = 10
let fixedThreshold = threshold

let isBigFixed: (Int) -> Bool = { x in
    x > fixedThreshold
}

threshold = 20

print(isBigFixed(15)) // true

Способ 2: capture list

Второй способ — capture list. Это синтаксис вроде { [someVar] in ... }, который явно говорит: “захвати вот это значение вот так”. В Swift capture list часто используют для контроля захвата, и в частности, чтобы захватить изменяемую переменную как неизменяемую “копию на момент создания”.

import Foundation

var suffix = "!"
let addSuffix: (String) -> String = { [suffix] text in	//запоминает suffix на момент захвата, а не вызова	
    text + suffix
}

suffix = "??"

print(addSuffix("Hi")) // Hi!

Мы изменили внешнюю suffix, но addSuffix продолжает использовать старое значение "!", потому что мы явно “заморозили” его через capture list.

Важно: мы сейчас не превращаем capture list в обязательную религию. На старте курса достаточно понимать идею: по умолчанию замыкание часто “смотрит” на переменную, а при необходимости можно заставить его “сфотографировать” значение.

6. Практика: фильтры для списка книг

Теперь давайте соберём всё в один смысловой пример, который выглядит как часть реального приложения. Пусть у нас есть мини‑каталог книг (пока без struct, просто tuples и массив — то, что вы уже умеете).

import Foundation

let books: [(title: String, year: Int)] = [
    (title: "Swift Basics", year: 2021),
    (title: "iOS Guide", year: 2020),
    (title: "Algorithms", year: 2018)
]

let minYear = 2020

let recent = books.filter { book in
    book.year >= minYear
}

print(recent) // [(title: "Swift Basics", year: 2021), (title: "iOS Guide", year: 2020)]

Здесь minYearlet, он захватывается спокойно и предсказуемо.

Теперь сделаем интереснее: хотим фабрику фильтров “содержит подстроку”. Это хороший кейс на захват, потому что строка‑запрос — это настройка фильтра.

import Foundation

func makeTitleContainsFilter(_ rawQuery: String) -> ((title: String, year: Int)) -> Bool {
    let query = rawQuery.lowercased()

    return { book in
        book.title.lowercased().contains(query)
    }
}

let containsSwift = makeTitleContainsFilter("swift")
print(containsSwift((title: "Swift Basics", year: 2021))) // true
print(containsSwift((title: "Algorithms", year: 2018)))   // false

Обратите внимание на маленький трюк для читаемости: мы специально сделали query константой (let query = ...). Это как раз “заморозка” значения, но без capture list: фильтр стабилен, и нам не нужно думать, что там будет “позже”.

Теперь совместим с filter:

import Foundation

let books: [(title: String, year: Int)] = [
    (title: "Swift Basics", year: 2021),
    (title: "iOS Guide", year: 2020),
    (title: "Algorithms", year: 2018)
]

let predicate = makeTitleContainsFilter("iOS")
let result = books.filter(predicate)

print(result) // [(title: "iOS Guide", year: 2020)]

Вот здесь вы начинаете чувствовать, зачем захват вообще нужен: мы сделали маленький “настраиваемый фильтр”, который можно передавать куда угодно — в filter, в свою функцию, хранить в переменной, комбинировать.

7. Подводные камни: inout и неожиданные копии

Есть особый класс ситуаций, когда захват приводит к эффекту “я думал, что меняю переменную, а она не меняется”. Чаще всего это связано с тем, что вы пытаетесь захватывать то, что живёт как временный “доступ на изменение”, например inout-параметр. На уровне интуиции это похоже на попытку унести из магазина тележку, пока кассир ещё пробивает товары: формально тележка есть, но правила доступа не дают сделать это так, как вам хочется.

В истории Swift это было источником очень неожиданных эффектов, когда замыкание “убегало” наружу (возвращалось из функции) и продолжало менять не оригинальную переменную, а её “теневую копию”.

Здесь нам не нужно углубляться в детали (мы не вводим сейчас правила escaping/non-escaping), но практическое ощущение такое: если вы видите inout и замыкание в одной комнате — действуйте осторожно и старайтесь не “уносить” замыкание наружу, если оно опирается на inout.

8. Типичные ошибки при захвате переменных

Ошибка №1: ожидать, что замыкание “запоминает значение навсегда”, хотя захвачена изменяемая переменная.
Это классическая ситуация с var threshold: вы создали замыкание, потом поменяли threshold, а потом удивились, что поведение изменилось. Лечится очень просто: либо делайте захватываемое значение константой (let fixed = threshold), либо используйте capture list, если вы уже готовы к этому синтаксису.

Ошибка №2: прятать важные входные данные в захват, вместо того чтобы сделать их параметрами.
Если замыкание зависит от “чего-то снаружи”, читателю приходится искать, где это “снаружи” объявлено и кто это меняет. Если значение по смыслу является входом “на каждый вызов”, лучше передать его параметром и не превращать код в квест.

Ошибка №3: случайно сделать замыкание stateful, а потом удивляться, что результат зависит от прошлых вызовов.
Счётчики, накопление суммы, генераторы ID — всё это нормально, но должно быть очевидно из кода. Если внутри замыкания есть counter += 1, это уже не “чистая функция”, а функция с памятью. Обычно лучше давать таким переменным честные имена (counter, nextID) и держать тело замыкания коротким.

Ошибка №4: захватывать слишком много всего подряд.
Замыкание легко превращается в “тащу весь мир”, когда вы используете внутри него кучу внешних переменных. От этого страдает и читаемость, и предсказуемость. Хорошая привычка — перед созданием замыкания сформировать небольшое “контекстное” let-значение (например, нормализованный query), и захватывать ровно его.

Ошибка №5: пытаться сочетать захват и inout так, как будто это обычная переменная.
Даже если какие-то примеры “работают”, в более сложных случаях можно получить очень неочевидное поведение или запреты компилятора. Если вам нужно изменить значение, переданное через inout, лучше сделать это напрямую в функции, а замыкание использовать как правило вычисления (без попытки удерживать inout внутри замыкания).

1
Задача
Swift SELF, 16 уровень, 2 лекция
Недоступна
Порог тревоги
Порог тревоги
1
Задача
Swift SELF, 16 уровень, 2 лекция
Недоступна
Суффикс наклейка
Суффикс наклейка
1
Задача
Swift SELF, 16 уровень, 2 лекция
Недоступна
Логгер с тегом
Логгер с тегом
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ