Замыкания и функции
Замыкания и семантика захвата — escaping против non-escaping, списки захвата, `@autoclosure`, функции высшего порядка, `inout` и result builder'ы.
12 вопросов
JuniorТеорияОчень частоЧто такое замыкание в Swift — trailing-синтаксис, сокращённые аргументы и «захват окружения»?
Что такое замыкание в Swift — trailing-синтаксис, сокращённые аргументы и «захват окружения»?
Замыкание — самодостаточный блок кода, который передают как безымянную функцию. Trailing-синтаксис выносит последнее замыкание за скобки вызова, $0/$1 — сокращённые имена аргументов, а захват окружения означает, что оно хранит ссылки на переменные объемлющей области.
Типичные ошибки
- ✗Думают, что замыкание не имеет доступа к переменным окружающей области
- ✗Считают, что
$0/$1нужно объявлять явно перед использованием - ✗Путают trailing-синтаксис с обязательным ключевым словом языка
Уточняющие вопросы
- →Когда захват переменной продлевает время её жизни?
- →Как trailing-синтаксис меняет вызов с несколькими замыканиями?
MiddleТеорияОчень частоПочему сильный захват self в escaping-замыкании приводит к утечке и что именно меняет [weak self]?
Почему сильный захват self в escaping-замыкании приводит к утечке и что именно меняет [weak self]?
Если объект хранит escaping-замыкание с сильным захватом self, каждая сторона удерживает другую: цикл, который никто не отпускает, поэтому deinit не срабатывает. [weak self] захватывает self как weak-optional без прироста счётчика ссылок, цикл разрывается, а self становится nil.
Типичные ошибки
- ✗Думают, что
[weak self]держитselfживым, а не убирает сильное удержание - ✗Винят поток вместо взаимных сильных ссылок
- ✗Полагают, что ссылку держит лишь одна сторона цикла
Уточняющие вопросы
- →Когда выбрать
[unowned self]вместо[weak self]? - →Как защититься от
self, равногоnil, внутри тела замыкания?
JuniorТеорияЧастоФункции и методы — что означают static func и class func у типа и какой из них может переопределить подкласс?
Функции и методы — что означают static func и class func у типа и какой из них может переопределить подкласс?
Метод — функция, привязанная к типу и получающая неявный self; у свободной функции его нет. И static func, и class func относятся к типу, но переопределить в подклассе можно только class func — static func фактически final.
Типичные ошибки
- ✗Считают, что
static funcможно переопределить, какclass func - ✗Думают, что метод и свободная функция — одно и то же (без
self) - ✗Полагают, что
class funcиstatic funcвзаимозаменяемы
Уточняющие вопросы
- →Почему подкласс может переопределить
class func, но неstatic func? - →Когда предпочесть
static funcвместоclass funcу класса?
JuniorКодЧастоИзменение массива вызывающего на месте через параметр inout
Изменение массива вызывающего на месте через параметр inout
inout использует copy-in/copy-out — вызываемая сторона меняет локальную копию, которая при возврате пишется обратно в переменную вызывающего; вызов через &list. Захватить его в escaping-замыкании нельзя: время жизни параметра кончается при возврате, и отложенная запись попала бы в мёртвую переменную.
Типичные ошибки
- ✗Думают, что
inoutпередаёт по ссылке, как указатель C, а не copy-in/copy-out - ✗Забывают префикс
&на месте вызова - ✗Полагают, что параметр
inoutможно спрятать в escaping-замыкании на потом
Уточняющие вопросы
- →Почему copy-out происходит при возврате, а не при каждой записи внутри функции?
- →Чем параметр
inoutотличается от передачи экземпляраclass?
MiddleДебаггингЧастоХранимый completion handler держит свой view controller живым — разберите владение и устраните утечку
Хранимый completion handler держит свой view controller живым — разберите владение и устраните утечку
Бессмертный синглтон Downloader.shared хранит onDone с сильным захватом view controller. Так как start его не вызывает и не очищает, замыкание удерживает контроллер, а deinit не срабатывает. Исправьте через [weak self] и onDone = nil после вызова.
Типичные ошибки
- ✗Винят фоновый поток вместо сильного захвата, который держит синглтон
- ✗Думают, что контроллер удерживает синглтон, а не наоборот
- ✗Считают причиной сам
@escaping, а не так и не очищенное хранимое замыкание
Уточняющие вопросы
- →Почему очистка
onDoneпосле вызова важна даже при[weak self]? - →Когда
[unowned self]неверен для обработчика, способного пережить контроллер?
MiddleТеорияЧастоEscaping против non-escaping замыканий — что меняет @escaping и почему non-escaping по умолчанию?
Escaping против non-escaping замыканий — что меняет @escaping и почему non-escaping по умолчанию?
@escaping помечает замыкание, которое может пережить вызов — сохранено или запущено на другом потоке. Non-escaping стоит по умолчанию, чтобы компилятор считал, что замыкание отработает до возврата, включая оптимизации и без явного self. Escaping-замыкания требуют явного self.
Типичные ошибки
- ✗Думают, что
@escapingстоит по умолчанию, а non-escaping надо включать - ✗Считают, что
@escapingне влияет на правила захватаself - ✗Полагают, что non-escaping замыкание можно сохранить и вызвать после возврата
Уточняющие вопросы
- →Почему escaping-замыкание вынуждает писать
selfявно? - →Какие оптимизации даёт компилятору non-escaping замыкание?
MiddleКодЧастоПерепишите накапливающий цикл через reduce, затем реализуйте свой reduce
Перепишите накапливающий цикл через reduce, затем реализуйте свой reduce
Цикл превращается в xs.reduce(0, +). reduce сворачивает последовательность в одно значение, прогоняя аккумулятор через комбинирующее замыкание. Ваш myReduce стартует с initial, проходит по self, переприсваивает result = next(result, element) на каждый элемент и возвращает итоговый аккумулятор.
Типичные ошибки
- ✗Путают
reduce(свёртка в одно значение) сmap(поэлементное преобразование) - ✗Забывают, что
reduceтребует начального значения аккумулятора - ✗Думают, что
reduceменяет исходную последовательность
Уточняющие вопросы
- →Когда
reduce(into:)предпочтительнееreduce(_:_:)? - →Чем
reduceотличается отmapс последующим суммированием?
JuniorТеорияИногдаЗамыкания в Swift — значимые или ссылочные типы, и что из этого следует?
Замыкания в Swift — значимые или ссылочные типы, и что из этого следует?
Замыкания — ссылочные типы. Присвоив одно замыкание двум переменным, вы разделяете один экземпляр, а захваченные переменные удерживаются по ссылке, поэтому мутация через одну копию видна через другую. Именно этот общий захват и позволяет замыканиям образовывать циклы удержания.
Типичные ошибки
- ✗Считают, что замыкания копируются по значению, как struct
- ✗Думают, что захваченные переменные по умолчанию снимаются по значению
- ✗Полагают, что замыкания не участвуют в циклах удержания
Уточняющие вопросы
- →Как список захвата меняет захват по ссылке по умолчанию?
- →Почему ссылочная природа замыканий делает возможными циклы удержания?
MiddleКодИногдаПредскажите вывод, когда захваченную переменную меняют после захвата
Предскажите вывод, когда захваченную переменную меняют после захвата
byValue() печатает 1; byReference() печатает 99. Список захвата [x] копирует x по значению при создании замыкания, поэтому позднее изменение x для него невидимо. Без списка захвата замыкание захватывает переменную по ссылке и читает её текущее значение в момент вызова — 99.
Типичные ошибки
- ✗Думают, что список захвата — лишь стилевая подсказка без эффекта в рантайме
- ✗Считают, что голый захват снимает значение в момент создания
- ✗Полагают, что оба замыкания всегда печатают одно и то же значение
Уточняющие вопросы
- →Чем
[weak self]отличается от[x]в том, что именно захватывается? - →Почему захват по ссылке читает значение в момент вызова, а не создания?
MiddleТеорияИногдаЧто делают @discardableResult, @inlinable и @frozen и к чему обязывает каждый из них?
Что делают @discardableResult, @inlinable и @frozen и к чему обязывает каждый из них?
@discardableResult глушит предупреждение о проигнорированном результате — на ABI не влияет. @inlinable открывает тело между модулями, чтобы вызывающие его встроили, фиксируя это тело в ABI. @frozen обещает неизменность раскладки типа, позволяя клиентам оптимизировать, но замораживая эволюцию.
Типичные ошибки
- ✗Думают, что
@inlinableбесплатен — он фиксирует тело в вашем ABI - ✗Считают, что
@frozenпозволяет дальше менять раскладку типа - ✗Полагают, что
@discardableResultменяет момент вычисления результата
Уточняющие вопросы
- →Почему
@frozenважен только для модулей с включённой library evolution? - →Что ломается у клиента, если изменить тело
@inlinable-функции?
SeniorДизайнИногдаВы проектируете публичный API сетевого модуля. Выберите один стиль доставки и защитите его от альтернатив — completion handler на escaping-замыкании, completion handler, возвращающий Result, функция async throws или асинхронный поток AsyncSequence. Места вызова — современный код Swift 6, уже использующий структурированную конкурентность; большинство запросов возвращают одно значение, но один endpoint передаёт обновления прогресса во времени. Нужны отмена, чистое распространение ошибок и тестируемость при компактном API. Какой стиль выберете для одиночных запросов и какой для потокового endpoint и как обоснуете каждый выбор против отвергнутых вариантов с точки зрения отмены, обработки ошибок и композиции с await?
Вы проектируете публичный API сетевого модуля. Выберите один стиль доставки и защитите его от альтернатив — completion handler на escaping-замыкании, completion handler, возвращающий Result, функция async throws или асинхронный поток AsyncSequence. Места вызова — современный код Swift 6, уже использующий структурированную конкурентность; большинство запросов возвращают одно значение, но один endpoint передаёт обновления прогресса во времени. Нужны отмена, чистое распространение ошибок и тестируемость при компактном API. Какой стиль выберете для одиночных запросов и какой для потокового endpoint и как обоснуете каждый выбор против отвергнутых вариантов с точки зрения отмены, обработки ошибок и композиции с await?
Возьмите async throws для одиночных запросов и AsyncSequence для потокового endpoint. async throws компонуется с await, распространяет ошибки через try и получает кооперативную отмену, а completion handler и Result требуют ручной отмены и вложенных колбэков. AsyncSequence — для значений во времени.
Типичные ошибки
- ✗Утверждают, что completion handler отменяется чище, чем структурированные
async-задачи - ✗Загоняют каждый endpoint в один стиль, игнорируя разницу одиночного и потокового
- ✗Считают, что
async throwsтеряет информацию об ошибке в сравнении сResult
Уточняющие вопросы
- →Как отмена задачи доходит до выполняющегося
async-сетевого вызова? - →Когда всё же стоит оставить completion-handler-перегрузку для вызывающих?
SeniorТеорияИногдаЧто делает @autoclosure и почему ?? и assert его используют?
Что делает @autoclosure и почему ?? и assert его используют?
@autoclosure оборачивает выражение-аргумент в замыкание, поэтому оно выполняется только когда вызываемая сторона его вызовет. ?? использует это, чтобы правый по умолчанию считался лишь когда левая часть nil; assert — чтобы пропускать условие в release-сборках. Обычный синтаксис вызова с ленивым вычислением.
Типичные ошибки
- ✗Думают, что
@autoclosureвычисляет аргумент жадно - ✗Считают, что дело в производительности или встраивании, а не в отложенном вычислении
- ✗Полагают, что правое значение
??считается всегда, независимо от левой части
Уточняющие вопросы
- →Какой риск для читаемости несёт злоупотребление
@autoclosure? - →Как
@autoclosure @escapingменяет момент, когда завёрнутое выражение может выполниться?