Значимые и ссылочные типы
Чем значимая семантика struct отличается от ссылочной семантики class во время выполнения — копирование, мутация, идентичность, copy-on-write и где живёт каждый тип.
11 вопросов
JuniorТеорияОчень частоЧто сравнивают == и === и что тип должен реализовать для каждого?
Что сравнивают == и === и что тип должен реализовать для каждого?
== проверяет равенство значений и требует конформности Equatable. === (и !==) проверяет идентичность ссылок — указывают ли две ссылки на один экземпляр — и работает только на class, без конформности.
Типичные ошибки
- ✗Думают, что
===работает на значенияхstruct, а не только на ссылках class - ✗Считают, что
==доступен бесплатно без конформностиEquatable - ✗Путают равенство значений с идентичностью ссылок
Уточняющие вопросы
- →Как компилятор синтезирует
Equatableдляstruct? - →Почему
===нельзя применить к сравнению двух значенийstruct?
JuniorТеорияОчень частоЧто на самом деле различается во время выполнения между struct и class?
Что на самом деле различается во время выполнения между struct и class?
struct — значимый тип: присваивание копирует значение, наследования и идентичности нет. class — ссылочный тип: переменные разделяют один экземпляр в куче под ARC и получают наследование, динамическую диспетчеризацию и deinit.
Типичные ошибки
- ✗Считают, что
structразмещается в куче за указателем, какclass - ✗Думают, что присваивание
structразделяет экземпляр, а не копирует его - ✗Полагают, что оба типа поддерживают наследование и динамическую диспетчеризацию
Уточняющие вопросы
- →Почему мутация скопированной
structникогда не влияет на оригинал? - →Когда метод
classвсё же диспетчеризуется статически, а не динамически?
JuniorКодЧастоПредскажите вывод, когда та же программа использует class вместо struct
Предскажите вывод, когда та же программа использует class вместо struct
Печатается 10. С class var b = a копирует только ссылку, поэтому a и b указывают на один экземпляр; мутация b.x меняет этот объект, и a.x даёт 10. Ссылочные типы разделяют экземпляр, а не копируют значение.
Типичные ошибки
- ✗Ожидают от
classкопирования значения, как уstruct - ✗Думают, что
var b = aглубоко копирует объект, а не ссылку - ✗Полагают, что свойства class — общие статики между экземплярами
Уточняющие вопросы
- →Как получить независимую копию экземпляра
class? - →Почему теперь
===возвращаетtrueдляaиb?
JuniorТеорияЧастоПочему метод, меняющий хранимое свойство struct, должен быть mutating, и почему его нельзя вызвать на let?
Почему метод, меняющий хранимое свойство struct, должен быть mutating, и почему его нельзя вызвать на let?
Методы значимого типа получают неизменяемый self, поэтому смена хранимого свойства требует mutating, который делает self параметром inout. На let вызвать нельзя, потому что значение константы менять нельзя.
Типичные ошибки
- ✗Думают, что любой метод может свободно менять хранимые свойства
struct - ✗Считают, что запрет на
letсвязан с размещением в памяти, а не с неизменяемостью - ✗Полагают, что
classнуждается вmutatingтак же, какstruct
Уточняющие вопросы
- →Почему методу
classне нуженmutating, чтобы поменять свойство? - →Как
mutatingсвязан с передачейselfкакinout?
JuniorКодЧастоПредскажите вывод, когда мутируют скопированную struct-переменную
Предскажите вывод, когда мутируют скопированную struct-переменную
Печатается 0. Присваивание var b = a копирует значение, поэтому b — независимая копия; мутация b.x меняет только эту копию, а a остаётся прежним. struct копируется при присваивании, а не разделяется.
Типичные ошибки
- ✗Ожидают, что вторая переменная станет псевдонимом первой, а не копией
- ✗Думают, что присваивание
structразделяет хранилище, какclass - ✗Полагают, что копия настолько ленива, что поздние мутации протекут обратно
Уточняющие вопросы
- →Как изменился бы вывод, будь
Pointклассом (class)? - →В какой момент значение действительно копируется при
var b = a?
MiddleДебаггингЧастоstruct, захваченный в замыкании, показывает старое значение после изменения внешней переменной — объясните и исправьте
struct, захваченный в замыкании, показывает старое значение после изменения внешней переменной — объясните и исправьте
Список захвата [value] копирует значимое значение в момент создания замыкания — это снимок, поэтому поздние мутации внешней переменной не видны. Уберите список захвата, чтобы захватывать саму переменную, или перейдите на class, чтобы разделять состояние.
Типичные ошибки
- ✗Думают, что список захвата читает переменную вживую, а не снимает снимок
- ✗Винят многопоточность вместо семантики захвата копированием значения
- ✗Полагают, что
@escapingменяет то, что захватывает список захвата
Уточняющие вопросы
- →Чем захват без списка отличается от списка
[counter]? - →Когда снимок через список захвата — это именно нужное поведение?
SeniorДизайнЧастоВы проектируете слой моделей iOS-приложения. Большинство моделей — структуры ради значимой семантики и безопасного копирования, но для некоторых типов struct — неверный выбор. Опишите конкретно, когда вы намеренно возьмёте class вместо struct — рассмотрите общий изменяемый объект, который многие части приложения должны наблюдать как один; тип, у которого важна сама идентичность (два экземпляра с равными полями всё равно должны считаться разными); работу жизненного цикла, которую нужно выполнить в deinit; крупные данные, где copy-on-write ничего не даёт, потому что вы всегда мутируете; и совместимость с Objective-C или API фреймворка, требующим ссылочный тип. Для каждого случая объясните, что не может выразить struct и что реально даёт class.
Вы проектируете слой моделей iOS-приложения. Большинство моделей — структуры ради значимой семантики и безопасного копирования, но для некоторых типов struct — неверный выбор. Опишите конкретно, когда вы намеренно возьмёте class вместо struct — рассмотрите общий изменяемый объект, который многие части приложения должны наблюдать как один; тип, у которого важна сама идентичность (два экземпляра с равными полями всё равно должны считаться разными); работу жизненного цикла, которую нужно выполнить в deinit; крупные данные, где copy-on-write ничего не даёт, потому что вы всегда мутируете; и совместимость с Objective-C или API фреймворка, требующим ссылочный тип. Для каждого случая объясните, что не может выразить struct и что реально даёт class.
Берите class, когда многие владельцы разделяют один изменяемый экземпляр, когда важна идентичность (===), когда deinit выполняет очистку или когда Obj-C-API требует ссылочный тип. struct копируется, поэтому не смоделирует один общий объект с идентичностью.
Типичные ошибки
- ✗Решают только по размеру, а не по потребности в идентичности и разделении
- ✗Думают, что
structможет моделировать один общий наблюдаемый изменяемый экземпляр - ✗Игнорируют
deinitи совместимость с Objective-C как решающие факторы
Уточняющие вопросы
- →Почему copy-on-write не снимает нужду в
classпри разделяемой мутации? - →Как идентичность ссылок (
===) меняет проектирование равенства для типа?
JuniorПроизводительностьИногдаЧто меняет пометка class как final для компилятора и что это даёт во время выполнения?
Что меняет пометка class как final для компилятора и что это даёт во время выполнения?
final запрещает наследование, поэтому компилятор знает, что переопределений не будет, и может девиртуализировать — превращая динамическую диспетчеризацию в прямые, встраиваемые вызовы. Это убирает поиск по vtable и ускоряет вызовы, особенно между модулями.
Типичные ошибки
- ✗Думают, что
finalменяет размещение с кучи на стек - ✗Считают, что диспетчеризация остаётся динамической даже у
final-класса - ✗Полагают, что
finalтолько про контроль доступа, а не про производительность
Уточняющие вопросы
- →Когда оптимизация всего модуля может сама вывести
final? - →Почему
finalпомогает сильнее всего между модулями?
JuniorТеорияИногдаПочему struct с хранимым свойством собственного типа не компилируется и как это исправить?
Почему struct с хранимым свойством собственного типа не компилируется и как это исправить?
Значимый тип хранится встроенно, поэтому struct, содержащий сам себя, потребовал бы бесконечного размера и не скомпилируется. Добавьте косвенность через ссылку — сделайте тип class или indirect enum; ссылка размером с указатель.
Типичные ошибки
- ✗Думают, что ошибка про именование или инициализацию, а не про размер хранения
- ✗Считают, что
structхранится за указателем, какclass - ✗Полагают, что
lazyили значение по умолчанию решает проблему бесконечного размера
Уточняющие вопросы
- →Почему ссылка размером с указатель независимо от того, на что указывает?
- →Когда
indirect enum— лучшее решение, чем класс-обёртка?
MiddleТеорияИногдаКак Array избегает копирования при каждом присваивании и в какой именно момент происходит копия?
Как Array избегает копирования при каждом присваивании и в какой именно момент происходит копия?
Присваивание Array копирует лишь ссылку на общий буфер и увеличивает счётчик ссылок — O(1). Буфер копируется лениво при первой записи в ещё разделённый буфер, когда владелец не единственный, сохраняя значимую семантику.
Типичные ошибки
- ✗Думают, что присваивание сразу копирует все элементы
- ✗Считают, что буфер копируется при присваивании, а не при первой записи
- ✗Полагают, что две переменные-массива разделяют буфер навсегда, как
class
Уточняющие вопросы
- →Что проверяет
isKnownUniquelyReferencedперед мутацией? - →Почему добавление в уникально удерживаемый массив не является копией?
SeniorКодРедкоРеализуйте copy-on-write вручную для типа с буфером-class, используя isKnownUniquelyReferenced
Реализуйте copy-on-write вручную для типа с буфером-class, используя isKnownUniquelyReferenced
Оберните буфер-class из кучи в struct. Перед каждой мутацией проверяйте isKnownUniquelyReferenced(&buffer); если false, замените его свежей глубокой копией, затем пишите. Копия возникает лишь при мутации разделённого буфера — ленивая значимая семантика.
Типичные ошибки
- ✗Копируют буфер при каждом присваивании, а не только при мутации разделённого
- ✗Проверяют уникальность в геттере, а не перед записью
- ✗Думают, что
isKnownUniquelyReferencedработает на значимых типах илиweak-ссылках
Уточняющие вопросы
- →Почему проверку уникальности нужно делать до записи, а не после?
- →Что ломается, если два потока мутируют один общий буфер одновременно?