При поэлементном заполнении массива через MaybeUninit<T> какой инвариант позволяет безопасно превратить его в массив T?
Перед вызовом assume_init каждый элемент массива должен быть инициализирован корректным значением типа T ровно один раз. Нельзя читать неинициализированный элемент, пропускать позиции или допускать двойное уничтожение уже созданных значений.
MaybeUninit<T> разрешает временно хранить память без значения T, но не отменяет требований к корректности самого T. Преобразование в массив T безопасно только после доказательства полноты инициализации всех элементов.
Обычный объект Rust должен всегда содержать корректное значение своего типа. Это важно не только для чтения: компилятор и стандартная библиотека могут полагаться на инварианты типа, например на допустимость всех значений bool, корректность ссылок или необходимость вызвать Drop.
При работе с низкоуровневыми буферами, FFI и поэлементным построением объекта память иногда нужно выделить раньше, чем все значения готовы. MaybeUninit<T> отделяет выделенную память от утверждения, что в ней уже находится инициализированный T, не заставляя создавать фиктивное значение.
Неинициализированная память не является массивом значений T. Если считать её инициализированной раньше времени, можно получить неопределённое поведение: прочитать недопустимое значение, вызвать Drop для несуществующего объекта или нарушить инварианты библиотеки.
Особенно опасен частично заполненный массив. Например, если построение прервалось после записи трёх элементов из пяти, преобразование всего буфера в [T; 5] некорректно. Если T владеет ресурсом, при обработке ошибки нужно уничтожить только уже записанные элементы, а не весь буфер.
Главный инвариант состоит из двух частей:
T, записана корректным значением;После выполнения этих условий assume_init лишь сообщает компилятору то, что уже доказал вызывающий код. Эта операция не проверяет память во время выполнения и не исправляет пропущенные элементы.
Минимальный пример для массива чисел:
В цикле каждая позиция записывается ровно один раз, поэтому после завершения цикла массив полностью инициализирован. write не читает старое содержимое и не пытается уничтожить значение, которого ещё нет.
Для типа с Drop необходима дополнительная логика обработки частичного заполнения. Если операция построения может завершиться паникой или ошибкой, код должен хранить число успешно инициализированных элементов и уничтожать только этот префикс. На практике часто применяют отдельный guard с Drop, который при размотке уничтожает уже созданные значения и отключается после успешного завершения.
Повторная запись в уже инициализированную позицию допустима только как осознанная замена значения с корректным управлением старым объектом. Простая запись через MaybeUninit::write безопасна для неинициализированного слота, но не должна использоваться как способ незаметно заменить живое значение, если это приводит к утечке ресурса.
MaybeUninit также не делает операции с raw pointers безопасными автоматически. Остаются требования к выравниванию, границам выделенного объекта, уникальности изменяемого доступа и корректности типа, которые должен обеспечить unsafe-код.
Низкоуровневая функция получает размер массива и заполняет его элементы результатами вызовов внешней библиотеки. Часть вызовов может завершиться ошибкой, а элементами являются структуры, владеющие дескрипторами ресурсов.
Первый вариант — сначала создать массив значениями по умолчанию, а затем заменять элементы. Он проще с точки зрения времени жизни, но требует существования корректного Default, может выполнять лишнюю работу и временно создавать ненужные ресурсы.
Второй вариант — использовать MaybeUninit и заполнять слоты по одному. Он эффективнее и не требует фиктивных значений, но обязан учитывать частичное заполнение: guard должен уничтожить уже созданные элементы при ошибке или панике.
Выбирается второй вариант, если создание T дорого, Default отсутствует или требуется прямое заполнение буфера. После успешного заполнения всех позиций guard отключается, и только тогда вызывается assume_init; при ошибке уничтожается ровно инициализированная часть. Это сохраняет отсутствие утечек и не допускает Drop для несуществующих значений.
Можно ли вызвать assume_init, если часть элементов массива никогда не будет использоваться?
Нет. assume_init относится ко всему объекту T, а не к фактическим будущим обращениям к его отдельным элементам. Для [T; N] все N позиций должны содержать корректные значения, даже если программа планирует читать только несколько из них.
Что произойдёт, если один элемент записать дважды через MaybeUninit::write?
Для слота, который всё ещё считается неинициализированным, запись корректна. Но если первая запись уже создала значение типа T, повторная запись поверх него без уничтожения старого значения может привести к утечке ресурса: прежний T не получит Drop. Для типов без значимого уничтожения это может не иметь практического эффекта, но общий контракт для произвольного T всё равно нарушен.
Достаточно ли считать элементы инициализированными после успешного вызова конструктора?
Только если конструктор действительно записал корректное значение в нужную позицию и не оставил её частично инициализированной. Для типа T должны быть выполнены все его инварианты, включая допустимые значения полей, корректные ссылки и требования к владению. Успешное завершение внешней функции само по себе не доказывает эти свойства: их обязан подтвердить Rust-код, принимающий результат.