Протоколы и дженерики
Протокол-ориентированное программирование, associated type, ограничения дженериков, `some` против `any` и как диспетчеризуются требования протоколов.
13 вопросов
JuniorТеорияОчень частоЧто такое протокол, чем он отличается от базового класса и что такое протокол-ориентированное программирование?
Что такое протокол, чем он отличается от базового класса и что такое протокол-ориентированное программирование?
Протокол — это контракт требований, которые тип выполняет, без своего состояния. В отличие от базового класса он даёт много конформностей, годится struct и enum, не только class. Протокол-ориентированность строит поведение из мелких протоколов с дефолтными extension.
Типичные ошибки
- ✗Думают, что протокол хранит состояние, как базовый класс
- ✗Считают, что конформировать протоколу могут только классы
- ✗Полагают, что тип может соответствовать лишь одному протоколу
Уточняющие вопросы
- →Почему struct может принять протокол, но не наследоваться от class?
- →Что дают дефолтные extension протокол-ориентированному программированию?
JuniorТеорияЧастоЧто может и не может попасть в extension и почему он не может добавить хранимое свойство?
Что может и не может попасть в extension и почему он не может добавить хранимое свойство?
extension может добавить типу вычисляемые свойства, методы, инициализаторы, сабскрипты, вложенные типы и конформности протоколов. Он не может добавить хранимое свойство, потому что это изменило бы разметку памяти типа, зафиксированную в исходном объявлении.
Типичные ошибки
- ✗Считают, что extension может добавить хранимое свойство
- ✗Думают, что extension может переопределить существующий метод базового типа
- ✗Полагают, что extension работает лишь со своими типами, а не со стандартной библиотекой
Уточняющие вопросы
- →Почему добавление хранимого свойства требует менять объявление типа?
- →Как extension может добавить состояние экземпляра без хранимого свойства?
JuniorТеорияЧастоКаким протоколам стандартной библиотеки стоит соответствовать модельному типу и что даёт каждый?
Каким протоколам стандартной библиотеки стоит соответствовать модельному типу и что даёт каждый?
Codable открывает кодирование и декодирование JSON, Equatable включает ==, Hashable позволяет типу быть элементом Set или ключом словаря, Identifiable даёт SwiftUI стабильный id для списков, а CustomStringConvertible задаёт description типа в логах.
Типичные ошибки
- ✗Путают, какой протокол разрешает
Setи ключ словаря, — этоHashable - ✗Думают, что
Identifiableпро кодирование, а не про стабильныйid - ✗Считают, что все эти протоколы всегда синтезируются без явного запроса
Уточняющие вопросы
- →Какие из них компилятор синтезирует за вас и когда?
- →Почему SwiftUI
ListтребуетIdentifiable?
MiddleКодЧастоНапишите условную конформность, делающую Array Summable, и объясните, что она открывает.
Напишите условную конформность, делающую Array Summable, и объясните, что она открывает.
Условная конформность делает generic-тип конформным, только когда конформен его параметр — extension Array: Summable where Element: Summable. Она открывает API для [Int], но не для [UIView], ровно как [T] становится Equatable, когда таков его T.
Типичные ошибки
- ✗Думают, что конформность применяется к любому типу элемента безусловно
- ✗Переписывают логику элемента вместо делегирования его конформности
- ✗Считают, что
whereна функции — это то же, что условная конформность
Уточняющие вопросы
- →Почему
[UIView]не компилируется против этой конформности? - →Как стандартная библиотека делает
[T]Equatableэтим способом?
MiddleКодЧастоКогда Swift синтезирует Equatable/Hashable и когда ==/hash(into:) надо писать вручную?
Когда Swift синтезирует Equatable/Hashable и когда ==/hash(into:) надо писать вручную?
Swift синтезирует Equatable/Hashable, когда каждое хранимое свойство конформно и вы объявляете конформность в файле типа. Пишите их вручную, когда равенство должно игнорировать поля — сравнивайте только id в == и подавайте лишь id в hash(into:).
Типичные ошибки
- ✗Думают, что синтез работает, даже когда хранимое свойство не конформно
- ✗Подают в
==иhash(into:)разные поля, нарушая контракт хеша - ✗Считают, что оба метода всегда надо писать вручную
Уточняющие вопросы
- →Почему равные значения обязаны давать равные хеши?
- →Где надо объявить конформность, чтобы синтез сработал?
MiddleТеорияЧастоПротокол даёт дефолт метода в extension — что срабатывает через any P и что через дженерик some P?
Протокол даёт дефолт метода в extension — что срабатывает через any P и что через дженерик some P?
Объявленное требование и через any P, и через some P вызывает собственную реализацию типа — witness table выбирает её, а дефолт срабатывает, только если тип не добавил своей. Метод, живущий лишь в extension, статичен, и дефолт побеждает всегда.
Типичные ошибки
- ✗Считают, что дефолт из extension побеждает всегда, даже когда тип сам объявляет требование
- ✗Думают, что
some Pиany Pвыбирают разные реализации для одного объявленного требования - ✗Не отличают требование протокола от метода, существующего только в extension
Уточняющие вопросы
- →Почему метод, объявленный только в extension, диспетчеризуется по статическому типу?
- →Как добавление метода в требование протокола меняет то, какая реализация срабатывает?
MiddleТеорияЧастоЧто означают some P и any P и когда реально нужен existential any?
Что означают some P и any P и когда реально нужен existential any?
some P — opaque-тип: один фиксированный тип, известный компилятору, поэтому вызовы статичны и без боксинга. any P — existential-бокс с любым конформным типом, диспетчеризуемый в рантайме через witness table. Берите any, только если тип должен меняться.
Типичные ошибки
- ✗Считают
some Pиany Pвзаимозаменяемыми - ✗Думают, что
some Pстирает конкретный тип в рантайме, какany P - ✗Берут
any Pтам, где хватило бы одного фиксированного типа возврата
Уточняющие вопросы
- →Почему
some Pможно специализировать, аany P— нет? - →Когда цена боксинга у existential действительно важна?
SeniorКодЧастоНапишите дженерик-функцию mostFrequent с ограничением в духе where и поясните каждое ограничение.
Напишите дженерик-функцию mostFrequent с ограничением в духе where и поясните каждое ограничение.
func mostFrequent<T: Hashable>(_ xs: [T]) -> T? — ограничение Hashable позволяет T быть ключом словаря для подсчёта, а T? возвращается, потому что у пустого входа нет моды. Клауза where добавляет ограничения вроде T: Comparable, чтобы разрешать ничьи.
Типичные ошибки
- ✗Убирают ограничение
Hashable, всё ещё используяTкак ключ словаря - ✗Возвращают неопциональный
T, хотя у пустого входа нет моды - ✗Думают, что клауза
whereнезаконна на свободной функции
Уточняющие вопросы
- →Почему использование
Tкак ключа словаря требуетT: Hashable? - →Как
T: Comparableпомог бы разрешить ничью по частоте?
SeniorТеорияИногдаAssociated type против generic-параметра у протокола — что выбрать и чего каждый стоит вызывающему?
Associated type против generic-параметра у протокола — что выбрать и чего каждый стоит вызывающему?
Associated type фиксируется конформным типом — по одному на конформность, вызывающий его не выбирает. Generic-параметр выбирается вызывающим на каждом использовании. Берите associated type, когда тип неотъемлем для конформера, и generic-параметр, когда решает вызывающий.
Типичные ошибки
- ✗Думают, что associated type выбирает вызывающий
- ✗Считают generic-параметр и associated type всегда взаимозаменяемыми
- ✗Полагают, что associated type может меняться на каждом вызове, как generic-параметр
Уточняющие вопросы
- →Почему у протокола может быть лишь одно значение associated type на конформность?
- →Когда принуждение вызывающего выбирать тип вредит API?
SeniorПроизводительностьИногдаКак whole-module optimization, final и private позволяют компилятору девиртуализовать и специализировать вызовы протокола?
Как whole-module optimization, final и private позволяют компилятору девиртуализовать и специализировать вызовы протокола?
Whole-module optimization видит все точки вызова сразу, поэтому может доказать, что метод нигде не переопределён, и сделать вызов через vtable или witness прямым. final и private заявляют эту гарантию локально, а специализация дженериков встраивает конкретный тип.
Типичные ошибки
- ✗Думают, что whole-module optimization лишь ускоряет компиляцию
- ✗Считают, что
finalиprivateменяют поведение, а не включают девиртуализацию - ✗Полагают, что специализация дженериков происходит в рантайме
Уточняющие вопросы
- →Почему
private-метод даёт оптимизатору ту же гарантию, что иfinal? - →Что мешает девиртуализации через границу модуля?
SeniorТеорияИногдаКогда Swift использует прямую, vtable, witness-table диспетчеризацию или objc_msgSend рантайма Objective-C?
Когда Swift использует прямую, vtable, witness-table диспетчеризацию или objc_msgSend рантайма Objective-C?
Прямая (статическая) диспетчеризация встраивает вызов для final, private, static и методов struct. Нефинальные методы class идут через vtable; требования протокола через дженерики или existential — через witness table; @objc dynamic использует objc_msgSend.
Типичные ошибки
- ✗Считают, что каждый вызов Swift использует
objc_msgSend, как это делал Objective-C - ✗Путают vtable класса с witness table протокола
- ✗Забывают, что
final,privateиstaticвключают прямую диспетчеризацию
Уточняющие вопросы
- →Почему оптимизатор может превратить вызов через vtable в прямой?
- →Из-за чего член использует
objc_msgSendвместо vtable?
SeniorПроизводительностьИногдаЧего стоит [any Shape] в рантайме по сравнению с дженериком [T] where T: Shape?
Чего стоит [any Shape] в рантайме по сравнению с дженериком [T] where T: Shape?
[any Shape] боксирует каждый элемент в existential-контейнер и диспетчеризует вызовы через witness table, без специализации. [T] where T: Shape — один конкретный тип, который оптимизатор специализирует и встраивает, поэтому вызовы статичны, а элементы без боксинга.
Типичные ошибки
- ✗Считают, что
[any Shape]и дженерик-массив компилируются в один код - ✗Думают, что боксинг existential бесплатен для малых значений
- ✗Полагают, что оптимизатор может специализировать вызовы через existential
Уточняющие вопросы
- →Когда гибкость
[any Shape]оправдывает свою цену в рантайме? - →Что хранится inline в existential-контейнере до выгрузки в кучу?
SeniorТеорияРедкоПочему протокол с associated type исторически ломал any P и что изменили primary associated type?
Почему протокол с associated type исторически ломал any P и что изменили primary associated type?
Associated type не даёт протоколу единого конкретного типа, поэтому до Swift 5.7 его нельзя использовать как existential — только как ограничение дженерика. Swift 5.7 разрешил такие existential и добавил primary associated type, закрепив его — any Collection<Int>.
Типичные ошибки
- ✗Думают, что associated type — это generic-параметр с той же мощью, что и
any - ✗Считают, что primary associated type убирают ограничения целиком, а не закрепляют одно
- ✗Полагают, что протоколы с associated type всё ещё не могут быть existential после Swift 5.7
Уточняющие вопросы
- →Что гарантирует
any Collection<Int>, чего не даёт голыйany Collection? - →Почему компилятор когда-то отвергал такой протокол как existential?