├── .github └── workflows │ └── speller.yml ├── .yaspellerrc.json ├── README.md └── src ├── ArchitecturalDesignPattern.md ├── MemoryManagement.md ├── Misc.md ├── SIL.md ├── Swift.md ├── Swift5.9.md ├── SwiftUI.md ├── ThreadingConcurrency.md ├── assemblyVision.md └── proposals ├── Actors.md ├── AsyncAwait.md ├── Sendable.md └── StructuredConcurrency.md /.github/workflows/speller.yml: -------------------------------------------------------------------------------- 1 | name: Search typos 2 | on: [pull_request] 3 | 4 | jobs: 5 | yaspeller: 6 | runs-on: ubuntu-latest 7 | name: Check spelling at PR 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions/setup-node@v3 11 | with: 12 | node-version: '18' 13 | 14 | - run: npm install -g yaspeller 15 | - run: yaspeller src/ 16 | -------------------------------------------------------------------------------- /.yaspellerrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "lang": "ru", 3 | "format": "markdown", 4 | "ignoreUrls": true, 5 | "ignoreCapitalization": true, 6 | "fileExtensions": [ 7 | ".md" 8 | ], 9 | "dictionary": [ 10 | "ая", 11 | "ти", 12 | "(|[А-Я]|)П", 13 | "проперти", 14 | "аллоцировать", 15 | "метатип(|[а-я]|)", 16 | "однопоточност(|[а-я]|)", 17 | "платформозависимые", 18 | "тупл(|[а-я])", 19 | "мангленный", 20 | "манглинга(|[а-я])", 21 | "кодогенераци(|[а-я])", 22 | "антипаттерн(|[а-я]|ом)", 23 | "клож(|[а-я]|ах|)", 24 | "фич(|[а-я])", 25 | "беззнаков(|[а-я]|ого|ыми)", 26 | "рантайм(|[а-я])", 27 | "потоко(|вость|подобные|безопасны|безопасность|безопасного|)", 28 | "врап(|пер|перов|пера|)", 29 | "бинд(|инг)", 30 | "вью(|[а-я]|хах|х[а-я])", 31 | "инлайн(|ится|)", 32 | "краш(|[а-я]|нется|ам|)", 33 | "кастом(|ная|ным|ных|ное|)", 34 | "кастомиз(|[а-я]|ации|ировать)", 35 | "таргет(|[а-я]|ов)", 36 | "скоуп(|[а-я])", 37 | "девайс(|[а-я])", 38 | "стек(|[а-я])", 39 | "мьютекс(|[а-я]|)", 40 | "инстанс(|[а-я]|ом|)", 41 | "дефолт(|н|[а-я]|ное)", 42 | "корутин(|[а-я]|)", 43 | "свитчинг(|[а-я]|)", 44 | "дебажить", 45 | "кастом(|[а-я]|ный|ную|)", 46 | "профайлинг(|[а-я]|)", 47 | "обнал(|[а-я]|)", 48 | "масштабируемост(|[а-я]|)", 49 | "деинициализатор(|[а-я]|)", 50 | "актор(|[а-я]|ным|)", 51 | "токен(|[а-я]|)", 52 | "релиз(|[а-я]|ной)", 53 | "сабскрипт(|[а-я]|ов|)", 54 | "дженерик(|[а-я]|ам|ами|[а-я]|)", 55 | "фич(|[а-я]|ей)", 56 | "раскомментируйте", 57 | "захардкодили", 58 | "деманглингом", 59 | "девиртуализ(|[а-я]|ация|ируются|)", 60 | "продиагностировано" 61 | ] 62 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift iOS Интервью Вопросы & Ответы 2 | 3 | Репозиторий разделен на 5 категорий: 4 | 5 | 1. [Общие вопросы](src/Swift.md) 6 | 2. [SwiftUI](src/SwiftUI.md) 7 | 3. [Threading & concurrency](src/ThreadingConcurrency.md) 8 | 4. [Управление памятью](src/MemoryManagement.md) 9 | 5. [Архитектура и паттерны проектирования](src/ArchitecturalDesignPattern.md) 10 | -------------------------------------------------------------------------------- /src/ArchitecturalDesignPattern.md: -------------------------------------------------------------------------------- 1 | 5-ая часть содержит вопросы и ответы дизайна и паттернов проектирования архитектуры iOS приложений. 2 | 3 | ## Расскажите об MVVM 4 | 5 | `MVVM`(Model–View–ViewModel) - это архитектурный паттерн проектирования программного обеспечения, который позволяет отделить разработку графического интерфейса пользователя (представление/вью) от разработки бизнес-логики или внутренней логики (модель), чтобы View не зависело от какой-либо модели. 6 | 7 | Компоненты MVVM паттерна: 8 | 9 |
10 | 1. Model 11 | 12 | Представляет собой логику работы с данными и описание фундаментальных данных, необходимых для работы приложения. 13 |
14 | 15 |
16 | 2. View 17 | 18 | Графический интерфейс (в контексте iOS — вьюхи) (окна, списки, кнопки и т. п.). Выступает подписчиком на событие изменения значений свойств или команд, предоставляемых ViewModel. В случае, если в ViewModel изменилось какое-либо свойство, то она оповещает всех подписчиков об этом, и View, в свою очередь, запрашивает обновлённое значение свойства из ViewModel. 19 |
20 | 21 |
22 | 3. ViewModel 23 | 24 | Обёртка данных из модели, подлежащих связыванию. То есть, ViewModel содержит Model, преобразованную к View, а также команды, которыми может пользоваться View, чтобы влиять на Model. 25 |
26 | 27 | ## Расскажите об Singleton pattern 28 | 29 | `Singleton` pattern — паттерн разработки ПО, в котором существует единственный инстанс некоторого класса, предоставляющий глобальную точку доступа к одному (single) инстансу: 30 | 31 | ```swift 32 | let calendar = Calendar.current 33 | let weatherInstance = WeatherService.shared 34 | let userDefaults = UserDefaults.standart 35 | ``` 36 | 37 | Реализация в Swift: 38 | 39 | ```swift 40 | class AsyncLocalNotifications { 41 | private init() {} 42 | static let instance = Self() 43 | } 44 | 45 | // применимо к структурам 46 | struct Settings { 47 | private init() {} 48 | static let shared = Self() 49 | } 50 | ``` 51 | 52 | Как видно, данный паттерн легко реализовать. 53 | 54 | Singleton убивает основную идею разделения, т.к. доступен глобально и делает код завязанным на инстансе, что в свою очередь затрудняет юнит тестирование. 55 | 56 | ## Какой подход лучше Singleton? 57 | 58 | Inversion of control (IoC) и Dependency injection паттерны, которые решают проблему `singleton`. 59 | 60 | ## Расскажите об Dependency Injection 61 | 62 | Dependency Injection (DI) — механизм внедрения зависимостей. Данная техника облегчает разработку и тестирование, т.к. мы можем сделать несколько различных сервисов. 63 | 64 | Зависимости может быть внедрена несколькими способами, с помощью инъекции в конструктор или сеттер. 65 | 66 | ## Расскажите об Decorator design pattern 67 | 68 | Декоратор — структурный шаблон проектирования, предназначенный для динамического подключения дополнительного поведения к объекту. В отличие от наследование, объекты декоратора не ограниченны родительскими классами. В swift данный паттерн достигается с использованием протоколов. 69 | 70 | ## Расскажите об Anti-pattern 71 | 72 | `Anti-pattern` (Антипаттерн) - это распространённый подход к решению класса часто встречающихся проблем, являющийся неэффективным, рискованным или непродуктивным. Известны тем, что их использование даёт негативные последствия. 73 | Иногда `singleton` считается антипаттерном из-за неправильного использования. 74 | 75 | ## Расскажите об Observer pattern 76 | 77 | `Observer` pattern (наблюдатель) - поведенческий шаблон проектирования. Реализует у класса механизм, который позволяет объекту этого класса получать оповещения об изменении состояния других объектов и тем самым наблюдать за ними. 78 | В swift данный паттерн реализуется с помощью Notifications и Key-Value Observing. 79 | 80 | 81 | ## SOLID 82 | 83 | В разработке программного обеспечения SOLID — это акроним пяти принципам проектирования, призванных сделать проекты программного обеспечения более понятными, гибкими и удобными в поддержке. 84 | 85 |
86 | S: single responsibility principle 87 | 88 | Принцип единственной ответственности. 89 | Для каждого класса должно быть определено единственное назначение. 90 |
91 | 92 | 93 |
94 | O: open-closed principle 95 | 96 | Принцип открытости/закрытости. 97 | Программные сущности должны быть открыты для расширения, но закрыты для модификации. 98 |
99 | 100 |
101 | L: Liskov substitution principle 102 | 103 | Принцип подстановки Лисков. 104 | Объекты в программе должны быть заменяемыми на экземпляры их подтипов без изменения правильности выполнения программы. 105 |
106 | 107 |
108 | I: interface segregation principle 109 | 110 | Принцип разделения интерфейса. 111 | Клиенты не должны зависеть от интерфейсов, которые они не используют. 112 |
113 | 114 |
115 | D: dependency inversion principle 116 | 117 | Принцип инверсии зависимостей. 118 | Модули высокого уровня не должен зависеть от модуля низкого уровня. Оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей. 119 |
120 | -------------------------------------------------------------------------------- /src/MemoryManagement.md: -------------------------------------------------------------------------------- 1 | Выделенная вашему приложению память логически делится на три области: 2 | 3 | 1. Статическая память, в которой размещается код приложения, различные библиотеки, метаданные и глобальные переменные, необходимые для работы. 4 | 2. Автоматическая память, в которой хранятся все локальные для текущей области видимости параметры (`Stack`). 5 | 3. Динамическая память, в которой память выделяется динамически по запросу (`Heap`). 6 | 7 | ## Что такое ARC? 8 | 9 | `ARC` - это специальный механизм автоматического подсчета ссылок на этапе компиляции. При использовании ARC память для объектов освобождается только тогда, когда на них имеется ноль сильных ссылок. ARC работает при компиляции, а подсчет ссылок в рантайме. 10 | 11 | 12 | ## Как iOS управляет памятью? 13 | 14 | iOS использует `ARC` (Automatic Reference Counting) для управления памятью. `ARC` увеличивает retain count на 1 когда объект имеет strong reference. Если на объект нет strong reference retain count равен нулю и `ARC` освобождает память. 15 | 16 | ## Как работает ARC? 17 | 18 | Каждый раз, когда вы создаете новый инстанс класса, ARC выделяет участок памяти для хранения информации об этом инстансе. В этой памяти хранится информация о типе инстанса, а также значения всех проперти, связанных с этим инстансом. 19 | 20 | Когда инстанс больше не нужен, `ARC` освобождает память, которая использовалась этим инстансом, чтобы использовать для других целей. Инстансы класса не занимают память, когда они не нужны. 21 | 22 | В том случае, если `ARC` освобождает инстанс, который используется, то доступ к его проперти и/или методам не будет доступен. 23 | При попытке вызвать такой инстанс, в большинстве случаев ваше приложение крашнется (crash). 24 | 25 | Для того, чтобы убедиться, что инстанс не исчезнет когда он нужен, `ARC` отслеживает количество проперти, констант и переменных ссылающихся на каждый инстанс класса. `ARC` не будет будет освобождать память инстанса до тех пор, пока существует хотя бы одна активная ссылка на него. 26 | 27 | Для осуществления вышесказанного, всякий раз при присваивании проперти инстанса, константы или переменной, создается сильная ссылка на инстанс. Ссылка называется «сильной», потому что удерживает инстанс и не позволяет освобождать память до тех пор, пока эта сильная ссылка сохраняется (живет). 28 | 29 | ## Перечислите и расскажите о счетчиках ссылок? 30 | 31 | 1. `Weak` 32 | 2. `Strong` 33 | 3. `Unowned` 34 | 35 | ## Что такое strong reference? 36 | 37 | Strong references используется для описания связей между объектами. Когда объект имеет сильную ссылку на другой объект, это создает `retain cycle`, который предотвращает освобождение памяти и увеличивает счетчик на 1. 38 | 39 | ## Что такое retain cycle? 40 | 41 | `Retain Cycle` - ситуация, когда два объекта хранят ссылку друг на друга и пытаются удержать, что создает `retain cycle` и делает невозможным освобождение (release) памяти. В основном такой цикл случается в классах и кложах (closures). Замыкания «живут» в памяти, поэтому вызывая `self` (является ссылкой), необходимо убедиться, что вы решили проблему с `retain cycle`. 42 | 43 | ## Что такое capture list? 44 | 45 | `capture list` (список захвата) - процедура обработки «захваченных» ссылок. Если вы не используете `capture list`, то по умолчанию все выражения будут иметь `strong reference`. Для объявления `capture lists` используйте ключевое слово `weak` или `unowned`. 46 | 1. `weak` захваченные выражения являются `Optional` и не удерживаются замыканием, таким образом, они могут быть освобождены и установлены в nil. 47 | 2. Используем `unowned` если значение не будет `.none` до освобождения. 48 | 49 | ## В чем разница между unowned и unowned(unsafe)? 50 | 51 | `unowned` гарантирует, что если что-то пойдёт не так, то отловится в trap и будет видна ошибка. С unowned всё ещё работает ARC и его накладные расходы. Плюс такие ссылки дополнительно требуют атомарности, а то ещё лишние такты. 52 | 53 | `unowned(unsafe)` - здесь нет никакого ARC, нет атомарных операций, но есть `UB` если что-то пойдёт не так. 54 | 55 | ## Что такое memory leak? 56 | 57 | Утечка памяти - процесс неконтролируемого уменьшения объёма свободной оперативной или виртуальной памяти компьютера, связанный с ошибками в работающих программах, вовремя не освобождающих память от ненужных данных, или с ошибками системных служб контроля памяти. 58 | 59 | Утечка памяти - это часть памяти, которая остается выделенной, но никогда не используется. 60 | Утечка памяти происходит когда объект остается в памяти даже после того, как `lifecycle` завершился. 61 | Вам следует избегать отношений `strong - strong`, которые создают retain cycle, порождающий утечку памяти. 62 | 63 | 64 | ## Может ли value type передаваться по ссылке? 65 | 66 | Замыкания всегда захватывают значение по ссылке, а не копированием, даже если это value type: 67 | 68 | ```swift 69 | class Man { 70 | var name: String = "class Man" 71 | deinit { print("class \(Self.self) has deinit") } 72 | } 73 | 74 | var fn: (() -> Void)? 75 | 76 | if true { 77 | let man = Man() 78 | fn = { 79 | print(man.name) 80 | } 81 | //fn = { [unowned man] in 82 | // print(man.name) 83 | //} 84 | fn!() 85 | } 86 | print("Destroy scope") 87 | ``` 88 | 89 | По выводу на консоль видно, что экземпляр создается, однако перед завершением программы его деинициализатор не вызывается. Так как замыкание является внешним по отношению к условному оператору, а ссылка сильной, то Swift самостоятельно не может принять решение о возможности удаления ссылки и последующего уничтожения экземпляра. Для решения проблемы в замыкании необходимо выполнить захват переменной, указав при этом, что в переменной содержится слабая ссылка (раскомментируйте `[unowned man]`). 90 | 91 | 92 | ## Что такое circular dependencies? 93 | 94 | Ошибка в дизайне проектирования. Когда два или более модуля прямо или косвенно зависят друг от друга. 95 | 96 | ## Объясните механизм COW? 97 | 98 | `Copy-On-Write`, `COW` (механизм копирования при записи) используется для оптимизации процессов. Идея подхода `copy-on-write` заключается в том, что при чтении области данных используется общая копия, в случае изменения данных — создается новая копия: 99 | 100 | ```swift 101 | func currentAddress(_ v: UnsafeRawPointer) { 102 | print(Int(bitPattern: v)) 103 | } 104 | 105 | var arr: [Any] = [1, 3, 5, "Anna"] 106 | var secondArr = arr 107 | 108 | currentAddress(arr) // 5519730800 109 | currentAddress(secondArr) // 5519730800 110 | ``` 111 | 112 | Оба массива ссылаются на один указатель в памяти `5519730800`. Попробуем добавить новый элемент в `secondArr`: 113 | 114 | ```swift 115 | secondArr.append(23) 116 | 117 | currentAddress(arr) // 5388678352 118 | currentAddress(secondArr) // 5388689776 119 | ``` 120 | 121 | Как можно заметить ссылки ведут на разные области в памяти. 122 | Это избавляет программу от лишних копирований, тем самым улучшая производительность. 123 | 124 | 125 | ## Что такое method dispatch? 126 | 127 | `Method dispatch` — это процесс выбора имплементации метода при его вызове. 128 | 129 | Виды диспетчеризации: 130 | 131 | ```mermaid 132 | graph TD 133 | A[Method Dispatch] -->B(Direct) 134 | A --> C(Table) 135 | A --> D(Message) 136 | C --> F(Virtual-table) 137 | C --> S(Witness-table) 138 | ``` 139 | 140 | 1. `Direct` 141 | 142 | - [x] Самый быстрый тип диспетчеризации 143 | - [ ] Отсутствие полиморфизма (протоколы) 144 | - [ ] Отсутствие наследования (классы) 145 | 146 | 2. `Virtual-table` 147 | 148 | - [ ] Медленнее, чем `Direct` 149 | - [x] Реализует полиморфизм 150 | - [ ] Отсутствие наследования 151 | 152 | 3. `Witness-table` 153 | 154 | - [ ] Медленнее, чем `Direct` 155 | - [x] Реализует полиморфизм 156 | - [x] Реализует наследование 157 | 158 | 4. `Message` 159 | 160 | - [x] `KVC` / `KVO` 161 | - [x] Method swizzling 162 | - [ ] Самый медленный 163 | 164 | > ⚠️ Message используется только для совместимости с Obc-C runtime (NextStep классы) 165 | 166 | 167 | ## В чем разница между garbage collector и ARC? 168 | 169 | Технически `ARC` является формой `GC`, но детерминированным: есть предсказуемость в поведении. Языки программирования как `java` и `C#` используют недетерминированный сборщик мусора. Это означает, что вы не можете точно сказать, когда объекты будут утилизированы, потому что этим управляет внешний процесс runtime. 170 | 171 | Основным преимуществом `ARC` является детерминированное поведение и предсказуемая деконструкция. Объекты освобождаются немедленно, когда они больше не нужны. 172 | 173 | Однако `ARC` не может работать с reference cycle без вмешательства разработчика. С другой стороны, основным преимуществом сборщика мусора заключается в обнаружении reference cycle. 174 | 175 | ## Что такое MemoryLayout и как посчитать размер протокола? 176 | 177 | `MemoryLayout` - `enum`, с помощью которого можно [узнать информацию о размере][memory_layout], выравнивании и шаге какого-либо типа данных: 178 | 179 | ```swift 180 | @frozen 181 | public enum MemoryLayout { 182 | /// The contiguous memory footprint of `T`, in bytes. 183 | /// 184 | /// A type's size does not include any dynamically allocated or out of line 185 | /// storage. In particular, `MemoryLayout.size`, when `T` is a class 186 | /// type, is the same regardless of how many stored properties `T` has. 187 | /// 188 | /// When allocating memory for multiple instances of `T` using an unsafe 189 | /// pointer, use a multiple of the type's stride instead of its size. 190 | @_transparent 191 | public static var size: Int { 192 | return Int(Builtin.sizeof(T.self)) 193 | } 194 | 195 | /// The number of bytes from the start of one instance of `T` to the start of 196 | /// the next when stored in contiguous memory or in an `Array`. 197 | /// 198 | /// This is the same as the number of bytes moved when an `UnsafePointer` 199 | /// instance is incremented. `T` may have a lower minimal alignment that 200 | /// trades runtime performance for space efficiency. This value is always 201 | /// positive. 202 | @_transparent 203 | public static var stride: Int { 204 | return Int(Builtin.strideof(T.self)) 205 | } 206 | 207 | /// The default memory alignment of `T`, in bytes. 208 | /// 209 | /// Use the `alignment` property for a type when allocating memory using an 210 | /// unsafe pointer. This value is always positive. 211 | @_transparent 212 | public static var alignment: Int { 213 | return Int(Builtin.alignof(T.self)) 214 | } 215 | } 216 | ``` 217 | 218 | Рассмотрим пустой протокол `P`: 219 | 220 | ```swift 221 | protocol P { } 222 | print(MemoryLayout

