В Swift, когда тип-значение (value type) не хранится в стеке (stack)?

Ответ

Тип-значение (структура или перечисление) по умолчанию хранится в стеке, но попадает в кучу (heap) в следующих сценариях:

  1. Когда он является свойством класса: Все содержимое экземпляра класса размещается в куче.

    class MyClass {
        var myStruct: MyStruct // MyStruct хранится в куче как часть MyClass.
    }
  2. При захвате замыканием с продлённым временем жизни: Если замыкание, захватившее структуру, является @escaping или сохраняется, значение переносится в кучу.

  3. При работе с механизмом Copy-on-Write (CoW): Большие типы-значения (как Array, String, Dictionary) используют кучу для хранения своих буферов. При копировании копируется только ссылка на буфер, а реальное копирование данных в новую область кучи происходит лишь при изменении (write).

    var array1 = [1, 2, 3] // Буфер массива в куче.
    var array2 = array1     // Копируется только ссылка (CoW). Память в куче общая.
    array2.append(4)        // Теперь происходит реальное копирование буфера в новое место кучи.
  4. При использовании indirect для перечислений: Ключевое слово indirect заставляет хранить связанное значение перечисления в куче, позволяя рекурсивные структуры.

    indirect enum LinkedListNode {
        case value(Int, next: LinkedListNode)
        case end
    }

Ответ 18+ 🔞

А, слушай, смотри, вот тебе на самом деле интересная хуйня, про которую многие думают, что они её понимают. Тип-значение, структура там или енум, он по умолчанию, конечно, в стеке. Прямо там, под носом, быстрый, локальный, красота. Но есть такие подлые сценарии, когда эта, блядь, невинная структурка вдруг пиздует прямиком в кучу, как будто так и надо. И вот почему.

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

class MyClass {
    var myStruct: MyStruct // MyStruct теперь сидит в куче, как часть MyClass. Смирись.
}

Во-вторых, этот момент с замыканиями, которые сбегают. Ну, @escaping там или просто сохраняются куда-то. Если твоя структура попала в плен к такому замыканию, которое само решило задержаться подольше, то её, бедолагу, тоже выпихивают в кучу, чтобы она не сдохла раньше времени. Захватили — и в ссылочный мир, пиздец.

В-третьих, вот эта ихняя любимая хуйня — Copy-on-Write (CoW). Это вообще гениальная подстава. Большие ребята вроде Array, String или Dictionary — они же тоже типы-значения, да? Но они не дураки, чтобы таскать с собой гигабайты данных по стеку. Они хранят свои настоящие данные — буфер — в куче. А у тебя в руках только билетик, ссылочка на этот буфер. Ты копируешь массив — копируется только билетик, а буфер в куче один на всех. И все довольны, пока ты не решишь один массив изменить. Вот тут-то система и говорит: «Ага, щас, блядь, разошёлся!» — и делает настоящее, дорогое копирование буфера в новое место кучи. Хитрая жопа, одним словом.

var array1 = [1, 2, 3] // Буфер массива уже в куче, ёпта.
var array2 = array1     // Копируем билетик. В куче всё тот же буфер, общий.
array2.append(4)        // Опа-на! Изменение! Теперь буфер копируется в новое место кучи. Вот тебе и CoW.

И наконец, четвёртое — indirect для перечислений. Это вообще специальный инструмент для создания рекурсивных структур, которые иначе бы не влезли в себя сами, понимаешь? Ты говоришь indirect — и связанное с кейсом значение летит прямиком в кучу. Потому что иначе как ты впихнёшь LinkedListNode внутрь самого LinkedListNode? Никак, блядь. Только через ссылку, через кучу.

indirect enum LinkedListNode {
    case value(Int, next: LinkedListNode) // 'next' теперь не прямо здесь, а где-то там, в куче.
    case end
}

Вот и получается, что «тип-значение» — это не всегда про стек. Это про семантику. А где физически лежат данные — это уже второй вопрос, на который иногда ответ — «в пизду, то есть в кучу».