Типы значений и ссылочные типы, распределение стека и кучи и т. Д.

Во время собеседований мне много раз задавали этот вопрос: «Что такое класс и что такое структура? Назовите несколько отличий и укажите, когда вы бы их использовали ».

Поэтому я подумал, что займусь этой темой, начиная с самых основ, и перейду к каждому из их нишевых свойств.

Общий обзор

На очень высоком уровне структуры и классы можно рассматривать как конструкции, которые используются для хранения значений (переменных и констант) и функций. Наборы функций, предоставляемые каждым из них, похожи, за исключением нескольких деталей реализации, которые фактически влияют на ваши решения о том, когда использовать структуру, а когда использовать класс.

Структуры отличаются от классов двумя способами:

  1. Структуры - это типы значений, тогда как классы - это ссылочные типы.
  2. Структуры не поддерживают наследование (по сути, следствие первого пункта).

Типы значений и ссылочные типы

В Swift классы и замыкания являются ссылочными типами, тогда как структуры и перечисления являются типами значений.

То, как типы значений и ссылочные типы работают по-разному, можно резюмировать в следующем предложении: Каждая новая ссылка на тип значения - это новая копия этого члена, тогда как каждая новая ссылка на ссылочный тип - это просто еще одна ссылка на тот же член.

Это можно хорошо понять на примере:

Из приведенного выше примера довольно очевидно, что происходят следующие вещи:

  1. Каждый раз, когда вы назначаете переменную типа значения (personStruct) новой переменной (personStruct2), новая копия переменной передается новой переменной (personStruct2). Следовательно, любые изменения, внесенные в любую из переменных внутри них, не повлияют на другие переменные.
  2. Каждый раз, когда вы назначаете переменную ссылочного типа (personClass) новой переменной (personClass2 или personClass3), новая копия переменной передается новой переменной (personClass2 или personClass3). Следовательно, любые изменения, внесенные в любую из переменных внутри них, будут влиять на другие переменные.
  3. Типы значений, определенные с помощью let, не позволят вам изменить их содержимое, но это не относится к ссылочным типам. Тип постоянной ссылки вызовет ошибку компиляции только в том случае, если сама переменная указывает на другую переменную.

мутирующий

mutating - это специальное ключевое слово, зарезервированное для случаев, когда вы хотите изменить внутренние элементы struct.

Управление памятью

Управление памятью обоих этих типов может сыграть важную роль в принятии решения о том, использовать ли структуры или классы, поскольку они влияют на производительность каждого типа в сценарии, в котором они используются. В Swift есть два способа управления памятью для типов значений и ссылок: выделение стека и выделение кучи.

Итак, давайте обсудим, как они сравниваются.

Распределение стека

Этот вид распределения работает путем выделения памяти в стеке, который представляет собой линейную структуру данных, которая ведет себя в стиле LIFO (последний вошел, первый ушел). Объекты, помещенные в стек последними, будут выселены первыми.

Каждый поток имеет свой собственный стек, и объекты, размещенные в этой стековой памяти, являются эксклюзивными для этого потока, что делает объекты, размещенные в стеке, потокобезопасными. Кроме того, объем памяти, который может быть выделен в стеке, меньше памяти кучи.

При распределении памяти стека программа предоставляет вам объем памяти, необходимый для выполнения определенного объема кода. Затем стек добавляет тот же объем памяти к себе, и после того, как область видимости закрывается (т.е. код завершает выполнение), память освобождается.

Теперь стоимость выделения / отмены выделения памяти в этом стеке заключается в перемещении указателя стека на требуемый объем памяти, и для этого требуется всего одна инструкция для выполнения.

Память, требуемая для типов значений, с которыми не связаны какие-либо ссылочные типы (т.е. они либо не содержатся в ссылочном типе, либо содержат его), может быть вычислена во время самой компиляции, поскольку они не являются динамическими и не нуждаются в семантике счетчика ссылок для решить, сколько им осталось жить. Они существуют в пределах области действия, в которой они необходимы, а затем сбрасываются после завершения их использования. Следовательно, память таких типов значений управляется в стеке.

На первый взгляд, это значительно ускоряет использование типов значений, поскольку их память управляется с использованием распределения стека.

Распределение кучи

Выделение кучи выполняется для объектов, размер которых не может быть вычислен во время компиляции, и для объектов, имеющих семантику ссылочного типа, поскольку это означает, что их время жизни не может быть ограничено только объемом памяти, выделенной в стеке. Вместо этого для его размещения потребуется некоторое пространство глобальной памяти.

Теперь распределение кучи происходит в структуре данных кучи, которой, как правило, труднее управлять по сравнению со стеком. Выделение и освобождение памяти не происходит в одной инструкции и, следовательно, немного сложнее, чем соответствующие процессы в стеке. Освобождение памяти происходит с помощью Автоматического подсчета ссылок.