.size) // 40 223 | ``` 224 | 225 | Обычный экзистенциальный контейнер по умолчанию содержит: 226 | - Буфер на три машинных слова для хранимого значения. Имеем на x64: 8 + 8 + 8 = 24 227 | - Ссылка на Метатип — ещё 8 228 | - Ссылка на массив таблиц `Protocol Witness Table` - снова 8 229 | Итого: 24 + 8 + 8 = 40 230 | 231 | ```swift 232 | print(MemoryLayout.size) // 32 233 | ``` 234 | 235 | `Sendable` это маркер протокол, а значит он не содержит ссылки на массив таблиц Protocol Witness Table. 236 | Итого: 40 - 8 = 32 237 | 238 | ```swift 239 | print(MemoryLayout.size) // 8 240 | ``` 241 | 242 | `AnyObject` - это специальный вид экзистенциального контейнера `Class Existential Containers`: 243 | - Содержит указатель на память в хипе - это 8 244 | - Нет ссылки на массив таблиц Protocol Witness Table 245 | Итого: 8 246 | 247 | 248 | ```swift 249 | print(MemoryLayout.size) // 16 250 | ``` 251 | 252 | `Actor` наследуется от `AnyObject`, а значит Class Existential Containers: 253 | - Содержит указатель на память в куче - это 8 254 | - Есть ссылка на массив таблиц Protocol Witness Table - ещё 8 255 | Итого: 8 + 8 = 16 256 | 257 | [memory_layout]: https://developer.apple.com/documentation/swift/memorylayout 258 | -------------------------------------------------------------------------------- /src/Misc.md: -------------------------------------------------------------------------------- 1 | Данный файл содержит список некоторых «скрытых» функций, которые можно найти в исходном коде компилятора. 2 | Поисковый запрос на GitHub: `repo:apple/swift public func path:/^stdlib\/public\/core\//` 3 | 4 | ```swift 5 | print(_canBeClass(MainActor.self)) 6 | print(_undefined("Unimplemented")) 7 | print(_isStdlibDebugChecksEnabled()) 8 | print(_isDebugAssertConfiguration()) 9 | print(_isPowerOf2(4)) 10 | print(_SwiftStdlibVersion.current) 11 | _log(2.0) 12 | 13 | // Retain count 14 | _getRetainCount() 15 | _getWeakRetainCount() 16 | _getUnownedRetainCount() 17 | 18 | // Дебаг для SwiftUI вьюх 19 | // SO: https://stackoverflow.com/questions/69859370/where-is-self-printchanges-defined-and-or-documented-for-swiftui 20 | let _ = Self._printChanges() 21 | ``` 22 | 23 | Для того, чтобы вывести имя типа по средствам языка Swift, вначале получим мангленный символ: 24 | 25 | ```swift 26 | func get_type_by_name(_ t: T.Type) -> Any.Type? { 27 | let mangledType: String? = _mangledTypeName(t) 28 | 29 | guard mangledType != nil else { 30 | return nil 31 | } 32 | let typeName: Any.Type? = _typeByName(mangledType!) 33 | 34 | return typeName 35 | } 36 | ``` 37 | 38 | Выведем тип для `_SwiftStdlibVersion` и опционального Int8: 39 | 40 | ```swift 41 | print(get_type_by_name(_SwiftStdlibVersion.self)) 42 | print(get_type_by_name(Optional.self)) 43 | ``` 44 | 45 | Более комплексный пример: 46 | 47 | ```swift 48 | protocol P {} 49 | struct A: P {} 50 | struct B: P {} 51 | struct MV {} 52 | 53 | print(get_type_by_name(MV.self)) 54 | ``` 55 | -------------------------------------------------------------------------------- /src/SIL.md: -------------------------------------------------------------------------------- 1 | # Swift Intermediate Language (SIL) 2 | 3 | SIL — высокоуровневая семантическая информация (представление) о коде SSA формы IR. Такое представление необходимо для реализации языка Swift. `SIL` используется для анализа потока данных, который обеспечивает выполнение требования самого языка Swift, таких как, например, окончательная инициализация переменных и конструкторов, достижимость различных участков кода, покрытие всех условий switch’a. 4 | Также благодаря `SIL` осуществляются более высокоуровневые оптимизации: retain/release оптимизация, девиртуализация динамических функций, closure inlining, создание и спецификация generic-функций. 5 | 6 | ## SSA 7 | 8 | `Static Single Assignment` — позволяет выражать код программы в виде графа. В нем отсутствуют переменные в привычном нам понимании. 9 | 10 | ## 2 формы SIL 11 | 12 | SIL представляется двумя инвариантами: 13 | 14 | 1. Raw SIL — данная форма получается на этапе SILGen, она не оптимизирована и не продиагностировано. Raw SIL может содержать в себе ошибки потоков данных, некоторые инструкции могут находиться в неканонических представлениях. Данный формат не должен быть использован для генерации или распространения нативного кода. 15 | 16 | 2. Canonical SIL — форма, получаемая из Raw SIL после диагностики и оптимизации. В ней уже устранены все ошибки потоков данных, и инструкции приведены к каноническим представлениям. Только эту форму можно использовать для генерации промежуточного представления LLVM или распространения. 17 | 18 | ## Использование 19 | 20 | ```bash 21 | # Abstract syntax tree (AST) 22 | swiftc -dump-ast example.swift 23 | 24 | # Swift intermediate language(SIl) без оптимизаций 25 | swiftc -emit-silgen example.swift 26 | 27 | 28 | # SIL с оптимизациями 29 | swiftc -emit-sil example.swift 30 | 31 | # LLVM Intermediate representation(IR) с оптимизациями LLVM 32 | swiftc -emit-ir example.swift 33 | 34 | # Ассемблер. Финальный вариант, после всех этапов выше. 35 | swiftc -S example.swift 36 | ``` 37 | 38 | Но при этом важно понимать, что в SIL все символы подвергаются так называемому `Name mangling` и становится не ясно, где и какой символ ты сейчас видишь. Обратная процедура называется деманглингом и выполняется так: 39 | 40 | ```bash 41 | swiftc -emit-sil example.swift | swift demangle 42 | ``` 43 | 44 | ## Полная инструкция 45 | 46 | [Доступна в репозитории компилятора](https://github.com/apple/swift/blob/main/docs/SIL.rst). -------------------------------------------------------------------------------- /src/Swift.md: -------------------------------------------------------------------------------- 1 | ## Что такое вывод типов (type inference)? 2 | 3 | [Возможность компилятора][type_inference] самому логически вывести тип значения у выражения: 4 | 5 | ```swift 6 | // константа `myStr` имеет тип данных `String` 7 | let myStr = "Hello, World!" 8 | 9 | // явно выводим тип данных 10 | let myStr: String = "Hello, World!" 11 | ``` 12 | 13 | ## Для чего используется ключевое слово fallthrough? 14 | 15 | С помощью ключевого слова `fallthrough` можно изменить логику функционирования оператора switch и не прерывать его работу после выполнения кода в case блоке. Данное ключевое слово позволяет перейти к телу последующего case-блока: 16 | 17 | ```swift 18 | let lvl: Character = "B" 19 | 20 | switch lvl { 21 | case "A": 22 | print("First case is A") 23 | fallthrough 24 | case "B": 25 | print("Second case is B") 26 | fallthrough 27 | case "C": 28 | print("Last one is C") 29 | default: 30 | break 31 | } 32 | 33 | // Second case is B 34 | // Last one is C 35 | ``` 36 | 37 | ## Что такое autoclosure? 38 | 39 | `Автозамыкания`(autoclosure) - это замыкания, которые автоматически создаются из переданного выражения. 40 | Например, существует функция, имеющая один или несколько входных параметров, которые при ее вызове передаются как значения, но во внутренней реализации функции используются как самостоятельные замыкания: 41 | 42 | ```swift 43 | func printName(nextName: @autoclosure () -> String) { 44 | print(nextName()) 45 | } 46 | ``` 47 | 48 | С помощью автозамыканий можно: отложить вычисление переданного значения и передавать значение в виде значения (без фигурных скобок). 49 | 50 | ## Что такое escaping closure? 51 | 52 | По умолчанию, замыкания имеют ограниченную область видимости, поэтому замыкания переданные в параметр функции не выходят за пределы тела функции. Для того, чтобы позволить замыканию выйти за пределы области видимости, необходимо указать атрибут `@escaping`. 53 | 54 | ## В чем разница между Int и UInt? 55 | 56 | Разница между знаковыми и беззнаковыми целочисленными типами в том, что значение знакового типа данных может находиться в интервале от `–2^n–2` до `+2^n–2`, а беззнакового — от `0` до `+2^n–1`, где n — разрядность типа данных (8, 16, 32 или 64). 57 | 58 | ## Что такое Generics (обобщенное программирование)? 59 | 60 | [Парадигма программирования][generics], заключающаяся в таком описании данных и алгоритмов, которое можно применять к различным типам данных, не меняя само описание. Парадигма позволяет писать код, который избавляет от дублирования и выражает цели в ясном, абстрактном подходе: 61 | 62 | ```swift 63 | // T — Generic тип 64 | func swapTwoValues(_ a: inout T, _ b: inout T) { 65 | (a, b) = (b, a) 66 | } 67 | 68 | var min = UInt8.min // 0 69 | var max = UInt8.max // 255 70 | 71 | swapTwoValues(&min, &max) 72 | 73 | var first = "first" 74 | var second = "second" 75 | 76 | swapTwoValues(&first, &second) 77 | ``` 78 | 79 | ## Что такое Протоколы (protocols)? 80 | 81 | [Протокол][protocols] - это набор методов, свойств (проперти) и других требований, которые соответствуют определенной задаче или функциональности. Протокол может быть принят классом, структурой или перечислением для реализации (implementation) этих требований. 82 | 83 | ```swift 84 | protocol Human { 85 | var givenName: String { get } 86 | } 87 | 88 | struct Person: Human { 89 | var givenName: String 90 | } 91 | 92 | let nick = Person(givenName: "Nick") // nick.givenName это "Nick" 93 | ``` 94 | 95 | > ⚠️ Протокол не содержит реализаций (implementation) свойств и методов. Проперти не содержит данных, а тело метода пустое. Тип, который наследуется от протокола, должен реализовывать все проперти и методы, которые присутствуют в данном протоколе. 96 | 97 | 98 | ## Протокол только для класса: protocol: class? 99 | 100 | Вы можете ограничить применение протокола исключительно на классы, запретив его использование для структур и перечислений. Для этого после имени протокола через двоеточие необходимо указать ключевое слово `class`, после которого могут быть определены родительские протоколы: 101 | 102 | ```swift 103 | protocol SuperProtocol { } 104 | protocol SubProtocol: class, SuperProtocol { } 105 | 106 | class ClassWithProtocol: SubProtocol { } // корректно 107 | struct StructWithProtocol: SubProtocol { } // ошибка 108 | ``` 109 | 110 | 111 | ## Что такое Tuples (тупл, кортёж)? 112 | 113 | Объект, который группирует значения различных типов в пределах одного составного значения. 114 | 115 | ```swift 116 | let coordinates: (Int, Int) = (2, 3) 117 | 118 | var someTuple = (top: 10, bottom: 12) // someTuple is of type (top: Int, bottom: Int) 119 | someTuple = (top: 4, bottom: 42) // OK: names match 120 | someTuple = (9, 99) // OK: names are inferred 121 | someTuple = (left: 5, right: 5) // Error: names don't match 122 | ``` 123 | 124 | > Типом данных тупла является фиксированная упорядоченная последовательность имен типов данных элементов тупла. 125 | 126 | 127 | ## Расскажите о коллекциях (collection)? 128 | 129 | `Collection` — это протокол, последовательность (`Sequence`), в которой можно обращаться к отдельному элементу напрямую. Не может быть бесконечной (в отличие от `Sequence`). 130 | 131 | ## Что такое Optional? (опциональное значение) 132 | 133 | Перечисление `Optional` c двумя кейсами: `.some(Wrapped)` и `.none`: 134 | 135 | ```swift 136 | @frozen 137 | public enum Optional: ExpressibleByNilLiteral { 138 | case none 139 | case some(Wrapped) 140 | } 141 | ``` 142 | 143 | Тип данных, который предоставляет обернутое значение или `nil` | `.none`, отсутствие значения. Иными словами, значение может быть или не может. Можно записать как `Optional`, но предпочтительна более коротка форма `Int?`: 144 | 145 | ```swift 146 | let shortForm: Int? = Int("42") 147 | let longForm: Optional = Int("42") 148 | 149 | let number: Int? = Optional.some(42) 150 | let noNumber: Int? = Optional.none 151 | print(noNumber == nil) // true 152 | ``` 153 | 154 | Развернуть (получить) значение можно с помощью: 155 | - `Optional Binding` (if let, guard let, switch) 156 | 157 | ```swift 158 | if let starPath = imagePaths["star"] { 159 | print("The star image is at '\(starPath)'") 160 | } else { 161 | print("Couldn't find the star image") 162 | } 163 | ``` 164 | 165 | - `Optional Chaining` используя опциональный постфикс оператор (postfix ?) 166 | 167 | ```swift 168 | if imagePaths["star"]?.hasSuffix(".png") == true { 169 | print("The star image is in PNG format") 170 | } 171 | ``` 172 | 173 | - `Nil-Coalescing Operator ??`, предоставляя дефолтное значение 174 | 175 | ```swift 176 | let defaultImagePath = "/images/default.png" 177 | let heartPath = imagePaths["heart"] ?? defaultImagePath 178 | print(heartPath) 179 | ``` 180 | 181 | - `Unconditional (Force) Unwrapping !` 182 | 183 | ```swift 184 | let number = Int("42")! 185 | // или c помощью проперти let number = Int("42").unsafelyUnwrapped 186 | print(number) 187 | ``` 188 | 189 | > ⚠️ Принудительная развертка выражения со значением `nil` приведет к `runtime error`. 190 | 191 | 192 | ## Что такое `Inout` и как его использовать? 193 | 194 | По умолчанию все параметры в функциях являются неизменяемыми константами (`immutable let`). Это означает, что ты не можешь изменить значение параметра, т.е. если ты захочешь изменить значение параметра функции в теле самой функции, то получишь ошибку компиляции. Если возникла такая необходимость, то используй ключевое слово `inout` между параметром и типом данных: 195 | 196 | ```swift 197 | func reverseName(_ name: inout String) { 198 | name = "github.com/" + String(name.reversed()) 199 | } 200 | 201 | var githubUser = "@wmorgue" 202 | print(githubUser) // @wmorgue 203 | reverseName(&githubUser) // github.com/eugromw@ 204 | print(githubUser) 205 | ``` 206 | 207 | > ⚠️ Ты не можешь установить дефолтное значение `_ name: inout String = "@wmorgue"`. 208 | Когда передаешь значение в `inout` параметр, то используй амперсанд `&` перед выражением. 209 | 210 | 211 | ## Разница между Self и self? 212 | 213 | `Self` указывает на тип протокола, класса, структуры. 214 | 215 | `self` указывает на значение, которое оно содержит. 216 | 217 | 218 | ## Что такое lazy? Ленивое свойство хранения и когда оно используется? 219 | 220 | Ленивое свойство хранения - свойство, начальное значение которого не вычисляется до первого использования. Индикатор ленивого свойства - ключевое слово `lazy`. 221 | Существует два типа ленивых элементов: 222 | 223 | 1. `lazy-by-name` — значение элемента вычисляется при каждом обращении к нему 224 | 2. `lazy-by-need` — элемент вычисляется один раз при первом обращении к нему, после чего фиксируется и больше не изменяется 225 | 226 | > ⚠️ Всегда объявляйте свойства ленивого хранения как переменные `var`, потому что ее значение может быть не получено до окончания инициализации. 227 | 228 | 229 | ## Что такое defer? 230 | 231 | Оператор `defer` — это блок кода, который будет выполнятся в случае выхода из текущей области видимости. 232 | 233 | ```swift 234 | func printStuff() { 235 | defer { 236 | print("Напечатаю 4 и выйду из области видимости") 237 | } 238 | print("4") 239 | } 240 | 241 | printStuff() 242 | // Выхлоп: 243 | // 4 244 | // Напечатаю 4 и выйду из области видимости 245 | ``` 246 | 247 | Как видно, `defer` откладывает выполнение определенного кода в теле функции до момента выхода из области видимости, в которой он был использован (например, после окончания выполнения функции). 248 | 249 | ## Разница между Void и ()? 250 | 251 | `Void` является typealias к `()`: 252 | 253 | ```swift 254 | typealias Void = () 255 | ``` 256 | 257 | `Void` является типом данных, а `()` одновременно является типом данных и значением: 258 | 259 | ```swift 260 | func get(_ value: ()) { 261 | print(0) 262 | } 263 | 264 | func get(_ value: ().Type) { 265 | print(1) 266 | } 267 | 268 | get(()) // 0 269 | get(Void.self) // 1 270 | 271 | print(type(of: Int.self)) // Int.Type 272 | print(type(of: Void.self)) // ().Type 273 | print(type(of: ().self)) // () 274 | ``` 275 | 276 | ## В чём разница между guard let и if let? 277 | 278 | Оба `guard let` и `if let` разворачивают опциональные значения, если они имеются. 279 | 280 | Обычно `guard let` используют для раннего выхода из текущей области видимости, поэтому любые значения которые вы разворачиваете, останутся (значения) после проверки в скоупе. 281 | Используя `if let` значение должно использоваться внутри области видимости `if`. 282 | 283 | 284 | ## Преимущества использовать guard let statement? 285 | 286 | Два основных преимущества использовать `guard let`: 287 | 288 | 1. Избежать пирамиду судьбы (pyramid of doom). Множество вложенных `if let`, уходящих в глубину. 289 | 2. Оператор предоставляет ранний выход из функции с помощью `return`. 290 | 291 | ## Что такое вариативный (variadic) параметр? 292 | 293 | Вариативный параметр в функции принимает 0 или несколько значений определенного типа: 294 | 295 | ```swift 296 | // вариативный параметр может принимать любое кол-во чисел 297 | func redeemVouchers(_ greetings: String, voucherValues: Int...) -> String { 298 | let voucherText = voucherValues.count > 0 ? " You have redeemed " + "\(voucherValues.reduce(0, +)) €" : "" 299 | return greetings + voucherText 300 | } 301 | // без вариативного параметра 302 | redeemVouchers("Congratulation!") // "Congratulation!" 303 | 304 | // несколько вариативных параметров 305 | redeemVouchers("Congratulation!", voucherValues: 12, 23, 10) // "Congratulation! You have redeemed 45 €" 306 | 307 | // один вариативный параметр 308 | redeemVouchers("Congratulation!", voucherValues: 10) // "Congratulation! You have redeemed 10 €" 309 | ``` 310 | 311 | Параметр объявляется с тремя точками `...`. Вызывая функцию, ты можешь не передавать значение второму аргументу или передать несколько. 312 | 313 | ## Ключевое слово `final` перед `class`? 314 | 315 | Добавляя ключевое слово `final` классу/членами класса, мы ограничиваем класс/метод/свойство на переопределение(overridden). Так же мы не можем наследоваться от этого класса: 316 | 317 | ```swift 318 | final class MyClass { 319 | final var myVar: String = "You can't override me!" 320 | final func myMethod() -> Optional { nil } 321 | final var myComputedProperty: Int { .zero } 322 | } 323 | ``` 324 | 325 | В определенном стечении обстоятельств, `final` может приводить к тому что, класс будет инлайнится на стеке, что теоретически может увеличить производительность (в типичной прикладной iOS разработке этого не заметить никогда, это может быть важно только при написании библиотек). 326 | 327 | ## Что происходит с классом при компиляции? 328 | 329 | 1. Разбирается в синтаксическом дереве. Отрабатывает алгоритм SIL-level aggregate layout. Строится vtable. Строится value witness table(не для всех). Дополнительная кодогенерация. Проходит процедуру манглинга. Разбивается на SIL блоки, которые содержат SIL инструкции. Проходит процедуру проверки достижимости, мелкие оптимизации. 330 | 2. Далее идут высокоуровневые контекстно-зависимые оптимизации, оптимизация ARC. 331 | 3. Далее все оптимизированные SIL блоки + SIL инструкции конвертируются в LLVM IR инструкции. Там происходят уже платформозависимые оптимизации, где после LLVM превращает эти инструкции в машинный код. 332 | 333 | 334 | ## В чём разница между static и final? 335 | 336 | Статическая переменная/функция доступна всему инстансу класса. `static` используется для проперти/методов, доступ к которым можно получить без создания инстанса класса. С `final` мы не может наследоваться. 337 | 338 | 339 | ## В чём разница между open и public? 340 | 341 | `open` позволяет другим модулям использовать класс и наследоваться от него. Члены `open` класса могут быть переопределены (be overridden) другими модулями. 342 | 343 | `public` позволяет другим модулям использовать только `public` классы, но не может быть подклассом или переопределен другими модулями. 344 | 345 | 346 | ## В чём разница между операторами `==` и `===`? 347 | 348 | Основное различие между двойным `=` и тройным `=` операторами в том, что оператор сравнения `==` сравнивает типы значений, чтобы проверить одинаковы ли они, а `===` оператор сравнивает ссылочные типы, чтобы проверить, указывают ли ссылки на тот же инстанс. 349 | 350 | 351 | ## Почему мы не может объявить open struct? 352 | 353 | Потому что наследование между структурами не возможно. `open` означает, что вы можете переопределить родительский метод/проперти данного класса вне модуля. 354 | 355 | ## В чём разница между fileprivate, private и public private(set) уровнями доступа? 356 | 357 | `fileprivate` доступен в пределах текущего файла. `private` доступен в пределах текущей типа, в котором был объявлен. `public private(set)` обозначает, что getter публичный, а setter приватный. 358 | 359 | ## Что такое внутренний (internal) уровень доступа? 360 | 361 | Внутренний (internal) уровень доступа позволяет использовать свойства и методы внутри модуля, но не в любом исходном файле за пределами модуля. `internal` установлен по умолчанию и не требует явного объявления: 362 | 363 | ```swift 364 | // записи равнозначны 365 | var number = 5 366 | internal var number = 5 367 | ``` 368 | 369 | ## Объясните разницу между структурой и классом в Swift? 370 | 371 | `struct` — value type. Копируются. Изменение одного инстанса не влияет на второй. 372 | 373 | `class` — reference type. Передаются по ссылке. Изменение одного инстанса влечет за собой изменение второго. 374 | 375 | ## Что такое value/refrence type в Swift? 376 | 377 | `Value type` хранит данные в памяти, выделенной на стеке. Когда вы создаете value type, для хранения данных создается одно единственное место(space) в памяти. Существует переменная, в которой хранится структура и вы присваиваете ее другой переменной. В таком случае, `value` копируется напрямую, обе переменные работают независимо и они имеют различный адрес в памяти. 378 | 379 | 380 | `Reference type` хранит в памяти адрес объекта, но не сам объект. Тип указывает адрес переменной, а не сами данные. Присваивая одну переменную другой, данные не копируются. Напротив, создается вторая копия ссылки, которая ссылается на тот же адрес, что и первая. 381 | В отличие от `value`, ссылочный тип не хранит в себе данных. Вместо этого хранится адрес, в котором находятся данные. `Reference type` содержит `pointer`(указатель) на другую область памяти, в которой хранятся данные. 382 | 383 | ### В чём различие? 384 | 385 | Основное различие заключается в том, что `value` type копирует данные, а `refrence` type шарит(shared) одну копию своих данных. Value type является `immutable`(неизменяемым). При объявлении инстанса с `value` type, создается уникальная копия данных. Ссылочный тип является `mutable`(изменяемым) и не создает уникальную копию данных. 386 | 387 | В Swift `class` является ссылочным типом. В Objective-C каждый унаследованный элемент от `NSObject` является ссылочным типом. Struct, enum, tuples — value type в Swift. `NSInteger` - это value type в Objective-C. 388 | 389 | Распространенные value types: 390 | 391 | - struct 392 | - enum 393 | - dictionary 394 | - set 395 | - tuple 396 | 397 | 398 | Распространенные reference types: 399 | 400 | - Class 401 | - Closure 402 | - Function 403 | - Actor 404 | 405 | ### Почему Apple предпочитает использовать value type по умолчанию? 406 | 407 | В языке Swift, value type довольно мощный. 408 | 409 | 1. Value type по умолчанию thread-safe(потокобезопасны) 410 | 2. Инстансы безопасны в multi-threading(многопоточной) среде 411 | 3. Может использовать value type не беспокоясь об race conditions(состояние гонки/гонки данных) или deadlocks(взаимная блокировка). 412 | 413 | Value type (struct) является предпочтительными. Копировать `value` type будет более безопасным, чем иметь много ссылок на `reference` инстанс. Value type не имеет ссылок в отличие от reference, поэтому никаких утечек памяти не происходит. 414 | 415 | 416 | > ⚠️ Если вы захватываете struct внутри closure, то по сути захватываете ссылку на инстанс. 417 | 418 | Используя `value` type, таких как `struct` или `enum`, вы упрощаете разработку. Можете сконцентрироваться на каком-то участке кода, не беря во внимание общее состояние приложения. Локальные изменения value type не доступны/видны остальной части приложения, в отличие от классов. 419 | 420 | ### Когда использовать value или reference type? 421 | 422 | Следуйте рекомендациям ниже для разработки приложений: 423 | 424 | - Используйте структуры там, где это возможно 425 | - Используйте классы для совместимости с Objective-C 426 | - Используйте структуры вместе с протоколами 427 | 428 | 429 | ## Может ли структура хранить данные в хипе? 430 | 431 | Хип и стек - это про то, где лежат данные, `value` и `ref` type - это про то, как они передаются. 432 | Действительно, структура может храниться в хипе. Не зависит от того есть у нее поля типа класса или нет. 433 | 434 | > ⚠️ структура никогда не будет являться `reference` типом 435 | > счетчик ссылок никак не связан с хипом или стеком, счетчик ссылок — это способ считать ссылки когда у тебя их много. Можно держать структуру в хипе и держать на нее всегда ровно одну ссылку, тем самым обходиться без счетчика. 436 | 437 | Важно уточнить, что структура никогда не будет передаваться по ссылке так же как и класс. 438 | Closure имеет ссылку на структуру, но не удерживает ее, то есть это больше похоже на weak ссылку, хотя технически это другой механизм. 439 | 440 | ## Каким простым способом можно реализовать абстракцию в проекте? 441 | 442 | Используя протоколы и разделяя модули, например SPM. 443 | 444 | ## В чём разница между assign and retain? 445 | 446 | `Assigned` элементы получают выделенное место в памяти, а `retain` зависит от жизненного цикла чего-либо выше по иерархии. 447 | 448 | ## Что такое функции первого класса и поддерживает ли их Swift? 449 | 450 | Несмотря на то, что Swift не является чистым функциональным языком программирования, он поддерживает функции первого класса. Это означает, что язык поддерживает передачу функций (closure) в качестве аргументов другим функциям. 451 | 452 | ## Что такое Copy on Write (COW)? 453 | 454 | Основная идея COW заключается в том, что когда несколько «вызовов» хотят получить доступ к одинаковому ресурсу, вы можете разместить их указатели на тот же ресурс. Состояние ресурса будет поддерживаться до тех пор, пока «вызывающий» не попытается изменить «копию» ресурса. 455 | 456 | `Copy-on-write` - это способ оптимизации, который идёт из реализации некоторых типов в стандартной библиотеки. Без типов из стандартной библиотеки нет явного способа сделать свой тип COW. Всякая инструкция COW начинается как `ref_element_addr` и заканчивается `end_cow_mutation`. 457 | 458 | 459 | > ⚠️ На текущий момент (Swift 5.7), COW реализован только для `std`, а не для языка Swift. 460 | 461 | ## Почему порядок ключей будет сохраняться между мутациями в словаре? 462 | 463 | Dictionary — неупорядоченная коллекция. Причина повторяющегося «иногда или при определённых обстоятельствах» порядка ключей заключается в следующем: 464 | 465 | Словарь в Swift, как и в любом другом языке, целиком и полностью основывается на таком понятии как `HashTable`. В свою очередь HashTable представляет из себя такую структуру, в которой "ключи"(на самом деле не ключ, а некая сущность Word, которая получается из хеша ключа + seed + адрес памяти буфера) хранятся в последовательном участке памяти. То есть очень похожее на то, что делает обычный массив, но со своей логикой вставки и удаления: 466 | 467 | ```swift 468 | internal struct _HashTable { 469 | internal typealias Word = _UnsafeBitset.Word 470 | internal var words: UnsafeMutablePointer 471 | internal let bucketMask: Int 472 | //... 473 | } 474 | ``` 475 | 476 | Отсюда, словарь гарантирует, что порядок ключей будет сохраняться только между мутациями. 477 | 478 | > Нет мутаций - порядок сохранился, произошла мутация, порядок изменился. 479 | 480 | Но это снова не даёт ответа на вопрос, почему всё таки при каждом проходе по ключам, словарь выдаёт их в разном порядке. Ведь словарь гарантирует порядок ключей между мутациями, которых мы и не делаем. Всё дело в таком понятии, как `DETERMINISTIC HASHING`. Детерминированный хеш означает, что в рамках одного и того же процесса, хеш от элемента всегда будет одним и тем же. Я напомню, что Словарь не использует просто хеш от ключа, а использует: 481 | 482 | > хеш от ключа в словаре = хеш от ключа + RANDOM_SEED_PER_INSTANCE + адрес памяти буфера. 483 | 484 | Поэтому, когда ты каждый раз пишешь: 485 | 486 | ```swift 487 | for (k, v) in dictionary { 488 | // ... 489 | } 490 | ``` 491 | 492 | создаётся итератор словаря, который в свою очередь содержит в себе итератор `_HashTable`: 493 | 494 | ```swift 495 | internal struct _HashTable.Iterator: IteratorProtocol { 496 | let hashTable: _HashTable 497 | // ... 498 | } 499 | ``` 500 | 501 | то есть каждый раз создаётся новый `_HashTable`, в котором элементы стоят на том же месте (это вообще те же самые элементы), где и стояли, но меняется seed и иногда и адрес памяти буфера. 502 | 503 | Отсюда, несмотря на то, что элементы стоят в том же порядке, что и стояли до этого, порядок их сменился ввиду того что сменились seed и возможно адрес памяти буфера. 504 | 505 | Детерминированный хеш хорошо иметь во время тестов, возможно где-то ещё, но в обычном смысле, такое поведение ничему хорошему не способствует. 506 | Если проводите тесты и вам нужен детерминированный хеш, то можно задать переменную окружения `SWIFT_DETERMINISTIC_HASHING=1` 507 | 508 | ## Расскажите о Swift Intermediate Language(SIL)? 509 | 510 | Swift Intermediate Language — это приватное для компилятора Swift промежуточное представление, которое находится на более низком уровне абстракции, чем AST, но всё ещё выше промежуточного представления LLVM. Для своего представления SIL использует нотацию SSA. 511 | 512 | ## Что такое trailing closure syntax? 513 | 514 | Swift позволяют использовать несколько замыканий в качестве параметра функции. Синтаксис `trailing closure` является синтаксическим сахаром для простоты чтения и написания. Trailing closure syntax используется в качестве последнего параметра функции: 515 | 516 | ```swift 517 | // Without trailing closure syntax 518 | func greeting(title: String, greetingCallBack: () -> ()) { 519 | print("Hello world, \(title)") 520 | greetingCallBack() 521 | } 522 | // With trailing closure syntax 523 | func greeting(title: String) { 524 | print("Hello world, \(title)") 525 | } 526 | ``` 527 | 528 | ## Что такое ассоциированный тип (associated type)? 529 | 530 | Ассоциированный тип в Swift предоставляет нам placeholder name(обозначение), которое будет использоваться как часть протокола. При наследовании от протокола, будет указан фактический тип. Ассоциированный тип делает протокол обобщенным (generic) предоставляя placeholder. 531 | 532 | ## Когда использовать Set вместо Array? 533 | 534 | `Set` - это неупорядоченная коллекция уникальных значений. 535 | Можно использовать `Set` когда элементы коллекции должны быть `hashable`, уникальными и порядок не важен. 536 | 537 | ## В чём разница между as?, as! и as в Swift? 538 | 539 | - `as` используется для upcasting. 540 | - `as?` Опциональная форма оператора понижающего приведения. Возвращает опциональное значение типа, к которому вы пытаетесь привести. Вернёт `nil` в случае неудачи. 541 | - `as!` принудительная форма оператора понижающего приведения. Приложение упадет, если каст завершился неудачно. 542 | 543 | 544 | 545 | [type_inference]: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/thebasics/#Type-Safety-and-Type-Inference 546 | [generics]: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/generics/ 547 | [protocols]: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/protocols/ 548 | -------------------------------------------------------------------------------- /src/Swift5.9.md: -------------------------------------------------------------------------------- 1 | Новые архитектуры в [Conditional Compilation Block](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/statements/#Conditional-Compilation-Block): 2 | 3 | ```swift 4 | #if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le) 5 | ... 6 | #elseif arch(i386) || arch(arm) || arch(arm64_32) 7 | ... 8 | #else 9 | #error ("Some error") 10 | #endif 11 | ``` 12 | 13 | WebAssembly: `#if os(WASI)` 14 | Проверка на Foundation: `#if FOUNDATION_FRAMEWORK` 15 | 16 | --- 17 | 18 | ## Expression Macros 19 | 20 | [Макросы позволяют](https://github.com/apple/swift-evolution/blob/main/proposals/0382-expression-macros.md) расширить возможности Swift, позволяя писать более выразительный код, библиотеки и избегать boilerplate. 21 | 22 | Макрос объявляется с помощью `#` , как и функции могут принимать параметры и возвращать результат: 23 | 24 | ```swift 25 | // вначале реализация, она скрыта 26 | 27 | // присваивание макроса к его реализации 28 | @freestanding(expression) 29 | macro stringify(_: T) -> (T, String) = #externalMacro(module: "ExampleMacros", type: "StringifyMacro") 30 | 31 | // объявляем макрос 32 | #stringify(x + y) 33 | 34 | // a: Double = 1.0, b: String = "2" 35 | let (a, b): (Double, String) = #stringify(1 + 2) 36 | ``` 37 | 38 | Атрибут `@freestanding(expression)` применяется только к макросам. Означает, что макрос является выражением макроса (macro is an expression macro). 39 | Термин «freestanding» взят из [документа freestanding атрибута](https://github.com/apple/swift-evolution/blob/main/proposals/0397-freestanding-declaration-macros.md) и описывает использование макроса помеченным символом `#` . 40 | 41 | ## Attached Macros 42 | 43 | [Прикрепленные макросы](https://github.com/apple/swift-evolution/blob/main/proposals/0389-attached-macros.md) называются так, потому что «прикрепляются» к конкретному объявлению. Реализуются с помощью синтаксиса кастомных атрибутов: `@AddCompletionHandler`. 44 | У таких макрос могут быть роли, отвечающие за определенное поведение. 45 | 46 | Объявляются с помощью ключевого слова `macro`, имеют проверку типа данных, что позволяет настраивать кастомное поведение: 47 | 48 | ```swift 49 | // вначале реализация, далее присваивание и вызов. 50 | // детали реализации скрыты 51 | 52 | // присваиваем макрос к его реализации 53 | @attached(peer, names: overloaded) 54 | macro AddCompletionHandler(parameterName: String = "completionHandler") 55 | 56 | // используем прикрепленный макрос 57 | @AddCompletionHandler(parameterName: "onCompletion") 58 | func fetchAvatar(_ username: String) async -> Image? { ... } 59 | ``` 60 | 61 | 62 | > ‼️ [Примеры с макросами](https://github.com/DougGregor/swift-macro-examples) доступны в репозитории. 63 | 64 | ## Package Manager Support for Custom Macros 65 | 66 | 67 | [Данное введение добавляет поддержку макросов](https://github.com/apple/swift-evolution/blob/main/proposals/0394-swiftpm-expression-macros.md) в таргет `SwiftPM`: 68 | 69 | ```swift 70 | import PackageDescription 71 | import CompilerPluginSupport 72 | 73 | let package = Package( 74 | name: "MacroPackage", 75 | dependencies: [ 76 | .package(url: "https://github.com/apple/swift-syntax", from: "509.0.0"), 77 | ], 78 | targets: [ 79 | .macro(name: "MacroImpl", 80 | dependencies: [ 81 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 82 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax") 83 | ]), 84 | .target(name: "MacroDef", dependencies: ["MacroImpl"]), 85 | .executableTarget(name: "MacroClient", dependencies: ["MacroDef"]), 86 | .testTarget(name: "MacroTests", dependencies: ["MacroImpl"]), 87 | ] 88 | ) 89 | ``` 90 | 91 | ## Swift SDKs for Cross-Compilation 92 | 93 | Появилась новая CLI команда `swift sdk` и [кросс компиляция для нескольких таргетов](https://github.com/apple/swift-evolution/blob/main/proposals/0387-cross-compilation-destinations.md). 94 | 95 | ## `if` and `switch` expressions 96 | 97 | Появилась [новая форма записи для `if` и `switch` паттернов](https://github.com/apple/swift-evolution/blob/main/proposals/0380-if-switch-expressions.md): 98 | 99 | 1. В качестве возвращаемого значения в функции, проперти и замыкании 100 | 2. Объявлять переменные 101 | 3. Присваивать значение переменной 102 | 103 | ```swift 104 | let x: Double? = if p { nil } else { 2.0 } 105 | 106 | let y: Float = switch x.value { 107 | case 0..<0x80: 1 108 | case 0x80..<0x0800: 2.0 109 | case 0x0800..<0x1_0000: 3.0 110 | default: 4.5 111 | } 112 | 113 | func test(_: (Int?) -> T) {} 114 | 115 | test { x -> Int? in 116 | guard let x { return nil } 117 | return x 118 | } 119 | 120 | // equivalent to let x = foo.map(process) ?? someDefaultValue 121 | let x = if let foo { process(foo) } else { someDefaultValue } 122 | 123 | let foo: String = do { 124 | try bar() 125 | } catch { 126 | "Error \(error)" 127 | } 128 | ``` 129 | 130 | ## Custom Actor Executors 131 | 132 | [Появился базовый механизм для настройки actor executors](https://github.com/apple/swift-evolution/blob/main/proposals/0392-custom-actor-executors.md). 133 | 134 | Предоставляя экземпляр executors, акторы могут влиять на то, «где» они будут выполнять задачу, которую выполняют, сохраняя при этом изоляцию акторов, гарантированную моделью акторов. 135 | 136 | Разработчикам предоставляется возможность реализовать простые `serial executors`, которые можно использовать с акторами, гарантируя выполнение кода на акторе с кастомным `executor`, который запускается в подходящем потоке или контексте. 137 | 138 | ## Noncopyable structs and enums 139 | 140 | Все существующие типы данных в Swift являются **copyable**, что означает возможность создать несколько одинаковых, не уникальных и взаимозаменяемых типов данных. 141 | Копируемые структуры и перечисления не лучшие кандидаты для уникальных ресурсов. 142 | В свою очередь, классы являются уникальных ресурсом, поскольку объект имеет уникальную сущность после инициализации и только reference value могут быть копируемыми, но классы всегда требуют совместного владения ресурсом, что приводит к некоторым расходам, например: аллокации хипа и подсчета ссылок. Помимо расходов, совместных доступ усложняет использование и добавляет небезопасное использование. 143 | 144 | На данный момент в Swift нет механизмов для создания уникального владения (unique ownership). 145 | 146 | [Поэтому для структур и перечислений предлагается использовать объявлять тип данных как `noncopyable`, используя новый синтаксис ~Copyable.](https://github.com/apple/swift-evolution/blob/main/proposals/0390-noncopyable-structs-and-enums.md) 147 | `Noncopyable` типы данных имеют уникальное владение (unique ownership) и никогда не могут быть копируемыми (за исключением неявного копирования). 148 | Поскольку такие типы данных имеют уникальных идентификаторы, то можно использовать `deinit`, как в классах и акторах, которые автоматически выполняются в конце жизни инстанса. 149 | 150 | Например: 151 | ```swift 152 | struct FileDescriptor: ~Copyable { 153 | private var fd: Int32 154 | 155 | init(fd: Int32) { self.fd = fd } 156 | 157 | func write(buffer: Data) { 158 | buffer.withUnsafeBytes { 159 | write(fd, $0.baseAddress!, $0.count) 160 | } 161 | } 162 | 163 | deinit { 164 | close(fd) 165 | } 166 | } 167 | ``` 168 | 169 | Как и классы, инстансы данной структуры предоставляют доступ к управлению дескриптора файла, автоматически закрывая его (`deinit`). 170 | В отличии от класса, нет необходимости аллоцировать объект. Простая структура, содержащая ID дескриптора файла, должна хранится в стеке. 171 | 172 | ## Value and Type Parameter Packs для variadic generic programming 173 | 174 | https://github.com/apple/swift-evolution/blob/main/proposals/0393-parameter-packs.md#motivation 175 | -------------------------------------------------------------------------------- /src/SwiftUI.md: -------------------------------------------------------------------------------- 1 | ## Что такое декларативный синтаксис? 2 | 3 | Декларативный синтаксис — это современная парадигма программирования, помогающая разработчиками писать код процедурно. 4 | Используя такой подход, разработчик описывает кодом то, что он хочет написать, не обращая внимание на то, как это будет показано/реализовано. 5 | SwiftUI использует декларативный синтаксис, поэтому вы можете просто указать, что должен делать ваш интерфейс, например: 6 | вы пишите кодом, что хотите видеть `Image` вью внизу вашего экрана. 7 | 8 | ## Что такое View в SwiftUI? 9 | 10 | `View`(вью) — это основной строительный блок UI. Вью наследуется от протокола `View`, который является типом представляющим часть UI приложения и имеет модификаторы (modifiers), используемые для настройки. 11 | 12 | ## Перечислите все ленивые (lazy) контейнеры/вью? 13 | 14 | Называются ленивыми (`lazy`), потому что вью не создает все элементы сразу. Элементы выводятся на экран по мере необходимости. 15 | 16 | `List`, `LazyVStack`, `LazyHStack`, `LazyHGrid`, `LazyVGrid`, `Table`. 17 | 18 | ## Что такое модификаторы в SwiftUI? 19 | 20 | С помощью `Modifiers`(модификатора) можно добавить определенное изменение вью. SwiftUI предоставляет около 100-та собственных/встроенных модификаторов, таких как: `.padding()`, `.background()` и `.offset()`. 21 | Так же вы можете создать собственный модификатор, который делает что-либо с вашей вью. 22 | 23 | ## Что будет если применить модификатор к вью? 24 | 25 | Применив модификатор к вью, добавляется определенное поведение, которое вы ожидаете и возвращается новая вью. 26 | 27 | ## Какие категории View вы знаете? 28 | 29 | `Lazy` вью, как `List` подгружают элементы UI, когда они находятся или будут находиться внутри области прокрутки. 30 | Таким образом, не все данные из вью хранятся в текущей вью. Попробуем назвать элементы UI, которые генерируются во вью, `displayables`. Вью `SwiftUI` может иметь `0` или `> 0` `dispayables`. 31 | 32 | Когда дело доходит до сравнения, можем выделить 4-ре основных категории: 33 | 34 | 1. `Unary`: Вью с единственным displayable, таким как shapes, colors, controls и labels. 35 | 2. `Structural`: Вью которая принимает `0` или `> 0` и комбинирует их в некоторое подмножество: `ForEach`, `EmptyView` и вью которые строятся с `ViewBuilder`, такие как `TupleView` и `_ConditionalView`. 36 | 3. `Container`: Вью, которые принимают другие вью и управляют их расположением: `HStack`, `VStack`, `List`, `LazyVStack`. 37 | 4. `Modifiers`: Вью, которое добавляет и изменяет внешний вид и/или поведение. 38 | 39 | 41 | 42 | 43 | 44 | 45 | 46 | Каждая иерархия View содержит примитивные View, и каждое примитивное View будет представлять собой комбинацию типов View перечисленных выше: 47 | 48 | ```swift 49 | struct MyView: View { 50 | @State var showSecret = false 51 | 52 | var body: some View { 53 | VStack { 54 | Group { 55 | Button("Show secret") { showSecret.toggle() } 56 | if showSecret { 57 | Text("Secret") 58 | } else { 59 | Color.red 60 | } 61 | } 62 | .padding() 63 | } 64 | } 65 | } 66 | ``` 67 | 68 | SwiftUI необходимо отслеживать иерархию вью в `runtime`. Ниже схематично изображена `MyView()`, которая хранит ссылки и состояние вью. 69 | 70 | ![View graph](https://rensbr.eu/blog/swiftui-diffing/view_graph.svg) 71 | 72 | ## Что такое ViewModifier? 73 | 74 | `ViewModifier` - это протокол с помощью которого можно кастомизировать другой модификатор(modifier) или вью. 75 | Вы можете объединить несколько модификатор, которые в конце вернут вью. Чтобы изменить `UIView` в `UIKit`, вы используете проперти вью. В UIKit вы, вероятно, используете `YourView.backgroudColor = .red`, чтобы настроить UIView. Такой подход называет императивным. Для кастомизации в декларативном стиле, в SwiftUI мы просто описываем как вью должна выглядеть, используя `ViewModifier`. 76 | 77 | ## Что такое property wrapper? 78 | 79 | Проперти враппер (property wrapper) добавляет слой разделения между кодом, отвечающим за хранение свойств и кодом, отвечающим за объявление этого property wrapper. 80 | 81 | Представим, что нужно написать метод, проверяющий потокобезопасность или хранение данных в БД. 82 | Вместо того, чтобы писать 2 разных метода, мы напишем один property wrapper и повторно применим его к нескольким свойствам: 83 | 84 | ```swift 85 | @propertyWrapper 86 | struct TwelveOrLess { 87 | private var number = 0 88 | var wrappedValue: Int { 89 | get { return number } 90 | set { number = min(newValue, 12) } 91 | } 92 | } 93 | 94 | struct SmallRectangle { 95 | @TwelveOrLess var height: Int 96 | @TwelveOrLess var width: Int 97 | } 98 | 99 | var rectangle = SmallRectangle() 100 | print(rectangle.height) 101 | // Выведет "0" 102 | 103 | rectangle.height = 24 104 | print(rectangle.height) 105 | // Выведет "12" 106 | ``` 107 | 108 | ## Какие property wrapper вы знаете (вопрос устарел с iOS 17)? 109 | 110 | 111 | На текущий момент в SwiftUI имеется более 17-ти оберток, каждая из которых предоставляет разный функционал: 112 | 113 |

