Константе let значение присваивается ровно один раз, а переменную var можно переприсваивать сколько угодно; по умолчанию пишут let и переходят на var, только когда значение действительно должно меняться.
Отложенная инициализация: let можно объявить сразу, а присвоить позже, но ровно один раз на каждом пути: let x: Int; if ok { x = 1 } else { x = 2 }.
Типы-значения: let замораживает всю структуру или enum целиком: нельзя присвоить свойство и нельзя вызвать mutating-метод.
Ссылочные типы: let фиксирует только ссылку; var-свойства самого объекта через неё по-прежнему можно менять.
Помощь компилятора: Xcode предупреждает, если var ни разу не меняется, а неизменяемые данные проще понимать и безопаснее передавать между потоками.
iOSJunior
Основы Swift
Переменные, optional, типы, control flow, замыкания, value vs reference
Закрыто вопросов
0 / 25
- 0Повторить
- 9Часто спрашивают
Вопросы
Optional либо хранит значение, либо хранит nil, поэтому «значения может не быть» проверяет компилятор, а не падение в рантайме; T? служит сокращением для обобщённого enum Optional<T> с кейсами .some(T) и .none.
Как он устроен
nil означает просто Optional.none; enum реализует ExpressibleByNilLiteral, поэтому литерал и работает
Раз это обычный enum, по нему можно сделать switch по .some / .none и вызывать на нём map / flatMap
Для ссылок на классы nil хранится как нулевой указатель, поэтому UIView? не занимает лишней памяти; Int? требует один дополнительный байт (MemoryLayout<Int?>.size равен 9)
Безопасные способы развернуть
if let / guard let: привязать значение, только если оно есть; if let name переиспользует то же имя
?. (optional chaining): вызов выполняется, только если значение есть, и всё выражение становится опциональным
?? (nil-coalescing): подставить значение по умолчанию, например name ?? "Guest"
! (force unwrap): падает на nil, поэтому оставьте его для значений, которые действительно не могут отсутствовать, например URL из строкового литерала
Оба разворачивают optional, но if let держит значение внутри своих фигурных скобок, а guard let делает его доступным до конца области видимости и требует, чтобы ветка else из этой области вышла.
**else обязан выйти**: через return, throw, break, continue или вызов, возвращающий Never, например fatalError(); else, который «проваливается» дальше, компилятор не пропустит.
**guard**: для предусловий в начале функции: случай nil значит «здесь делать нечего», и основной путь остаётся без отступов, а не уходит вглубь с каждой проверкой.
**if let**: когда случай nil просто ещё одна ветка или значение нужно лишь на пару строк.
Оба принимают несколько условий через запятую, включая обычные булевы проверки: guard let user, user.isActive else { return }.
Код выведет Hello, Optional("Ada"), затем Hello, Ada, затем 5: интерполяция optional печатает описание обёртки Optional, а ?? связывает слабее, чем +.
Строка 1: name всё ещё String?, поэтому интерполяция описывает обёртку, а не текст. Компилятор даже предупреждает: «String interpolation produces a debug description for an optional value».
Строка 2: ?? сначала превращает значение в обычный String, поэтому печатается сам текст.
Строка 3: выражение разбирается как bonus ?? (0 + 1). bonus равен 5, так что значение по умолчанию не используется. Чтобы получить 6, пишите (bonus ?? 0) + 1.
Решите один раз, как должен выглядеть nil (через ??, if let или вычисляемое свойство рядом с данными), а не в каждом print. String(describing:) только убирает предупреждение и по-прежнему печатает Optional("Ada").
String! обозначает implicitly unwrapped optional: внутри тот же Optional<String>, но компилятор вставляет force unwrap везде, где значение используется как неопциональное, поэтому чтение при nil падает точно так же, как !.
Это всё ещё optional: if let, ?. и ?? с ним работают, и его можно присвоить в String? без преобразований.
Почему он у аутлетов: @IBOutlet равен nil от init до загрузки view из сториборда или nib, но к моменту viewDidLoad он всегда подключён.
Типичное падение: обращение к аутлету до загрузки view: сразу после создания контроллера или в prepare(for:sender:) у показывающего контроллера. Передайте данные в обычное свойство и примените их к аутлету в viewDidLoad.
Другие оправданные случаи: sut в тесте, который заполняется в setUp(), и Objective-C API без аннотаций nullability.
Там, где вы сами создаёте объект, передайте значение через init и сделайте свойство неопциональным.
Тип-значение копируется при присваивании и передаче в функцию, поэтому у каждой переменной свои данные; ссылочный тип разделяется, поэтому несколько переменных указывают на один экземпляр и видят изменения друг друга.
Типы-значения: struct, enum, кортежи и стандартные коллекции Array, Dictionary, Set и String.
Ссылочные типы: class, actor и замыкания. У них есть идентичность (===), классы можно наследовать, и у них есть deinit.
Память: экземпляры классов живут в куче и освобождаются ARC, когда исчезает последняя сильная ссылка. Типы-значения хранятся прямо в содержащем значении: на стеке для локальных переменных или внутри того, что их содержит, хотя компилятор может перенести их в кучу, например, когда их захватывает escaping-замыкание.
Цена копии: стандартные коллекции используют copy-on-write: копия разделяет буфер, пока одна из сторон его не изменит, поэтому передавать массив дёшево.
Выбор по умолчанию: начинайте со struct; берите class, когда нужно общее изменяемое состояние, идентичность, наследование или совместимость с Objective-C.
== спрашивает, равны ли два значения по содержимому, и приходит из протокола Equatable; === спрашивает, указывают ли две ссылки на один и тот же экземпляр класса.
== работает для любого Equatable-типа. Структурам и enum == синтезируется, как только они объявляют соответствие Equatable и все хранимые свойства Equatable; классам его нужно реализовать самим.
=== и !== принимают AnyObject?, поэтому работают только с экземплярами классов и акторов. У структур нет идентичности, так что для них этот вопрос не имеет смысла.
Два разных объекта могут быть == и при этом не ===: например, два экземпляра User, декодированные из одного и того же JSON.
is проверяет тип значения в рантайме, as? пытается сделать даункаст и возвращает optional, as! делает даункаст принудительно и падает при неудаче, а простой as делает апкаст или бриджинг, безопасность которого компилятор может доказать.
is возвращает Bool: имеет ли значение этот тип или соответствует ли оно протоколу
as?: безопасный даункаст, обычно внутри if let или guard let
as! ведёт себя как force unwrap: падает, если догадка неверна
as: апкаст (Dog в Animal), задание типа литералу (3 as Double) и бриджинг (String в NSString)
Any может хранить значение любого типа, включая структуры, enum, optional и функции; AnyObject хранит только экземпляры классов, поэтому это ещё и ограничение для протоколов только для классов.
Протоколы только для классов: protocol Delegate: AnyObject как раз и позволяет объявить weak var delegate.
Чтение нетипизированных данных: значение из [String: Any] или userInfo уведомления нужно привести через as?, прежде чем использовать.
Новый код: предпочитайте Codable-модели или дженерики вместо Any, который отказывается от проверок на этапе компиляции.
Выбирайте по задаче: Array хранит порядок и допускает дубликаты, Set хранит уникальные элементы с быстрым поиском, а Dictionary сопоставляет уникальные ключи значениям.
Array: упорядочен, доступ по индексу за O(1), добавление в конец за амортизированное O(1); contains, вставка и удаление в начале стоят O(n).
Set: без порядка и без повторов, элементы должны быть Hashable; insert и contains в среднем O(1), плюс union, intersection и subtracting.
Dictionary: пары ключ-значение без порядка с Hashable-ключами; поиск в среднем O(1) и возвращает optional, либо используйте dict[key, default: 0].
Порядок не гарантирован: порядок обхода Set и Dictionary может меняться между запусками, потому что хеширование получает случайный seed в каждом процессе. Никогда на него не полагайтесь.
Все три относятся к типам-значениям с copy-on-write. Классическая оптимизация: contains по массиву внутри цикла даёт O(n²); соберите Set один раз и ищите в нём.
map преобразует каждый элемент один к одному, compactMap преобразует и выбрасывает результаты nil, а flatMap превращает каждый элемент в последовательность и склеивает их все в один массив.
map по [String?] даёт [Int?]: nil никуда не делись.
compactMap служит идиоматичным способом распарсить список, где часть элементов не парсится: ["1", "x"].compactMap(Int.init) даёт [1].
flatMap раскрывает один уровень вложенности: [[1, 2], [3]] становится [1, 2, 3]. Старую перегрузку flatMap, которая убирала nil, в Swift 4.1 переименовали в compactMap.
У Optional метод map преобразует обёрнутое значение, а flatMap нужен, когда само преобразование возвращает optional: получается Int?, а не Int??.
filter оставляет подходящие элементы, а reduce сворачивает последовательность в одно значение.
switch в Swift сопоставляет паттерны, а не только целые числа, обязан быть исчерпывающим и никогда не проваливается в следующий кейс, если не написать fallthrough.
Паттерны: кейсы enum с ассоциированными значениями, диапазоны (case 0..<50), кортежи, типы (case let x as String), optional и строки.
**where**: добавляет условие к кейсу: case .running(let p) where p > 0.9.
Исчерпываемость: должны быть обработаны все возможные значения. Описывайте состояние через enum и пишите switch без default: тогда новый кейс ломает компиляцию каждого switch, который его забыл, и это ошибка компиляции, а не баг.
**@unknown default: для enum из фреймворков Apple, которые могут получить новые кейсы: код компилируется, но при появлении нового кейса вы получите предупреждение.
Нет неявного проваливания**: в каждом кейсе нужен хотя бы один оператор; чтобы ничего не делать, пишите break.
Один паттерн: if case и guard case проверяют один паттерн без полного switch.
Swift никогда не преобразует числовые типы неявно, поэтому Int плюс Double даёт ошибку компиляции, пока вы не напишете Double(count); Float 32-битный, Double 64-битный и используется по умолчанию, а CGFloat служит типом, в котором Core Graphics и UIKit хранят размеры.
Литералы составляют исключение: литерал вроде 3 подстраивается под ожидаемый тип, поэтому let x: Double = 3 компилируется. Без контекста целочисленный литерал становится Int, поэтому let half = 1 / 2 равно 0.
**Float: около 7 значащих цифр; используется в Metal, ML и аудиобуферах, где важна память.
Double: около 15–16 цифр; тип по умолчанию для дробных литералов и правильный выбор для большинства вычислений.
CGFloat**: 64-битный на iPhone, iPad и Mac; начиная со Swift 5.5 он неявно конвертируется в Double и обратно, и это единственное исключение из правила.
Замыкание представляет собой блок кода, который можно сохранить в переменную и передавать дальше, и он помнит переменные той области, где был создан; замыкания, как и функции, относятся к ссылочным типам.
Синтаксис: { (a: Int, b: Int) -> Int in a + b } сокращается до { $0 + $1 }, когда типы можно вывести, а замыкание, переданное последним аргументом, можно вынести за скобки как trailing closure.
Захват: замыкание захватывает саму переменную, а не снимок её значения, поэтому видит последующие изменения и может менять переменную. Список захвата вроде [value] или [weak self] это меняет.
Ссылочный тип: присваивание замыкания второй константе не копирует захваченное состояние; оба имени разделяют его. Поэтому же замыкание, сохранённое в свойство self и использующее self, создаёт retain cycle.
Escaping: параметры-замыкания по умолчанию non-escaping; замыкание, которое сохраняется или вызывается после возврата из функции, нужно пометить @escaping.
Ни одна из них не удерживает объект: weak работает как optional, который становится nil, когда объект освобождён, а unowned неопционален и падает, если обратиться к нему после того, как объект исчез. Используйте [weak self] в escaping-замыкании, которым владеет сам self.
**weak**: всегда optional (weak var или, начиная со Swift 6.2, weak let). Безопасный выбор по умолчанию, когда другой объект может исчезнуть первым: делегаты, колбэки, которые держит сервис, живущий дольше экрана.
**unowned: только когда объект гарантированно живёт дольше ссылки, например дочерний объект ссылается на родителя, который им владеет. Ошибка в этом предположении приводит к крашу, а не к тихому багу.
Когда [weak self] нужен**: когда self владеет замыканием напрямую или через то, что он хранит: сохранённый колбэк, Timer, Combine-sink, лежащий в cancellables, наблюдатель уведомлений.
Когда не нужен: non-escaping замыкания (map, filter, forEach) и короткоживущие escaping, такие как UIView.animate или DispatchQueue.main.async, которые выполняются один раз и освобождаются; сильный захват там лишь ненадолго откладывает освобождение.
Внутри замыкания: guard let self else { return } разворачивает один раз; начиная со Swift 5.8 после этого можно вызывать методы, не написав self..
Функция, которая может завершиться ошибкой, помечается throws, каждый её вызов помечается try, а ошибка либо обрабатывается в do/catch, либо передаётся вызывающему, так что возможность сбоя видна прямо в сигнатуре.
Ошибки: любой тип, соответствующий Error; обычно enum, чтобы вызывающий мог сопоставить конкретные кейсы и прочитать их ассоциированные значения.
try пробрасывает ошибку дальше; внешняя функция тоже должна быть throws, либо вызов должен стоять внутри do/catch.
try? превращает результат в optional и отбрасывает ошибку; подходит, когда причина сбоя не важна.
try! падает при ошибке, с тем же риском, что и force unwrap.
**Result<Success, Failure>**: хранит успех или ошибку как значение: полезно для completion-хендлеров, кеширования или передачи результата кому-то дальше. С async/await большинство случаев покрывает обычный throws.
**rethrows**: функция бросает ошибку, только если бросает переданное в неё замыкание; так работает map.
Typed throws: начиная со Swift 6 можно писать throws(AuthError), и catch знает точный тип ошибки. Нетипизированный throws остаётся выбором по умолчанию для большинства API, потому что типизированная ошибка намертво вшивает тип ошибки в публичную сигнатуру.
Код выведет Changed, а затем 2: сам массив скопировался, поэтому добавление в copy не трогает original, но оба массива хранят ссылки на одни и те же объекты Item.
**Array относится к типам-значениям**: var copy = original даёт независимый массив (copy-on-write делает это дешёвым), поэтому original.count остаётся 2.
В элементах лежат экземпляры класса: при копировании массива копируются ссылки, а не объекты. copy[0] и original[0] указывают на один и тот же экземпляр, поэтому новый title виден через оба массива.
Исправление 1: сделать Item структурой; тогда copy[0].title = "Changed" изменит только копию, и первая строка выведет A.
Исправление 2: если Item должен остаться классом, копируйте объекты явно: original.map { Item(title: $0.title) }.
То же правило действует для структуры со свойством-классом: семантика значений заканчивается на первой сохранённой ссылке.
if var existing = items.first(...) привязывает копию структуры, поэтому quantity += 1 меняет эту локальную копию, а в массиве остаётся старое значение: у каждого товара количество так и остаётся 1.
Семантика значений: CartItem объявлен структурой, поэтому first(where:) возвращает копию. if var лишь делает эту копию изменяемой; связи с items у неё нет, и на закрывающей скобке она выбрасывается.
Исправление: найти индекс через firstIndex(where:) и менять через сабскрипт: items[index].quantity += 1 изменяет элемент прямо в хранилище массива.
Та же ловушка в других местах: for var item in items { item.quantity += 1 } и guard var тоже меняют копии. Перебирайте items.indices, а для словаря пишите dict[key]?.quantity += 1.
Почему это проходит ревью: += читает переменную, поэтому компилятор не выдаёт предупреждение «never read». С классом CartItem тот же код работает, поэтому баг часто появляется, когда модель переводят из класса в структуру.
Как поймать: юнит-тест, который дважды добавляет один и тот же товар и проверяет quantity == 2.
Код выведет captured: 2, а затем capture list: 1: обычное замыкание захватывает саму переменную и читает её в момент вызова, а список захвата копирует значение в момент создания замыкания.
Без списка захвата: замыкание держит хранилище переменной, поэтому видит присваивание, сделанное после его создания, и могло бы само изменить переменную.
**С [value]: текущее значение копируется в неизменяемую константу внутри замыкания; последующие изменения снаружи ему не видны.
Для экземпляров классов**: [object] копирует ссылку, а не объект: замыкание всё равно видит изменения свойств объекта и удерживает его в памяти.
**[weak self] и [unowned self]**: тот же механизм с другим владением, который используют, чтобы разорвать retain cycle.
Код выведет 0 1, затем C, B, A: блок defer выполняется при выходе из своей области видимости, уже после того, как возвращаемое значение вычислено, а несколько блоков defer выполняются в обратном порядке.
**После вычисления return**: next() возвращает старое значение 0, затем срабатывает отложенный инкремент, поэтому к моменту печати counter уже равен 1.
Последним пришёл, первым ушёл: первым выполняется последний зарегистрированный defer, поэтому B идёт раньше A.
Любой выход: он срабатывает при return, при брошенной ошибке, при break и когда выполнение доходит до закрывающей скобки, поэтому это место для парной очистки: isLoading = false, lock.unlock(), закрытие файла.
Область задают фигурные скобки: defer внутри тела цикла срабатывает в конце каждой итерации, а не в конце функции.
Только если до него дошли: defer, написанный после раннего return, не выполнится, и ничего не выполняется при краше или fatalError().
Параметр inout работает по схеме copy-in, copy-out: функция получает изменяемую копию вашей переменной и записывает её обратно при возврате, поэтому вызывающий видит изменение; аргумент передаётся с &.
Только изменяемое хранилище: var, свойство или элемент массива; let или литерал не скомпилируются.
Эксклюзивный доступ: передать одну и ту же переменную в два inout-параметра нельзя: это отклонит компилятор или остановит проверка в рантайме, потому что две обратные записи конфликтовали бы.
Не может пережить вызов: escaping-замыкание не может захватить inout-параметр: позже некуда было бы записывать результат.
**mutating-методы**: внутри такого метода self по сути является inout-параметром.
Экземпляры классов: ссылка и так общая, поэтому функция может менять свойства объекта без inout. inout у параметра-класса означает другое: функция может заменить объект вызывающего на другой.
Предпочитайте возвращать новое значение; inout используйте для алгоритмов, работающих на месте с большими значениями.
Character в Swift представляет собой графемный кластер, который может занимать несколько байт, поэтому нет способа за постоянное время перейти к четвёртому символу; у String намеренно нет целочисленного сабскрипта, и по строке перемещаются через String.Index.
Индексы непрозрачны: их получают из startIndex, index(_:offsetBy:) или firstIndex(of:); индекс одной строки бессмыслен для другой.
**count работает за O(n)**: он проходит всю строку, поэтому проверяйте isEmpty, а не count == 0.
Представления по кодовым единицам: utf8, utf16 и unicodeScalars, когда нужны байты или кодовые единицы.
**Substring**: то, что возвращает срез (prefix, split, s[range]). Он разделяет память с исходной строкой, поэтому срез дешёвый, но удерживает в памяти весь оригинал. Перед сохранением конвертируйте через String(sub).
dropFirst возвращает ArraySlice, который сохраняет индексы исходного массива, поэтому его первый элемент лежит по индексу 1, а не 0, и rest[0] действительно выходит за границы.
dropFirst, dropLast, prefix, suffix и сабскрипты с диапазоном вроде items[2...] возвращают ArraySlice, а не новый Array.
count считает элементы, но допустимые индексы идут от startIndex до endIndex, не включая его: для массива из четырёх элементов count равен 3, а индексы равны 1, 2 и 3.
enumerated() выдаёт смещения, а не индексы, поэтому rest[offset] скрывает ту же ошибку.
Безопасный доступ: rest.first, rest[rest.startIndex], цикл по rest.indices или Array(rest), чтобы индексы начинались с 0.
Память: срез удерживает весь базовый массив; перед сохранением в свойство конвертируйте через Array(slice). Substring ведёт себя так же.
Double хранит двоичное число с плавающей точкой, и у значений вроде 1.1 или 0.1 нет точного двоичного представления, поэтому каждая операция добавляет крошечную ошибку, которая всплывает при печати результата; храните деньги в целых минорных единицах или в Decimal.
Целые минорные единицы: Int с количеством центов или копеек: точно, быстро, легко суммировать. Попросите бэкенд присылать суммы в таком виде.
**Decimal: десятичная арифметика до 38 значащих цифр; правильный выбор, как только появляются налоговые ставки и проценты.
Создавайте Decimal из строки**: Decimal(string: "19.99"). Decimal(19.99) и let d: Decimal = 19.99 сначала проходят через Double и приносят ошибку с собой.
Конвертация тоже ловушка: Int(4.35 * 100) равно 434, потому что произведение равно 434.99999999999994, а Int(_:) отбрасывает дробную часть.
Округляйте один раз и осознанно: режим округления и шаг (по строке или по заказу) задаются бизнес-правилами; NSDecimalRound принимает и количество знаков, и режим.
Форматируйте на границе: amount.formatted(.currency(code: "USD")) учитывает локаль пользователя.
Double остаётся правильным типом для измерений, геометрии и анимаций.
assert проверяется только в Debug-сборке, precondition проверяется и в Debug, и в Release, а fatalError останавливает приложение всегда; выбирайте по тому, можно ли продолжать работу в продакшене, если допущение нарушено.
**assert / assertionFailure**: ловят ошибки программиста во время разработки. В Release-сборке (-O) условие даже не вычисляется, поэтому никогда не кладите в него побочные эффекты.
**precondition / preconditionFailure**: защищают инварианты, нарушение которых испортит данные; они останавливают приложение и в Release. Убирает их только -Ounchecked, и с ним собирают немногие приложения.
**fatalError**: падает в любой сборке и возвращает Never, поэтому компилятор принимает его там, где нужно значение или выход: недостижимый default или required init?(coder:), который вы не поддерживаете.
Дешёвые сообщения: аргумент-сообщение объявлен как @autoclosure, поэтому строка не строится, если проверка прошла.
Это не обработка ошибок: некорректный ввод пользователя или неудачный запрос должны приводить к throw или optional, а не к крашу приложения.