Более того, кучная память - это глобальное пространство для размещения и, следовательно, небезопасная для потоков, поскольку она не относится к конкретному потоку. Это требует от вас управления безопасностью потоков при доступе к памяти кучи.

Все эти моменты делают довольно очевидным, что память кучи сравнительно сложнее в управлении по сравнению с памятью стека. У него есть свои накладные расходы, и поэтому его следует использовать только тогда, когда этого требует ситуация - и Swift делает именно это.

Типы значений, связанные со ссылочными типами

В реальном мире у вас всегда будут ситуации, когда вам придется использовать как ссылочные типы, так и типы значений, переплетенные друг с другом. Вот где следующее обсуждение поможет вам принять гораздо более обоснованное решение о том, какой тип использовать для хранения ваших данных.

При игре с обоими типами могут произойти два случая:

  1. Ссылочный тип, который имеет член типа значения (или несколько).
  2. Тип значения, члены которого являются ссылочными типами.

Случай 1: ссылочный тип, содержащий тип значения

В разделе «Распределение стека» я специально упомянул, что типы значений, которые не имеют связанных с ними ссылочных типов, управляются памятью с помощью стека.

Причина этого в том, что когда у вас есть ссылочный тип, содержащий член типа значения, ссылочный тип будет управляться памятью в куче. Следовательно, связанный с ним тип значения также должен храниться в куче, чтобы его не было отменено до отмены выделения ссылочного типа. Очень распространенный, но часто упускаемый из виду пример этого типа - типы значений, используемые внутри замыканий.

Случай 2: Тип значения, содержащий ссылочный тип

В этом случае тип значения не будет размещен в куче, но с ним должна быть связана логика подсчета ссылок, поскольку теперь он содержит внутри себя ссылочный тип. Причина этого в том, что если этого не происходит, то ссылочный тип, хранящийся внутри него, может быть отменен, даже если сам тип значения содержит ссылку на него.

Копирование при переуступке и копирование при записи

По умолчанию типы значений следуют поведению копирования при назначении. Это означает, что когда вы назначаете структуру новой переменной, создается новая копия структуры.

Копирование при записи - это оптимизация памяти, согласно которой новые типы значений не создаются при назначении новому члену. Вместо этого они создаются только при изменении значения второго члена. Это сделано потому, что копирование типов значений, которые содержат ссылочные типы, сопряжено с огромными накладными расходами из-за семантики подсчета ссылок, которая должна быть связана с ними. Это дорогостоящая операция.

Эта оптимизация памяти доступна только для определенных специальных структур Swift (например, Array, Set и Dictionary), а не для всех типов значений, содержащих ссылочные типы.

Однако, если вы действительно хотите, вы всегда можете создать свои собственные структуры копирования при записи, используя этот пример.

Оба этих случая вызывают общие накладные расходы с точки зрения производительности для типов значений, поскольку они либо должны распределяться по-другому, либо с ними должны быть связаны некоторые характеристики, которые не являются их неотъемлемым поведением. Swift должен будет управлять этим отдельно (что требует дополнительного времени).

Наследование

Как типы значений, структуры не поддерживают наследование, что дает им много преимуществ в производительности (в отношении управления памятью, диспетчеризации методов и т. Д.), Но это не означает, что вы не можете расширить их функциональность.

Способ расширения функциональности структур - использование протоколов.

Выбор между структурами и классами

Итак, мы увидели, что структуры и классы имеют некоторые различия в способах управления, что дает ряд веских аргументов в пользу использования структур:

  • Безопасность потока - у каждого потока будет своя копия переменной. Следовательно, вам не нужно реализовывать стратегии блокировки.
  • Никаких побочных эффектов - вы можете быть уверены, что если вы поделились экземпляром struct, любые изменения этого экземпляра не повлияют на другие экземпляры того же struct, что соответствует Принципу наименьшего удивления.
  • Безопасное использование производных значений.

Есть больше преимуществ использования неизменяемых, но это тема для другого дня (они обычно вращаются вокруг того факта, что они, как правило, более предсказуемы для рассуждений).

Но мы также увидели, что совместное использование структур и классов может поставить вас в затруднительные ситуации, когда использование структуры может не дать тех преимуществ, которые вы изначально думали. Вы можете столкнуться с ситуациями, когда на самом деле было бы лучше использовать класс с точки зрения производительности.

Итак, вот несколько общих правил, которым нужно следовать при выборе между структурой и классом:

  1. Используйте структуры, если вы хотите хранить простые значения данных, которые сами по себе являются типами значений.
  2. Используйте структуры, когда им нужно удерживать световые объекты, потому что память стека обычно ограничена.
  3. Используйте структуры, когда вы не контролируете личность объекта.
  4. Используйте классы, если вам нужна совместимость с Objective-C.

Разное чтение