114 | @Published 115 | 116 | `@Published` применяется к проперти внутри `ObservableObject` и при изменении значения сообщает SwiftUI о перерисовки любой вью, которая использует эту проперти. 117 |
118 | 119 |
120 | @State 121 | 122 | `@State` property wrapper используется внутри `View` объекта и позволяет вашей вью реагировать на любые изменения. Данная обертка не принимает данные с других объектов. В качестве лучшей практики вы должны пометить свои проперти @State как `private`. Никакие внешние источники не должны изменять ваш @State проперти. В большинстве случаев используется для простых типов данных как `Int`, `String`, `Bool` и т.д. 123 |
124 | 125 |
126 | @Binding 127 | 128 | `@Binding` property wrapper используется для передачи значений в дочернюю(child) вью. Вью принимающая биндинг может читать проперти, реагировать на изменения от родительской вью и имеет доступ на запись проперти. 129 |
130 | 131 |
132 | @StateObject 133 | 134 | `@StateObject` схож со `@State`, но использует более сложные типы данных и применяется к `ObservableObject`. `ObservableObject` принимает reference type (class) и информирует SwiftUI когда в одном из `@Published` проперти произошли изменения. 135 | 136 | > ⚠️ Вы должны использовать `@StateObject` только один раз для каждого объекта. 137 |
138 | 139 |
140 | @ObservedObject 141 | 142 | `@ObservedObject` схож со `@StateObject`, за исключением того, что в нем не упоминается создание или хранение инстанса. `ObservedObject` используется для отслеживания изменений уже созданного объекта c использованием `@StateObject`. 143 | 144 | > ⚠️ До создания `StateObject` использовали `ObservedObject` для сохранения и хранения объектов, но это было не безопасно. Иногда `ObservedObject` мог случайно освободить объект, который он хранил. Поэтому была создана проперти враппер `StateObject`. 145 |
146 | 147 |
148 | @EnvironmentObject 149 | 150 | Временами нужно получить доступ к объекту из разных вьюх в приложении или во всех дочерних вьюхах. 151 | Достичь такого можно с помощью `@EnvironmentObject`. Проперти к которому применили `EnvironmentObject` должны наследоваться от `ObservableObject` протокола. 152 | Применяем модификатор `.environmentObject()` и объект доступен во всех вью, к которой применили модификатор. 153 | 154 | `@EnvironmentObject` похож на `@ObservedObject`. Основное различие в том, что `@EnvironmentObject` доступен в большем диапазоне, во множестве вложенных вью. 155 |
156 | 157 |
158 | @Environment 159 | 160 | Если вы знакомы с переменными окружения `env` в Linux, то вы сразу поймете о чем идет речь. 161 | `@Environment` считывает значения окружения ОС и перерисовывает вью если значение изменяется. Чтобы применить `@Environment` проперти к вью используйте `.environment` модификатор. 162 | 163 | Список всех значений [доступен в документации][environmentValues]. 164 |
165 | 166 |
167 | @AppStorage 168 | 169 | `@AppStorage` является оберткой над `UserDefaults`. Используйте обертку для хранения маленьких, простых значений. 170 | 171 | > ⚠️ Не следует хранить `CVV` код от кредитной карты. 172 |
173 | 174 | [environmentValues]: https://developer.apple.com/documentation/swiftui/environmentvalues 175 | 176 | 177 | ## Какие property wrapper вы знаете (iOS 17)? 178 | 179 | Начиная с iOS 17, разработчикам предлагается использовать [новый подход для отслеживания изменений](https://developer.apple.com/documentation/observation). 180 | 181 | 182 |
183 | @Bindable 184 | 185 | `@Bindable` 186 | [Документация](https://developer.apple.com/documentation/swiftui/bindable) 187 |
188 | 189 | 190 | 191 | ## Назовите property wrapper, которые объявляют reference семантику? 192 | 193 | SwiftUI предоставляет property wrappers, которые объявляют reference type в качестве источника истины (source of truth): `@ObservedObject`, `@StateObject` и `@EnvironmentObject`. 194 | Для использования этих врапперов, необходимо чтобы класс стал наблюдаемым (observable). 195 | 196 | ```mermaid 197 | flowchart TD 198 | A(Reference семантика) --> B(Источник истины/Source of Truth); 199 | B --> State("@StateObject"); 200 | B --> Environment("@EnvironmentObject"); 201 | B --> Observed("@ObservedObject") 202 | ``` 203 | 204 | ## Назовите property wrapper, которые объявляют value семантику? 205 | 206 | Существует 2 враппера, которые является `value` типом: `@State` и `@Binding`. 207 | Из них только `@State` является источником истины (source of truth): 208 | 209 | ```mermaid 210 | flowchart TD 211 | A(Value семантика) --> B(Источник истины/Source of Truth?); 212 | B --> Y(ДА); 213 | B --> N(НЕТ); 214 | Y --> S("@State") 215 | N --> Bind("@Binding") 216 | ``` 217 | 218 | ## Как сделать класс наблюдаемым (вопрос устарел, до iOS 17)? 219 | 220 | Для этого необходимо подписать класс на протокол `ObservableObject`. 221 | Если мы хотим, чтобы проперти класса реагировало на изменения и обновляло вью, то нужно добавить атрибут `@Published`: 222 | 223 | ```swift 224 | // старый подход 225 | class Counter: ObservableObject { 226 | @Published 227 | var increase: Int = 1 228 | } 229 | ``` 230 | 231 | > ℹ️ `@Published` — это проперти враппер, но в контексте применения к свойствам класса — атрибут. 232 | 233 | ## Как сделать класс наблюдаемым (начиная с iOS 17)? 234 | 235 | Начиная с iOS 17, разработчикам предлагается использовать новый макрос `@Observable`: 236 | 237 | ```swift 238 | // новый подход 239 | @Observable class Counter { 240 | var increase: Int = 1 241 | } 242 | ``` 243 | 244 | [Документация по макросу Observable](https://developer.apple.com/documentation/observation/observable-swift.macro) 245 | 246 | ## Состояние сцены и переход (Scene phases and transitions) 247 | 248 | Во время выполнения приложения, сцены могут переходить между тремя состояниями: 249 | 250 | - `активная` — сцена активна и пользователь может взаимодействовать с ней. 251 | - `неактивная` — сцена видна, но взаимодействие с ней отключено системой. Например, в режиме мультизадачности, вы можете увидеть список приложений, но эта панель не является активной. 252 | - `фон` — Приложение работает, но сцена не видна в UI. Сцена переходит в это состояние перед завершением работы приложения. 253 | 254 | ![Scene phases](https://docs-assets.developer.apple.com/published/1ab03c14da0d40b5cb696ef741df917a/SUI_067-010-040~dark.png) 255 | 256 | Узнать текущую сцену можно из значения среды `scenePhase`. 257 | 258 | ## Какие модификаторы изменяют View life cycle events? 259 | 260 | SwiftUI включает три модификатора, которые реагируют на события жизненного цикла вью: 261 | 262 | - `onAppear(perform:)` выполняет действие при каждом появлении вью, даже если вью появилась не первый раз. 263 | - `onDisappear(perform:)` выполняет действие при уходе с вью. 264 | - `task(priority:_:)` выполняет действие в асинхронной среде при появлении вью. Перестает выполнять при уходе с вью. 265 | 266 | > ⚠️ Ключевая разница между `onAppear` и `task` в том, что покидая вью `onAppear` не останавливает выполнение задачи, а `task` наоборот автоматически запускает и останавливает. 267 | -------------------------------------------------------------------------------- /src/ThreadingConcurrency.md: -------------------------------------------------------------------------------- 1 | ## Grand Central Dispatch 2 | 3 | `libdispatch` (aka `GCD`) – библиотека предоставляющая доступ к высокоуровневому API для конкурентного выполнения задач с управлением потоками за сценой. `GCD` абстрагируется от кода управления потоками и переносит его на системный уровень, предоставляя легкий API для определения задач и их выполнения в соответствующей очереди диспетчеризации. `GCD` работает на системном уровне, таким образом он может удовлетворить потребности всех запущенных приложений на девайсе, при этом управляя ресурсами эффективно. 4 | 5 | 6 | ## Что такое dispatch queue? 7 | `Queue` (очередь) представляет собой сущность, выполняющую задачи, поступающие на вход, на одном или множестве потоков. Очередь работает по принципу FIFO (first in, first out), таким образом первая задача на очереди будет первой направлена на выполнение на потоке. 8 | 9 | ## Что такое main thread и его использование? 10 | 11 | В iOS главный поток приложения - это очередь, состоящая из процессов. Главный поток должен использоваться для всех View, а элементы интерфейса не должны блокироваться выполняющимися задачами. Разработчику следует избегать функций, использующих главный поток для загрузки данных, изображений и т.д. 12 | 13 | ## Существует 2 типа queue. Перечислите и расскажите. 14 | 15 | ```mermaid 16 | flowchart TD 17 | A(2 queue) 18 | A --> Serial("serial"); 19 | A --> Concurrent("concurrent"); 20 | ``` 21 | 22 | 1. `serial` – выполняет задачи последовательно (поочередно). До тех пор, пока задача не будет выполнена, поток не приступит к выполнения следующей задачи в очереди. 23 | 2. `concurrent` – выполняет задачи конкурентно. Задачи, поступающие в concurrent очередь, могут выполняться одновременно на разных потоках. 24 | 25 | ## Объясните 3 GCD queues? 26 | 27 | ```mermaid 28 | flowchart TD 29 | A("3 типа queues") 30 | A --> Main("Main"); 31 | A --> Global("Global"); 32 | A --> Custom("Custom"); 33 | ``` 34 | 35 | `GCD` предоставляет 3 типа queues – 36 | 37 | 1. `Main queue`: Serial queue – работает на main thread. 38 | 2. `Global queue`: Concurrent queue – работает с разным приоритетом и доступен всей системе. 39 | 3. `Custom queue`: Serial/ Concurrent queue. 40 | 41 | ## Что такое Quality of Service? 42 | 43 | `Quality of Service` (QOS) - это приоритет выполнения задачи в `GCD`. Если `task` имеет более высокий `qos`, чем другие, оно будет обработано раньше, чем `task` с более низким приоритетом. 44 | 45 | `QOS` ранжируется от высшего к низшему: 46 | 47 | 1. `User Interactive`: Задача, которая запускается в основном потоке, например, анимация или операции рисования. 48 | 2. `User Initiated`: Задача, которую запускает пользователь и которая должна дать немедленные результаты. Эта задача должна быть завершена, чтобы пользователь мог продолжить работу. 49 | 3. `Utility`: Задача, которая может занять некоторое время и не требует немедленного завершения. Аналогично индикаторам выполнения и импорту данных. 50 | 4. `Background`: Данная задача не видна пользователю. Резервное копирование, синхронизация, индексирование и т.д. 51 | 52 | 53 | ## Объясните data race? 54 | 55 | `Data Race` - это ситуация, когда две инструкции из двух разных потоков пытаются получить доступ к одному и тому же участку памяти, при это один из этих доступов на запись и между ними нет никакой синхронизации. 56 | 57 | ## Объясните race condition? 58 | 59 | Это семантическая ошибка, возникающая во времени или порядке событий, которая приводит к неверному поведению программы. Race Condition может быть вызвана `Data Race`, но может быть вызвана и другими причинами. Одним из решений может быть `NSLock()` 60 | 61 | ## Объясните deadlock? 62 | 63 | `Deadlock` (тупик) возникает, когда две, а иногда и больше задач ожидают завершения другой задачи, но ни одна из них так и не завершается. Первое действие не может быть закончено, потому что оно ожидает окончания второго. А второе не может закончено, потому что оно ожидает окончания первого. 64 | 65 | ## Что такое priority inversion? 66 | 67 | `Priority inversion` (инверсия приоритетов) - это критическое состояние в потоках, когда поток с низким приоритетом блокирует выполнение потока с высоким приоритетом и делает назначенные приоритеты бессмысленными для потока. Такая ситуация возникает, когда поток с более низким приоритетом захватил какой-то общий ресурс и использует его, тем самым поток с высоким приоритетом не может использовать этот ресурс. 68 | 69 | ## Объясните NSOperation? 70 | 71 | `NSOperation` построен поверх `libdispatch` для упрощения работы с несколькими потоками. Используя `NSOperation` можно добавить зависимость для различных операций, повторно использовать, отменить или приостановить их. 72 | 73 | ## Объясните ThreadPool? 74 | 75 | `ThreadPool` — пул потоков, которые повторно используют фиксированное число потоков для выполнения конкретной задачи. 76 | 77 | ## Что такое Semaphore? 78 | 79 | `Semaphore` предоставляет нам возможность контролировать доступ нескольких поток к общему ресурсу. Семафор состоит из очереди потоков и счетчика значений. Счетчик значений используется семафором, чтобы решить, должен ли поток получить доступ к общему ресурсу или нет. Счетчик значений изменяется при вызове методов `signal()` или `wait()`: 80 | 81 | ```swift 82 | class DispatchSemaphore : DispatchObject { 83 | func signal() -> Int { } // Signals (increments) a semaphore. 84 | func wait() { } // Waits for, or decrements, a semaphore. 85 | } 86 | ``` 87 | 88 | # Structured Concurrency 89 | 90 | ## Что такое Task? 91 | 92 | `Task` - юнит асинхронной работы, в котором выполняется `async` функция: 93 | 94 | ```swift 95 | @frozen struct Task where Success : Sendable, Failure : Error 96 | ``` 97 | 98 | 99 | `Task` позволяют создавать конкурентное окружение из не-конкурентных методов с помощью вызова `async/await`. 100 | Задача выполняется немедленно после объявления и не требует явного запуска. 101 | Задачи имеют приоритет, их можно отменять, и они могут находиться в трёх состояниях - приостановленные, выполняющиеся и завершенные. Могут быть структурированными и неструктурированными. 102 | 103 | 104 | ## Расскажите о Task priority 105 | 106 | За приоритеты в `Task` отвечает структура `TaskPriority`, которая содержит следующие приоритеты: 107 | 108 | ```mermaid 109 | flowchart TD 110 | A("Task Priority") 111 | A --> high("high"); 112 | A --> medium("medium"); 113 | A --> low("low"); 114 | A --> userInitiated("userInitiated"); 115 | A --> background("background"); 116 | A --> utility("utility"); 117 | ``` 118 | 119 | 1. `high` 120 | 2. `medium` 121 | 3. `low` 122 | 4. `userInitiated` 123 | 5. `background` 124 | 6. `utility` 125 | 126 | Дочерние таски автоматически наследуют приоритет родительской. 127 | `Detached` таски, созданные методом `detach(priority:operation:)`, не наследуют приоритет, поскольку они не присоединены к текущей задаче. 128 | 129 | ## Расскажите о TaskGroup 130 | 131 | `TaskGroup` — группа, содержащая динамически созданные дочерние таски: 132 | 133 | ```swift 134 | @frozen struct TaskGroup where ChildTaskResult : Sendable 135 | ``` 136 | 137 | Для создания группы, вызовите метод `withTaskGroup(of:returning:body:)`. 138 | Не используйте группу вне области Task. В большинстве случаев, система типов `Swift`не позволит выйти из области видимости task group, поскольку добавление дочерней задачи в task group является операцией мутации, которые не могут быть выполнены из конкурентного контекста. 139 | 140 | ## Что такое actor? 141 | 142 | actor — reference type, который защищает доступ к изменяемому состоянию и объявляется с помощью ключевого слова `actor`. Тип данных, который можно безопасно использовать в конкурентной среде. Содержит следующие характеристики: 143 | 144 | 1. Имеют собственное изолированное состояние. 145 | 2. Может содержать логику для изменения собственного состояния. 146 | 3. Может общаться с другими участниками только асинхронно (через их адреса). 147 | 4. Может создавать других дочерних акторов (в данном случае нас это не особо волнует). 148 | 149 | ## Расскажите о ключевом слове await 150 | 151 | `await` — это специальная пометка, которая говорит о том, что в этом месте может быть `suspension point`. 152 | 153 | `Suspension points`(точки приостановки) – динамическая концепция всей программы, которая гласит: существуют места, которые могут фактически приостанавливать выполнение задачи и оставить поток. 154 | 155 | `Potential suspension points` - are the static, conservative, function-local view of suspension points: they're the points in the function where a suspension could potentially occur and therefore the function ought to be prepared to abandon the thread. Any dynamic suspension point is always "inside" a potential suspension point from the perspective of every async function on that task: all of the outer functions must be awaiting an asynchronous call, and the innermost function must be awaiting at the actual dynamic suspension point. From all of those functions' local perspectives, the exact reason for the suspension (which may be N levels of call deep) is much less important than the fact that a suspension is possible at that point in their execution. 156 | 157 | 195 | -------------------------------------------------------------------------------- /src/assemblyVision.md: -------------------------------------------------------------------------------- 1 | ### Атрибут @_assemblyVision 2 | 3 | [Данный атрибут](https://github.com/apple/swift/blob/main/docs/ReferenceGuides/UnderscoredAttributes.md#_assemblyvision) показывает заметки компилятора для функции/метода, указывая где на исходном уровне после оптимизации находятся различные runtime вызовы и факторы, влияющие на производительность. Так же, затраты и выгоды при `inline`, где происходят `ARC` операции, каким образом генерируются generics и какие вызовы девиртуализируются. 4 | 5 | Для использования применим атрибут к методу и соберем проект в релизной сборке: 6 | 7 | ```swift 8 | @_assemblyVision 9 | func loadModel(model: SDModel, computeUnit: ComputeUnits, reduceMemory: Bool) async throws { 10 | let fm = FileManager. default 11 | 12 | // A Pure call. Always profitable to inline "default argument 0 of Foundation.URL.path (percentEncoded:)" 13 | guard fm.fileExists(atPath: model.url.path()) else { ... } 14 | ... 15 | } 16 | ``` 17 | 18 | ![Assembly Vision](https://global.discourse-cdn.com/swift/original/3X/b/5/b5d913e55449f99da9c658b8c27a54a8ef8a3fb0.jpeg) 19 | 20 | > Убедитесь, что вы отредактировали вашу схему (scheme) и в `Build Configuration` выбрали `Release`. 21 | -------------------------------------------------------------------------------- /src/proposals/Actors.md: -------------------------------------------------------------------------------- 1 | ## Введение 2 | 3 | `Concurrency` призвана обеспечить безопасную модель программирования, которая статически обнаруживает гонки данных (data races) и другие классические ошибки. 4 | Данное введение знакомит со способом объявления concurrent tasks и предоставляет `data-race` безопасность для функций и замыканий. 5 | Такая модель подходит для ряда шаблонов проектирования, включая `parallel maps` и concurrent callback patterns, но модель ограниченна работой с состоянием, захваченным в замыканиях. 6 | 7 | Существуют классы предоставляющие механизм для объявления неизменяемого (mutable) состояния, доступные по всей программе. 8 | Однако, возникают трудности при правильном использовании классов в concurrent программах, поскольку для предотвращения data races нужна ручная синхронизация. 9 | 10 | Мы хотим предоставить возможность использовать изменяемое состояние предоставляя статическое обнаружение data races и других классических concurrency ошибок. 11 | Модель акторов идеально подходит для задачи, описанной выше. 12 | Каждый актор защищает свои собственные данные посредством изоляции данных, гарантируя, что только один поток будет иметь доступ к этим данным в определенный момент времени, даже если многие клиенты одновременно делают запросы к актору. 13 | Акторы обеспечивают те же свойства безопасности гонки и памяти, что и structured concurrency, но при этом предоставляют привычные возможности абстракции и повторного использования, которыми обладают другие типы данных. 14 | 15 | ### Actors 16 | Actor — reference type, который защищает доступ к своему изменяемому состоянию и вводит новое ключевое слово: `actor`. 17 | Актор имеет инициализатор, методы, свойства и сабскрипты. Может быть расширен, подписываться на протоколы и поддерживает generic. 18 | Основное различие — актор защищает своё состояние от data races. Такую защиту обеспечивает компилятор, на этапе статической проверки, с помощью набора ограничений на использование акторов и их членов экземпляра, которые в совокупности называются изоляцией акторов. 19 | 20 | ### Actor isolation 21 | 22 | Изоляция акторов - это то, как акторы защищают свое изменяемое состояние. 23 | Для акторов основным механизмом такой защиты является разрешение доступа к хранимым свойствам экземпляра только через self. 24 | 25 | Ссылка на изолированное объявление актора из вне этого актора называются **cross-actor reference**. 26 | Такие ссылки возможны в двух случаях: 27 | 28 | 1. cross-actor ссылка на неизменяемое состояние разрешена из любого места в том же модуле, где объявлен актор, поскольку после инициализации это состояние никогда не может быть изменено (ни внутри актора, ни вне его), поэтому data races отсутствуют по определению. 29 | Ссылка на `other.accountNumber` разрешена на основе этого правила, потому что `accountNumber` объявлен с помощью `let` и имеет `value-semantic` тип данных `Int`. 30 | 31 | 2. Вторым способом для `cross-actor` ссылки — выполняется с помощью вызова асинхронной функции. 32 | Такие вызовы асинхронной функций превращаются в «сообщения», запрашивающие у актора выполнение задачи, когда это можно сделать безопасно. 33 | Данные сообщения хранятся в «почтовом ящике» (`mailbox`) актора и вызов асинхронной функции может быть приостановлен до тех пор, пока актор не сможет обработать соответствующее сообщение в своем почтовом ящике. 34 | > ⚠️ Актор обрабатывает сообщения в своем почтовом ящике по одному за раз, поэтому у актора никогда не будет двух задач, выполняющих изолированный от актора код. 35 | Это гарантирует отсутствие `data races` на изолированном изменяемом состоянии, потому `concurrency` отсутствует в любом коде, который может получить доступ к изолированному состоянию актора. 36 | 37 | Например, если бы мы хотели внести депозит на банковский счет, мы могли бы сделать вызов метода `deposit(amount:)` на другом акторе, и данный вызов стал бы «сообщением», помещенным в «почтовый ящик» актора и вызов приостановился. 38 | При обработки сообщений, будет вызов функции в области изоляции актора при условии, что другой код не выполняется в области этого же актора. 39 | 40 | > ⚠️ Синхронный метод актора можно вызвать через `self.`, но `cross-actor` вызов для этого метода требует асинхронного вызова, т.е. использовать `await`. 41 | 42 | ### Cross-actor references and `Sendable` 43 | 44 | `SE-0302` знакомит вас с протоколом `Sendable`. 45 | Типы данных, наследующихся от `Sendable`, безопасны для общего выполнения в `concurrently` коде. 46 | Существуют типы, которые работают вместе с `Sendable`: value-semantic типы — `Int`, `String`, value-semantic коллекции — `[String]` или `[Int: String]`, неизменяемые классы, классы которые выполняют свою собственную синхронизацию внутри (например, concurrent hash table). 47 | 48 | Актор защищает свое изменяемое состояние, поэтому инстансы могут свободно использоваться в конкурентной среде и сам по себе актор поддерживает внутреннюю синхронизацию. 49 | > ⚠️ Каждый актор неявно соответствует протоколу `Sendable` 50 | 51 | Все `cross-actor` ссылки работают с value типами. 52 | Наш `BankAccount` содержит список владельцев, где каждый владелец является классом `Person`: 53 | 54 | ```swift 55 | class Person { 56 | var name: String 57 | let birthDate: Date 58 | } 59 | 60 | actor BankAccount { 61 | // ... 62 | var owners: [Person] 63 | 64 | func primaryOwner() -> Person? { return owners.first } 65 | } 66 | ``` 67 | Функцию `primaryOwner()` можно вызвать из другого актора, поэтому инстанс `Person` можно изменить откуда угодно: 68 | 69 | ```swift 70 | if let primary = await account.primaryOwner() { 71 | primary.name = "The Honorable " + primary.name // problem: concurrent mutation of actor-isolated state 72 | } 73 | ``` 74 | 75 | Каждый не изменяемый доступ проблематичен, поскольку имя может быть изменено внутри актора в тоже время, когда исходный вызов пытается получить доступ. 76 | Для предотвращения одновременного доступа к изолированному актору, необходимо чтобы все типы соответствовали `Sendable`. 77 | Таким образом можно гарантировать, что никакие `cross-actor` ссылки не выйдут за изолированное состояние. 78 | Вызов `account.primaryOwner()` выдаст ошибку: 79 | 80 | `error: cannot call function returning non-Sendable type 'Person?' across actors` 81 | 82 | Функцию `primaryOwner()` можно использовать в изолированной области видимости актора: 83 | 84 | ```swift 85 | extension BankAccount { 86 | func primaryOwnerName() -> String? { 87 | return primaryOwner()?.name 88 | } 89 | } 90 | ``` 91 | 92 | Функция `primaryOwnerName()` безопасна для асинхронного вызова, поскольку `String` и `Optional` соответствуют `Sendable`. 93 | 94 | 95 | -------------------------------------------------------------------------------- /src/proposals/AsyncAwait.md: -------------------------------------------------------------------------------- 1 | ## Введение 2 | 3 | В современной разработке на языке Swift мы часто прибегаем к использованию асинхронной работы, используя замыкания и completion handlers, но с этими API трудно работать. 4 | Особенно заметно, когда в коде содержится много асинхронных операций, требуется обработать ошибки и тем самым усложняется control flow между вызовами. 5 | 6 | Данная конструкция вводит модель корутин (`coroutine`) в Swift. 7 | В функции можно использовать ключевое слово `async`, позволяя вам проектировать более комплексную/сложную логику, включая асинхронные операции, используя обычный control flow механизм. 8 | За преобразование асинхронный функций в набор замыканий(appropriate set of closures) и машинное представление отвечает компилятор. 9 | 10 | Данный файл описывает семантику и проблемы, но не `concurrency`. 11 | Concurrency рассматривается в введении `structured concurrency`, which associates asynchronous functions with concurrently-executing tasks and provides APIs for creating, querying, and cancelling tasks. 12 | 13 | ## Мотивация: Completion handlers не удобны 14 | 15 | При асинхронном программировании с помощью `explicit callbacks` (также completion handlers) появляется несколько проблем, описанных ниже. 16 | Асинхронные функции служат решением этих проблем. С `async` функциями можно писать код в обычном стиле. 17 | 18 | ### Проблема #1: Pyramid of doom (Пирамида судьбы) 19 | 20 | Последовательность асинхронных выражений часто требует глубоко вложенных замыканий: 21 | 22 | ```swift 23 | // Код уходит в глубину, вправо 24 | func processImageData1(completionBlock: (_ result: Image) -> Void) { 25 | loadWebResource("dataprofile.txt") { dataResource in 26 | loadWebResource("imagedata.dat") { imageResource in 27 | decodeImage(dataResource, imageResource) { imageTmp in 28 | dewarpAndCleanupImage(imageTmp) { imageResult in 29 | completionBlock(imageResult) 30 | } 31 | } 32 | } 33 | } 34 | } 35 | 36 | processImageData1 { image in 37 | display(image) 38 | } 39 | ``` 40 | 41 | Такая пирамида судьбы значительно усложняет написание, чтение и отслеживание выполняемого кода. Дополнительно, используется стопка из замыканий. 42 | 43 | ### Проблема #2: Обработка ошибок 44 | 45 | Используя callbacks становится значительно трудней обрабатывать ошибки. В Swift 2 появилась модель обработки ошибок для синхронного кода, но интерфейсы на основе callbacks не получают от нее никаких преимуществ: 46 | 47 | ```swift 48 | // (2a) Using a `guard` statement for each callback: 49 | func processImageData2a(completionBlock: (_ result: Image?, _ error: Error?) -> Void) { 50 | loadWebResource("dataprofile.txt") { dataResource, error in 51 | guard let dataResource = dataResource else { 52 | completionBlock(nil, error) 53 | return 54 | } 55 | loadWebResource("imagedata.dat") { imageResource, error in 56 | guard let imageResource = imageResource else { 57 | completionBlock(nil, error) 58 | return 59 | } 60 | decodeImage(dataResource, imageResource) { imageTmp, error in 61 | guard let imageTmp = imageTmp else { 62 | completionBlock(nil, error) 63 | return 64 | } 65 | dewarpAndCleanupImage(imageTmp) { imageResult, error in 66 | guard let imageResult = imageResult else { 67 | completionBlock(nil, error) 68 | return 69 | } 70 | completionBlock(imageResult) 71 | } 72 | } 73 | } 74 | } 75 | } 76 | 77 | processImageData2a { image, error in 78 | guard let image = image else { 79 | display("No image today", error) 80 | return 81 | } 82 | display(image) 83 | } 84 | ``` 85 | 86 | ### Проблема #3: Выполнение условий сложно и чревато ошибками 87 | 88 | Проходить по условиям асинхронной функции - это большая проблема. 89 | Предположим, что нам нужно при появлении «смахнуть» изображение, но иногда нужно сделать асинхронный вызов для декодирования изображения, прежде чем мы сможем выполнить свитчинг/условие. 90 | Возможно, лучшим подходом к структурированию этой функции является запись кода `swizzling` в вспомогательное замыкание, которое условно перехватывается в обработчике завершения: 91 | 92 | 93 | ```swift 94 | func processImageData3(recipient: Person, completionBlock: (_ result: Image) -> Void) { 95 | let swizzle: (_ contents: Image) -> Void = { 96 | // ... continuation closure that calls completionBlock eventually 97 | } 98 | if recipient.hasProfilePicture { 99 | swizzle(recipient.profilePicture) 100 | } else { 101 | decodeImage { image in 102 | swizzle(image) 103 | } 104 | } 105 | } 106 | ``` 107 | 108 | ### Проблема #4: Легко наделать ошибок 109 | 110 | Можно выйти из функции раньше забыв вызвать `completion handler` 111 | Если мы забыли это сделать, то становиться сложнее дебажить данную функцию: 112 | 113 | ```swift 114 | func processImageData4a(completionBlock: (_ result: Image?, _ error: Error?) -> Void) { 115 | loadWebResource("dataprofile.txt") { dataResource, error in 116 | guard let dataResource = dataResource else { 117 | return // <- забыли вызвать блок 118 | } 119 | loadWebResource("imagedata.dat") { imageResource, error in 120 | guard let imageResource = imageResource else { 121 | return // <- забыли вызвать блок 122 | } 123 | ... 124 | } 125 | } 126 | } 127 | ``` 128 | 129 | К счастью, вызов оператора `guard` в некоторой степени защищает от возвращаемого значения, но это не всегда актуально. 130 | 131 | ### Проблема #5: Большинство completion handlers не удобны, поэтому множество APIs являются синхронными 132 | 133 | Трудно оценить количество, но некоторые разработчики считают, чтобы использование асинхронных API совместно с completion handlers значительно затрудняет написание кода, поэтому используют синхронный подход. 134 | Это приводит к проблемам с производительностью, отзывчивостью и плавности UI. 135 | 136 | ## Решение: async/await 137 | 138 | Асинхронные функции, известные как `async/await`, позволяют писать код в том же стиле, как и обычный синхронный код. 139 | Более того, это сразу решает описанные проблемы выше, позволяя разработчикам использовать те же конструкции языка, доступные для синхронного кода. 140 | 141 | Использование async/await также естественным образом сохраняет семантическую структуру кода, предоставляя информацию, необходимую как минимум для трех улучшений языка: 142 | 143 | 1. Более лучшая производительность 144 | 2. Более современные инструменты для отладки, профайлинга и исследования кода 145 | 3. Фундамент для concurrency фич, таких как приоритет (`task priority`) и отмена задач `task`. 146 | 147 | Пример из предыдущего раздела демонстрирует, как async/await радикально упрощает асинхронный код: 148 | 149 | ```swift 150 | func loadWebResource(_ path: String) async throws -> Resource 151 | func decodeImage(_ r1: Resource, _ r2: Resource) async throws -> Image 152 | func dewarpAndCleanupImage(_ i : Image) async throws -> Image 153 | 154 | func processImageData() async throws -> Image { 155 | let dataResource = try await loadWebResource("dataprofile.txt") 156 | let imageResource = try await loadWebResource("imagedata.dat") 157 | let imageTmp = try await decodeImage(dataResource, imageResource) 158 | let imageResult = try await dewarpAndCleanupImage(imageTmp) 159 | return imageResult 160 | } 161 | ``` 162 | 163 | Многие описания `async/await` рассматривают его через общий механизм реализации: пропуск компилятора, который делит функцию на несколько компонентов. 164 | Это важно на низком уровне абстракции, чтобы понять, как работает механизм. 165 | Думайте об асинхронной функции как об обычной функции, которая обладает особой способностью оставить/отказаться от своего потока. Асинхронные функции обычно не используют эту способность напрямую, вместо этого они делают вызовы, и иногда эти вызовы требуют, чтобы они отказались от своего потока и ждали, пока что-то произойдет. 166 | Когда «это что-то произойдет», функция возобновит выполнение. 167 | 168 | Поскольку асинхронные функции должны уметь оставлять свой поток, а синхронные функции не умеют этого делать, то синхронная функция не может вызвать асинхронную функцию: асинхронная функция сможет отдать только ту часть потока, которую она занимала, а если она попытается это сделать, то вызывающая ее синхронная функция воспримет это как возврат и попытается продолжить работу, только без возвращаемого значения. 169 | Единственным способом заставить это работать в общем случае было бы блокирование всего потока до возобновления и завершения асинхронной функции, что полностью уничтожило бы смысл асинхронных функций, а также привело бы к неприятным системным последствиям. 170 | 171 | В свою очередь, асинхронная функция может вызывать как синхронные, так и асинхронные функции. 172 | Вызывая синхронную функцию, она, конечно же, не может отказаться от своего потока. 173 | 174 | > ⚠️ Асинхронные функции никогда самопроизвольно не оставляют свой поток! Оставить поток можно тогда, когда достигается точки приостановки `suspention point`. 175 | 176 | Точка приостановки может возникнуть непосредственно внутри функции или в другой асинхронной функции, которую вызывает функция, но в любом случае функция и все вызывающие ее асинхронные функции одновременно оставляют поток. 177 | На практике асинхронные функции компилируются специальным образом, чтобы не зависеть от потока во время асинхронного вызова. 178 | 179 | 180 | ## Suspension points 181 | 182 | `Suspension point` (точка/момент приостановки) — это момент в выполнении `async` функции, в котором оставляется текущий поток. 183 | Основной формой момента приостановки является вызов асинхронной функции, связанной с другим контекстом выполнения. 184 | Важно, чтобы момент приостановки был связан только с явными операциями. 185 | Вызов `async` функции с ключевым словом `await` называется **потенциальным моментом приостановки**, поскольку статически неизвестно, приостановятся ли она на самом деле. 186 | 187 | Зависит от 188 | 1. кода, не видимого в месте вызова. Например, вызывающая сторона может зависеть от асинхронного ввода-вывода. 189 | 2. динамических условий. Например, нужно ли ждать завершения асинхронного ввода-вывода. 190 | 191 | Требование указывать ключевое слово `await` такая же необходимость, как и требование указывать `try` в функциях, которые могут вернуть ошибку, обозначенных `throws`. 192 | Если `async` функция работает в заданном контексте, который защищен `serial queue` (последовательной очередью), достигая момента остановки, это означает другой код может чередоваться на той же последовательной очереди. 193 | Классическим примером служит проектирование банка: если депозит зачисляется на один счет, но операция приостанавливается перед обработкой обнала, это создает окно, в котором эти средства могут быть дважды потрачены. 194 | На примере UI: `suspention point` - это момент, в которых создается UI, а затем приостанавливают создание, могут столкнуться с мерцающим, частично построенным пользовательским интерфейсом. 195 | 196 | Несмотря на то, что потенциальный `suspention point`(момент приостановки) может появляться только в явно обозначенной асинхронной функции, сложные/длительные вычисления все равно могут блокировать потоки. 197 | Это происходит при вызове синхронной функции, которая занимается тяжелыми операциями/вычислениями. 198 | В любом случае поток не может чередовать код во время выполнения этих вычислений, что обычно является правильным выбором с точки зрения корректности, но может стать проблемой масштабируемости. 199 | Асинхронные функции, которым выполняют интенсивные вычисления, должны выполняться в отдельном контексте. Когда это невозможно, существуют библиотечные средства для искусственного приостановления и обеспечения чередования других операций. 200 | 201 | Нужно избегать вызова функции, которые могут заблокировать поток `async` функции. -------------------------------------------------------------------------------- /src/proposals/Sendable.md: -------------------------------------------------------------------------------- 1 | # `Sendable` and `@Sendable` closures 2 | 3 | ## Введение 4 | 5 | Основной целью `Swift Concurrency` является «обеспечить механизм для изолированного состояния в конкурентных программах для устранения data races». 6 | Такой механизм будет большим прогрессом для многих языков программирования — большинство из ЯП предоставляют некую абстракцию для конкурентного программирования, что влечет за собой классические ошибки: race conditions, deadlocks и другие. 7 | 8 | Данное введение описывает подход для решения одной из проблем в области конкурентного программирования: «Как проверить типы данных переданные между structured concurrency constructs и сообщениями акторов?». 9 | Это теория, которая предоставляет механизм для типов данных, который обеспечивает хорошую и безопасную работу вместе. 10 | 11 | Ниже описана реализация маркерного протокола `Sendable` и атрибута `@Sendable`, который может применяться к функциям. 12 | 13 | ## Мотивация 14 | 15 | Каждый инстанс актора и задача(`task`) в программе представляют собой «остров однопоточности», что делает их естественной точкой синхронизации, хранящей сумку с изменяемым состоянием. 16 | Острова выполняют вычисления параллельно с другими задачами, но мы хотим, чтобы подавляющее большинство кода в такой системе было свободным от синхронизации - основываясь на логической независимости актора и используя его «почтовый ящик» в качестве точки синхронизации для своих данных. 17 | 18 | Вопрос: «Когда и как разрешать передавать данные между concurrency доменами?». 19 | Такие передачи/перемещения происходят в вызовах метода актора или `task`. 20 | `Swift Concurrency` направлены на создание безопасной и мощной модели программирования. Мы хотим добиться трех вещей: 21 | 22 | 1. Получать статическую ошибку компилятора при передаче данных через `concurrency` домены, которые могут иметь незащищенное изменяемое состояние. 23 | 2. Позволить реализовывать сложные библиотеки, которые можно безопасно использовать. 24 | 3. Плавная и постепенная миграция на новую модель `Swift Concurrency`. 25 | 26 | И прежде, чем перейти к решению, рассмотрим общие случаи и проблемы. Это поможет рассуждать о дизайне проектирования. 27 | 28 | ### Swift + Value Semantics 29 | 30 | Первым типом поддерживаемых данных, будут простые значения, например структура `Int`. 31 | `Int` можно легко передавать через `concurrency` домены, поскольку структура не содержит указателей (`pointers`). 32 | 33 | Swift уделяет большое внимание типам данных с `value` семантикой, которые можно безопасно передавать через границы `concurrency`. 34 | Например, коллекцию `Dictionary` можно передавать напрямую через домены. В контексте `Copy on Write` оптимизации, это означает что такие коллекции могут передаваться **без создания копии**, поэтому `concurrency` модель очень эффективна на практике. 35 | 36 | Но есть ограничения. Коллекции из стандартной библиотеки (`std`) могут передаваться не безопасно, если они содержат ссылки на классы, замыкания захватывающие изменяемое состояние и другие не `value` типы данных. 37 | Поэтому нам нужен способ, с помощью которого мы сможем различать, какие типы данных можно передать безопасно, а какие нет. 38 | 39 | ### Value Semantic Composition 40 | 41 | Structs, enums и tuples являются основным способом композиции в Swift. 42 | Эти типы данных безопасны для передачи через concurrency домены — при условии, что содержащиеся в них данные сами по себе безопасны для передачи. 43 | 44 | ### Функции высшего порядка 45 | 46 | В свифте и других языках с поддержкой ФП, принято использовать функции высшего порядка (передача функции в другую функцию). 47 | Функции высшего порядка являются ссылочным типом, но большинство из можно можно безопасно передавать через concurrency домены. 48 | Рассмотрим на примере с актором: 49 | 50 | ```swift 51 | actor MyContactList { 52 | func filteredElements(_ fn: (ContactElement) -> Bool) async -> [ContactElement] { … } 53 | } 54 | ``` 55 | 56 | Далее можно использовать так: 57 | 58 | ```swift 59 | // Замыкание без захвата ok! 60 | list = await contactList.filteredElements { $0.firstName != "Max" } 61 | 62 | // Захватывать 'searchName' строку по значению ok, потому что строки 63 | // передаются по concurrency домену. 64 | list = await contactList.filteredElements { 65 | [searchName] in $0.firstName == searchName 66 | } 67 | ``` 68 | 69 | Мы считаем, что важно обеспечить возможность передачи функций через домены, но так же мы не должны разрешать захват локального состояния по ссылке в этих функциях, как и захват небезопасных выражений по значению. 70 | В обоих случаях это создаст проблемы с безопасностью памяти. 71 | 72 | ### Неизменяемые классы 73 | 74 | Одним из распространенных и эффективных шаблонов проектирования в concurrent программировании является создание неизменяемых структур данных - совершенно безопасно передавать ссылку на класс через домены, если состояние внутри никогда не изменяется. 75 | Этот шаблон проектирования чрезвычайно эффективен (не требуется синхронизация за пределами `ARC`), может быть использован для построения продвинутых структур данных и широко изучается сообществом чисто функциональных языков. 76 | 77 | ### Internally Synchronized Reference Types (Локальная синхронизация ссылочного типа) 78 | 79 | Стандартным шаблоном проектирования в concurrency системах является создание классом «потокобезопасного» API: он защищает свое состояние с помощью явной синхронизации (мьютексы, atomics и т.д.). 80 | Поскольку публичный API класса безопасен для использования из нескольких доменов, ссылка на класс может быть безопасно передана напрямую. 81 | 82 | Примером могут служить ссылки на сами экземпляры акторов: безопасно передавать указатель между доменами, поскольку изменяемое состояние внутри актора неявно защищено его «почтовым ящиком». 83 | 84 | ### Deep Copying Classes 85 | 86 | Одним из безопасных способов передачи ссылочного типа является создание «глубокой копии данных», гарантируя что исходный и конечный домен имеют собственную копию изменяемого состояния. 87 | Может быть затратным для большого кол-ва данных, но широко используется в некоторых фреймворках Objective-C. 88 | 89 | ### Заключение введения 90 | 91 | Это всего лишь выборка паттернов, но как мы видим, существует широкий спектр различных паттернов `concurrent` проектирования, которые широко используются. 92 | Дизайн Swift вокруг типов значений и использования структур - это очень мощная и полезная отправная точка, но нам нужно уметь рассуждать и о сложных случаях - как для сообществ, которые хотят иметь возможность создавать высокопроизводительные API для определенной области, так и потому что нам нужно работать с устаревшим кодом, который не будет переписан за одну ночь. 93 | 94 | 95 | ## Решение и детали дизайна 96 | 97 | Высокоуровневый дизайн решения знакомит вас с маркерным протоколом `Sendable`, адаптацией стандартной библиотеки с помощью `Sendable` и новый атрибутом для функций `@Sendable`. 98 | 99 | ## Marker Protocols (протокол маркер) 100 | 101 | Наконец-то вы познакомитесь с концептом протокола «маркера», который сообщает, что протокол имеет некоторые семантические свойства, которые не влияют на runtime. 102 | Протокол маркер имеет следующие ограничения: 103 | 104 | 1. Не имеет требований (`requirements`). 105 | 2. Не может подписываться на «не маркерные» протоколы. 106 | 3. Не может назван в качестве типа, проверки `is` или каста (e.g., x `as?` Sendable is an error). 107 | 4. Не может использоваться вместе с дженериками в качестве соответствия не маркерному протоколу. 108 | 109 | Мы считаем, что это полезная функция, но на данный момент она должна быть внутренней фичей компилятора. 110 | Поэтому ниже мы используем эту концепцию с синтаксисом атрибута `@_marker`. 111 | 112 | ## `Sendable` Protocol 113 | 114 | Сосредоточимся на протоколе маркере объявленным в стандартной библиотеке (`std`), который имеет специальные правила проверки соответствия: 115 | 116 | ```swift 117 | @_marker 118 | protocol Sendable {} 119 | ``` 120 | 121 | Хорошей идеей для типов данных является соответствие протоколу Sendable, если они разработаны так, что все их публичные API безопасны для использования в разных доменах. 122 | 123 | Компилятор отклоняет любые попытки передачи данных через домены, когда аргумент или результат отправки сообщения актором не подписаны на `Sendable.` 124 | 125 | ```swift 126 | actor SomeActor { 127 | // async функция объявлена внутри актора, поэтому 128 | // ошибок нет. 129 | func doThing(string: NSMutableString) async {...} 130 | } 131 | 132 | // ... но нельзя вызвать из другого места, не защищеным 133 | // почтовым ящиком актора: 134 | func f(a: SomeActor, myString: NSMutableString) async { 135 | // ошибка: 'NSMutableString' не может быть передан между акторами; 136 | // не подписан на 'Sendable' 137 | await a.doThing(string: myString) 138 | } 139 | ``` 140 | 141 | Протокол `Sendable` «моделирует» типы, которые можно безопасно передавать между concurrency доменами, с помощью копирования значения. 142 | Сюда входят типы с `value` значения, ссылки на неизменяемые ссылочные типы, внутренние синхронизированные ссылочные типы, замыкания `@Sendable` и другие __будущие__ расширения системы типов для уникального владения (`ownership`) и т.д. 143 | 144 | > ⚠️ Неправильное соответствие этому протоколу может внести ошибки в программу (так же, как неправильная реализация `Hashable` может сломать инварианты), поэтому компилятор делает соответствие. 145 | 146 | #### Tuple + `Sendable` 147 | 148 | В свифте захардкодили `tuple` на определенные протоколы и тип данных должен реализовывать расширение с подписью на `Senddable`. 149 | 150 | #### Metatype conformance to Sendable 151 | 152 | Метатипы, такие как `Int.Type`, `Int.self`, всегда подписаны на `Sendable`, поскольку они неизменяемые (`immutable`). 153 | 154 | 155 | #### `Sendable` conformance checking for `structs` and `enums` 156 | 157 | Типы данных, подписанные на `Sendable` очень распространены в Swift и их агрегаты безопасны для передачи между доменами. 158 | Компилятор позволяет подписывать структур и классов на `Sendable`, которые являются композициями других типов `Sendable`: 159 | 160 | ```swift 161 | struct MyPerson : Sendable { var name: String, age: Int } 162 | struct MyNSPerson { var name: NSMutableString, age: Int } 163 | 164 | actor SomeActor { 165 | // Structs and tuples are ok to send and receive! 166 | public func doThing(x: MyPerson, y: (Int, Float)) async {..} 167 | 168 | // error if called across actor boundaries: MyNSPerson doesn't conform to Sendable! 169 | public func doThing(x: MyNSPerson) async {..} 170 | } 171 | ``` 172 | 173 | Компилятор сообщит об ошибке, если в структуре или `enum` проперти не соответствуют `Sendable` или сам тип данных, в том числе дженерик: 174 | 175 | ```swift 176 | // ошибка: структура MyNSPerson не может быть подписана на Sendable из-за проперти NSMutableString. 177 | // заметка: добавьте '@unchecked' атрибут если вы знаете что здесь происходит. 178 | struct MyNSPerson : Sendable { 179 | var name: NSMutableString 180 | var age: Int 181 | } 182 | 183 | // ошибка: структура MyPair не может быть подписана на Sendable из-за дженерика 'T', который сам не подписан на Sendable 184 | // заметка: смотрите ниже как подписать джененик 185 | struct MyPair : Sendable { 186 | var a, b: T 187 | } 188 | 189 | // используйте расширение, чтобы задать протокол явно 190 | struct MyCorrectPair { 191 | var a, b: T 192 | } 193 | 194 | extension MyCorrectPair: Sendable where T: Sendable { } 195 | ``` 196 | 197 | Выше компилятор сообщил о том, что можно добавить атрибут `@unchecked Sendable`, чтобы перезаписать поведение и убрать ошибку. 198 | Данный атрибут означает, что данные можно безопасно передавать между доменами, но разработчику требуется убедиться в безопасности такой передачи. 199 | 200 | Структура или `enum` можно подписать на `Sendable` только в том же файле, где был объявлен тип данных: 201 | Это гарантия, что хранящиеся проперти структуры и `associated values` перечисления будут доступны для проверки на соответствие `Sendable`: 202 | 203 | ```swift 204 | // MySneakyNSPerson.swift 205 | struct MySneakyNSPerson { 206 | private var name: NSMutableString 207 | public var age: Int 208 | } 209 | 210 | // NotAMySneakyNSPerson.swift 211 | // данное расширение объвлено в другом файле или модуле... 212 | // ошибка: нельзя подписаться на Sendable вне исходного 213 | // объявления MySneakyNSPerson 214 | extension MySneakyNSPerson: Sendable { } 215 | ``` 216 | 217 | Без такой проверки, другой исходный файл или модуль, который не может получить доступ к приватной проперти `name`, думал что структура подписана на `Sendable`. 218 | Однако, мы может добавить не безопасный атрибут `@unckecked` для отключения проверки: 219 | 220 | ```swift 221 | // в другом файле или модуле... 222 | // okay: unchecked позволяет подписать структуру на Sendable из другого файла/модуля 223 | extension MySneakyNSPerson: @unchecked Sendable { } 224 | ``` 225 | 226 | #### Неявная подпись struct/enum на `Sendable` 227 | 228 | Многие структуры и перечисления подписаны на `Sendable` и необходимость всегда писать `: Sendable` для каждого такого типа может утомить. 229 | Для непубличных структур и перечислений, которые также не являются `@usableFromInline` и для `frozen` публичных структур и перечислений соответствие Sendable неявно обеспечивается при проверке(описанной в предыдущем разделе): 230 | 231 | ```swift 232 | struct MyPerson2 { // Структура неявно подписана на Sendable! 233 | var name: String, age: Int 234 | } 235 | 236 | class NotConcurrent { } // Не подписан на Sendable 237 | 238 | struct MyPerson3 { // Не подписан на Sendable, потому что nc не является Sendable типом 239 | var nc: NotConcurrent 240 | } 241 | ``` 242 | 243 | Public non-frozen структуры и `enums` не подписаны неявно на `Sendable`, потому что это вызовет проблемы при проектировании API. 244 | Добавив не `Sendable` в такой тип данных можно сломать логику. 245 | 246 | > ⚠️ `Sendable` не влияет на размер бинарного файла, в отличии от других протоколов. 247 | 248 | #### `Sendable` conformance checking for classes 249 | 250 | Каждый объявленный класс можно подписать на `Sendable` с атрибутом `@unchecked`, позволяя передавать из между акторами без семантической проверки. 251 | Это подходит для классов, которые используют контроль доступа и внутреннюю синхронизацию для обеспечения безопасности памяти - обычно эти механизмы не могут быть проверены компилятором. 252 | 253 | Класс можно подписать на `Sendable` и проверка безопасности памяти будет работать только в ситуации, когда класс имеет ключевое слово `final` и содержит только не изменяемые проперти соответствующие `Sendable`: 254 | 255 | ```swift 256 | final class MyClass : Sendable { 257 | let state: String 258 | } 259 | ``` 260 | 261 | Такие классы не могут наследоваться от классов, отличных от `NSObject` (для совместимости с `Objective-C`). 262 | Классы с `Sendable` имеют то же ограничение, что и structs и enums, которые требуют, чтобы подпись на `Sendable` происходила в том же исходном файле. 263 | 264 | Такое поведение позволяет безопасно создавать и передавать неизменяемые состояния между акторам. На данный момент нельзя неявно подписываться на `Sendable`. 265 | Такое ограничение сделано специально. 266 | 267 | #### В акторах 268 | 269 | Акторы содержат свою внутреннюю синхронизацию, поэтому неявно подписаны на `Sendable`. [Ознакомьтесь с введением в акторы](Actors.md). 270 | 271 | #### Key path literals 272 | 273 | `Key paths` сам по себе подписан на протокол `Sendable`. Чтобы убедиться в безопасности, литералы `key path` должны захватывать значение соответствующее протоколу `Sendable`. 274 | Это затрагивает использование сабскриптов в `key paths`: 275 | 276 | ```swift 277 | class SomeClass: Hashable { 278 | var value: Int 279 | } 280 | 281 | class SomeContainer { 282 | var dict: [SomeClass : String] 283 | } 284 | 285 | let sc = SomeClass(...) 286 | 287 | // ошибка: при захвате 'sc' в key path 'SomeClass' не подписан 288 | // на 'Sendable' 289 | let keyPath = \SomeContainer.dict[sc] 290 | ``` 291 | 292 | ### Атрибут `@Sendable` для функций 293 | 294 | Функции являются важным ссылочным типом, которые в настоящий момент не могут подписываться на протоколы. 295 | В языке Свифт функции представлены в нескольких формах: глобальное объявление, вложенные функции, `get/set/subscript` и замыкания. 296 | Важно разрешить безопасную передачу функций между конкурентными доменами, чтобы использовать методы высшего порядка, ФП и т.д. 297 | 298 | Данный раздел знакомит вас с новым атрибутом для функций: `@Sendable`. Функции с `@Sendable` типом можно безопасно передавать между доменами(таким образом функции неявно соответствуют `Sendable`). 299 | Для обеспечения безопасности памяти функций с `@Sendable` типом, компилятор выполняет проверки: 300 | 301 | 1. К функции можно применить атрибут `@Sendable`. Любое захваченное значение подписывается на протокол `Sendable`. 302 | 2. Замыкания содержащие функциональный тип `@Sendable` могут делать захват по `value`. Захват неизменяемых значений с помощью `let` неявно являются `value`. Любой иной захват осуществляется с помощью `capture list`: 303 | 304 | ```swift 305 | let prefix: String = ... 306 | var suffix: String = ... 307 | strings.parallelMap { [suffix] in prefix + $0 + suffix } 308 | ``` 309 | 310 | Типы захваченных значений должны соответствовать `Sendable`. 311 | 312 | 3. В настоящее время `get/set` не поддерживают `@Sendable`. Если будет спрос, то появится новое предложение по введению в такой функционал. 313 | 314 | #### Inference of `@Sendable` for Closure Expressions 315 | 316 | Атрибут `@Sendable` переданный в качестве параметра функции работает аналогично сбегающему замыканию `@escaping`. 317 | Замыкание считается выражением с `@Sendable` если: 318 | 319 | 1. используется в контексте `@Sendable` функции(`parallelMap` или `Task.runDetached`) 320 | 2. `@Sendable` в замыкании `in` 321 | 322 | Различие между `@Sendable` и `@escaping` состоит в том, что по умолчанию замыкание не является `@Sendable`, а является как раз `@escaping`: 323 | 324 | ```swift 325 | // по умолчанию это @escaping, а не @Sendable 326 | let fn = { (x: Int, y: Int) -> Int in x+y } 327 | ``` 328 | 329 | Вложенные функции являются важным аспектом потому что как и замыкания могут захватывать значение. 330 | Атрибут `@Sendable` объявляется во вложенной функции для проверки на `concurrency`: 331 | 332 | ```swift 333 | func globalFunction(arr: [Int]) { 334 | var state = 42 335 | 336 | // Ошибка, 'state' захвачен как неизменяемое значение, потому что это @Sendable замыкание. 337 | arr.parallelForEach { state += $0 } 338 | 339 | // Ok, функция захватила 'state' по ссылке. 340 | func mutateLocalState1(value: Int) { 341 | state += value 342 | } 343 | 344 | // Ошибка: не @Sendable функция не может быть преобразована в @Sendable функцию. 345 | arr.parallelForEach(mutateLocalState1) 346 | 347 | @Sendable 348 | func mutateLocalState2(value: Int) { 349 | // Ошибка: 'state' захвачен как let потому что присутствует атрибут @Sendable 350 | state += value 351 | } 352 | 353 | // Ok, mutateLocalState2 is @Sendable. 354 | arr.parallelForEach(mutateLocalState2) 355 | } 356 | ``` 357 | 358 | Такое поведение обеспечивает чистую композицию для `structured concurrency` и акторов. 359 | 360 | ### Thrown errors 361 | 362 | Функция или замыкание, которые возвращают ошибки, фактические могут вернуть значение `any` типа соответствующего протоколу `Error`. 363 | Если функция вызвана из другого `concurrency` домена, то ошибка пробрасывается между ними (доменами). 364 | 365 | ```swift 366 | class MutableStorage { 367 | var counter: Int 368 | } 369 | struct ProblematicError: Error { 370 | var storage: MutableStorage 371 | } 372 | 373 | actor MyActor { 374 | var storage: MutableStorage 375 | func doSomethingRisky() throws -> String { 376 | throw ProblematicError(storage: storage) 377 | } 378 | } 379 | ``` 380 | 381 | Вызов `myActor.doSomethingRisky()` из другого `concurrency` домена вызовет ошибку, захватив часть изменяемого состояния `myActor`, а затем предоставит ошибку в другой домен, нарушая изоляцию акторов. 382 | Поскольку в сигнатуре `doSomethingRisky()` нет информации о типе возвращаемой ошибки, а функция может быть вызвана из другого файла, то нет возможности проверить, что ошибка соответствует протоколу `Sendable`. 383 | 384 | Для решения этой задачи, мы подпишем протокол `Error` на протокол `Sendable`: 385 | 386 | ```swift 387 | protocol Error: Sendable { … } 388 | ``` 389 | 390 | Теперь `ProblematicError` покажет нам ошибку, потому что содержит проперти не `Sendable` типа — `MutableStorage`. 391 | 392 | В общем, подписывая существующий протокол на `Sendalbe`, мы нарушили совместимость исходного и двоичного кода. 393 | Однако маркерные протоколы не влияют на `ABI` и не предъявляют требований, поэтому бинарная совместимость сохраняется. 394 | 395 | Такая совместимость требует больше внимательности. `ProblematicError` хорошо совмести с текущей версией Свифта, но начиная с Swift 6 вы получите ошибку. 396 | 397 | ### Адаптация `Sendable` к типам из стандартной библиотеки 398 | 399 | Важно, чтобы типы данных из `std` могли безопасно передаваться между `concurrency` доменами. 400 | Большая часть типов являются `value` семантикой, поэтому должны без проблем соответствовать `Sendable`: 401 | 402 | ```swift 403 | extension Int: Sendable {} 404 | extension String: Sendable {} 405 | ``` 406 | -------------------------------------------------------------------------------- /src/proposals/StructuredConcurrency.md: -------------------------------------------------------------------------------- 1 | ## Введение 2 | 3 | [`async`/`await`](AsyncAwait.md) — это механизм для написания понятного и производительного асинхронного кода. 4 | Асинхронные функции (обозначенные `async`) могут оставить поток, на котором они выполняются, в любой заданной точке приостановки (обозначенной `await`), что необходимо для построения `highly-concurrent` систем. 5 | 6 | Proposal по `async`/`await` не знакомит с понятием `concurrency` *как таковым*: игнорируя suspension points в `async` функции, выполнение будет таким же, как в синхронной. 7 | Данный proposal вводит поддержку для [structured concurrency](https://en.wikipedia.org/wiki/Structured_concurrency) в Swift, включая concurrent выполнение асинхронного кода с более удобной, предсказуемой моделью и эффективной реализацией. 8 | 9 | ## Мотивация 10 | 11 | На простом примере, приготовим ужин, асинхронно: 12 | 13 | ```swift 14 | func chopVegetables() async throws -> [Vegetable] { ... } 15 | func marinateMeat() async -> Meat { ... } 16 | func preheatOven(temperature: Double) async throws -> Oven { ... } 17 | 18 | // ... 19 | 20 | func makeDinner() async throws -> Meal { 21 | let veggies = try await chopVegetables() 22 | let meat = await marinateMeat() 23 | let oven = try await preheatOven(temperature: 350) 24 | 25 | let dish = Dish(ingredients: [veggies, meat]) 26 | return try await oven.cook(dish, duration: .hours(3)) 27 | } 28 | ``` 29 | 30 | Каждый шаг в приготовлении ужина — асинхронная операция, поэтому существует несколько suspention point. 31 | В ожидании нарезки овощей, `makeDinner` не будет блокировать поток. Функция приостановится, пока овощи не готовы, а затем возобновится. 32 | Возможно, множество готовящихся ужинов на разных стадиях и большинство из них будут приостановлены до завершения текущего шага. 33 | 34 | Несмотря на `async` вызовы, приготовление остается последовательным. Сначала ждем нарезки овощей, затем мариновки мяса с овощами, далее разогреть духовку и только сейчас приступить к приготовлению. 35 | К тому времени, посетители будут голодны и недовольны такой скоростью приготовления. 36 | 37 | Для ускорения процесса будет делать некоторые действия одновременно. Овощи можно нарезать во время мариновки мяса и в это же время разогреем духовку. 38 | Иногда между задачами существует зависимость: как только овощи и мясо будут готовы, мы можем соединить их в блюдо, но не можем поставить это блюдо в духовку, пока духовка не нагрелась. 39 | Все эти задачи являются одной большой задачей по приготовлению ужина. При выполнении всех задач, ужин будет подан. 40 | 41 | Данный proposal представляет инструменты для разделения задач на сопрограммы (более мелкие задачи), выполняя задачи конкурентно, что позволит задачам ждать завершения друг друга и эффективно управлять общим ходом выполнения задачи. 42 | 43 | ## Structured concurrency 44 | 45 | Любая concurrency система должна предоставлять базовый набор инструментов. Должен быть способ создания нового потока, который будет `concurrency` выполняться с уже существующими. 46 | Также, должен быть способ заставить поток ждать до сигнала другого потока о продолжении работы. Благодаря таким мощным инструментам, можно написать сложные системы. 47 | В свою очередь, такие инструменты примитивны: дают мало возможностей и ограниченную поддержку. 48 | 49 | Представим функцию, которая выполняет тяжелую работу на `CPU`. Оптимизировав функцию, мы разделили работу на два ядра. 50 | Теперь функция создает новый поток, выполняет **1/2** работы в каждом, а затем заставляет исходный поток ждать завершения нового потока. 51 | Существует взаимосвязь между работой, выполняемой этими двумя потоками, но система об этом не знает. Это значительно усложняет решение системных проблем. 52 | 53 | Существует операция с высоким приоритетом, которая требует от функции ускориться и завершить работу. 54 | Операция может знать, что нужно повысить приоритет первого потока, но на деле нужно повысить на обоих потоках. 55 | В лучшем случае она не будет повышать приоритет второго потока, пока первый поток не начнет его ждать. 56 | Данную задачу легко решить в текущей, узкой ситуации, позволив функции создать второй поток, но это не является универсальным решением. 57 | 58 | `Structured concurrency` решает эту проблему, предлагая программистам организовать использование конкурентности в виде высокоуровневых и их дочерних задач. 59 | Такие `task` являются основным элементом конкурентности, чем более низкоуровневые потоки. 60 | `Structured concurrency` позволяет информации естественным образом перемещаться вверх/вниз по иерархии задач, что в противном случае потребовало бы хорошей поддержки каждого уровня абстракции и при каждом переходе между потоками. 61 | В свою очередь, позволяет относительно легко решать многие проблемы высокого уровня. 62 | 63 | Например: 64 | 65 | 1. Часто необходимо ограничить затрачиваемое время на выполнение задачи. Некоторые API поддерживают установку timeout, но требуется поразмыслить, как правильно передать timeout через каждый уровень абстракции. 66 | Разработчики хотят передавать тайм-ауты в виде относительной длительности (например, 20 мс), но правильное представление для библиотек - это абсолютный срок (например, now() + 20 мс). 67 | При `structured concurrency`, дедлайн можно установить для task и передать через произвольные уровни API, включая дочерние таски. 68 | 69 | 2. Возможность отменить активную задачу. Асинхронные интерфейсы, который поддерживают отмену задачи, обычно реализуют это с помощью возврата токена, который вызывает некую функцию `cancel()`. 70 | Часто такой отмены нет, потому что это усложняет проектирование дизайна API и добавляет инженерные проблемы для программы. 71 | Такая возможность есть в `structured concurrency`, в том числе и для дочерних задач. API поддерживает обработку на такое событие. 72 | 73 | 3. Часто графические интерфейсы полагаются на приоритет задачи для своевременного обновления и реагирование на события. 74 | В `structured concurrency` дочерние задачи наследуют приоритет от родительской. 75 | > ⚠️ Более того, в ситуации, когда задача с более высоким приоритетом ожидает завершения задачи с более низким приоритетом, приоритет может быть повышен. 76 | 77 | 4. Многие системы хотят хранить свою собственную контекстную информацию для операции без необходимости передавать ее через все уровни абстракции, например, сервер, который записывает информацию о текущем соединении. 78 | `Structured concurrency`позволяет передавать эту информацию вниз через асинхронные операции как своего рода "локальное хранилище задач", которое может быть подхвачено дочерними задачами. 79 | 80 | 5. Системы, полагающиеся на очереди, подвержены переполнению. 81 | Ситуация, когда очередь принимает больше, чем может. Обычным решение служит «выдавливание»(обратное давление). 82 | Акторным системам часто удается избежать этого, поскольку на уровне планировщика трудно отказаться от добавления задачи в очередь актора, поскольку это может окончательно дестабилизировать систему за счет утечки ресурсов или иного препятствования завершению операций. 83 | `Structured concurrency` предлагает ограниченное, кооперативное решение, позволяя системе взаимодействовать c иерархией задач, позволяя родительским задачам остановить или замедлить создание предположительно новых задач. 84 | 85 | Данное введение не предлагает решение для всех проблем выше, но исследование показывают перспективы. 86 | 87 | ## Task (задачи) 88 | 89 | `Task` является основной единицей конкурентности в системе (concurrency in the system). Каждая `async` функция выполняется в `task`. 90 | Другими словами, `task` — асинхронная функция, тогда как `thread` — синхронная функция: 91 | 92 | - Все асинхронный функции работают как часть некоторой `task`. 93 | - `Task` выполняет одну функцию за 1 раз; в одной `task` нет concurrency. 94 | - Когда функция делает `async` вызов, вызванная функция выполняется как часть некоторой `task` (и ожидается возвращение). 95 | - Когда функция возвращается из `async` вызова, функция продолжает выполнять `task`. 96 | 97 | > ⚠️ Синхронные функции не обязательно выполняются как часть задачи. 98 | 99 | `Task` может быть в одном из трех состояний: 100 | 101 | 1. **running**. Запущенная задача в данный момент выполняется в потоке. 102 | Будет выполняться до тех пор, пока не вернется из своей начальной функции (и не станет завершенной), либо не достигнет момента/точки приостановки (и не станет приостановленной). 103 | В точке приостановки она может сразу стать планируемой, если для ее выполнения нужно просто сменить исполнителя. 104 | 105 | 2. **suspended**. У приостановленной задачи еще осталась работа, но в данный момент она не выполняется. 106 | Она может быть запланирована, то есть готова к выполнению и просто ждет, когда система даст команду потоку начать ее выполнение, 107 | или она может ожидать какого-то внешнего события, прежде чем она станет планируемой. 108 | 109 | 3. **completed**. Завершенная задача, которая никогда не перейдет в другое состояние. 110 | Система может ждать завершения `task` различными способами, но прежде всего с помощь `await`. 111 | 112 | Говоря о выполнении задач, то `async` функции выполняются сложнее, чем синхронные. 113 | `Async` функция выполняется как часть `task`. Если `task` выполняется, то она и ее функция тоже выполняются в потоке. 114 | 115 | Когда `async` функция вызывает другую `async` функцию, мы говорим, что вызываемая функция `suspended`, но это не означает, что приостановлена задача целиком. 116 | С точки зрения функции, она приостановлена, ожидая возврата вызова. С точки зрения задачи, она могла продолжать выполняться в вызывающей функции, или она могла быть приостановлена, чтобы, скажем, перейти в другой контекст выполнения. 117 | 118 | Задачи служат 3-ем высоко уровневым целям: 119 | 120 | 1. Содержат `scheduling` информацию — task priority 121 | 2. Являются обработчиком через который операции могут быть отменены, queried or manipulated. 122 | 3. Могут содержать локальные данные задачи, предоставляемые пользователем. 123 | 124 | ## Child tasks 125 | 126 | Асинхронная функция может создать дочернюю задачу. 127 | Дочерние задачи наследуют часть структуры своей родительской, включая ее приоритет, но могут выполняться одновременно с ней. 128 | 129 | ## Jobs 130 | 131 | Выполнение `task` можно рассматривать как последовательность периодов, в течение которых выполнялось задание, каждый из которых заканчивается в точке приостановки или завершается. Такие периоды называются `jobs`. 132 | `Jobs` - это основные юниты планируемой работы в системе. Они также являются примитивом, с помощью которого асинхронные функции взаимодействуют с основным синхронным миром. 133 | По большей части программистам не приходится работать непосредственно с `jobs`, если только они не реализуют кастомный `executor`. 134 | 135 | ## Executors 136 | 137 | `Executor` — сервис, который принимает заявку от `jobs` и организует поток для выполнения. 138 | Система предполагает, что `executors` надежны и никогда не откажутся от выполнения `job`. 139 | 140 | `Async` функция, которая выполняется, всегда знает на каком `executor` она запущена. 141 | Это позволяет функции избежать ненужной приостановки при вызове того же `executor`, а так же позволяет продолжить выполнение на исходном `executor`. 142 | 143 | `Executor` называется __exclusive__ если переданные ему `jobs` никогда не будут выполняться `concurrently`. 144 | По умолчанию Swift предоставляет реализацию `executor`, но `actor` и `global actor` можно предоставить кастомную реализацию. 145 | Как правило, большинству разработчикам не нужно взаимодействовать с `executor` напрямую, они используют их неявно. 146 | 147 | ## Task priorities 148 | 149 | `task` ассоциируется с установленным приоритетом. 150 | Приоритет задачи может информировать `executor`: как и когда планировать выполнение переданных `tasks`. An executor may utilize priority information to attempt to run higher priority tasks first, and then continuing to serve lower priority tasks. It may also use priority information to affect the platform thread priority. 151 | Точная семантика обработки приоритета, зависит от конкретной платформы и конкретной реализации `executor`. 152 | Дочерние задачи автоматически наследуют приоритет родительской задачи. Отделенные задачи не наследуют приоритет (или любую другую информацию), поскольку семантически не имеют родительской задачи. 153 | 154 | Приоритет задачи не обязательно должен совпадать с приоритетом `executor`, например: UI поток на платформах Apple является `executor` с высоким приоритетом. 155 | Любая задача переданная в UI thread будет выполняться с высоким приоритетом в течении всего времени выполнения. 156 | Это помогает гарантировать, что UI thread будет доступен для выполнения более приоритетной `task`, если она будет представлена позже. 157 | 158 | ## Priority Escalation 159 | 160 | В некоторых ситуациях приоритет задачи может быть повышен, чтобы избежать инверсии приоритетов: 161 | 162 | - Если `task` выполняется от actor и task с высоким приоритетом встала в очередь actor, то `task` может временно выполняться с тем же приоритетом, что и задача с высоким приоритетом. 163 | Не влияет на дочерние задачи. 164 | - Если `task` создана во время обработки `task` и `task` с высоким приоритетом ожидает завершения, то приоритет `task` будет повышен постоянно, чтобы соответствовать задаче с высоким приоритетом. 165 | Не влияет на дочерние задачи. 166 | 167 | --------------------------------------------------------------------------------