% Типы экзотического размера

Большую часть времени мы думаем в терминах типов фиксированного положительного размера. Однако, это не всегда так.

Типы динамического размера

Rust на самом деле поддерживает типы динамического размера (ТДР; англ. dynamically sized type, DST): типы без статически известного размера или выравнивания. На поверхности, все это кажется бессмысленным: Rust должен знать размер и выравнивание чего-либо, чтобы корректно с этим работать! В этом отношении, ТДР - это не нормальные типы. Из-за отсутствия статически известного размера, они могут скрываться только за указателем особого типа. Любой указатель на ТДР вследствие этого становится толстым, состоящим из указателя и информации, которая "дополняет" его (подробнее об этом ниже).

Язык предлагает два главных ТДР: типажи-объекты и срезы.

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

Срез - это просто отображение в какое-то подходящее хранилище - обычно в массив или Vec. Информация, которая дополняет срез - это просто количество элементов, на которые он указывает.

Вообще, структуры могут хранить один ТДР прямо в своем последнем поле, но это делает их также ТДР:


#![allow(unused)]
fn main() {
// Невозможно хранить напрямую в стеке
struct Foo {
    info: u32,
    data: [u8],
}
}

Внимание: В Rust 1.0 ТДР, являющиеся структурами, работают неправильно, если последнее поле имеет переменную позицию, зависящую от выравнивания.

Типы с нулевым размером

Rust на самом деле позволяет объявлять типы, которые не занимают места:


#![allow(unused)]
fn main() {
struct Foo; // Нет полей = нет размера

// У каждого поля нет размера  = нет размера
struct Baz {
    foo: Foo,
    qux: (),      // у пустого кортежа нет размера
    baz: [u8; 0], // у пустого массива нет размера
}
}

Сами по себе типы с нулевым размером (ТНР; англ. zero-sized types, ZST), по очевидным причинам, довольно бесполезны. Но, как и с другими любопытными решениями по выделению памяти в Rust, их потенциал выражается в контексте обобщений: Rust очень хорошо понимает, что любые операции, которые создают или хранят ТНР, могут быть заменены на пустую операцию. Прежде всего хранение их вообще не имеет смысла - они не занимают никакого места. Также, есть только одно значение этого типа, поэтому все, что загружает их, может создать их из эфира - что тоже является пустой операцией из-за того, что они опять же не занимают никакого места.

Одним из самых важных примеров являются множества (sets) и словари (maps). Имея Map<Key, Value>, часто реализуют Set<Key> в качестве тонкой обёртки вокруг Map<Key, БесполезныйМусор>. Многие языки заставляют выделять место под БесполезныйМусор и вынуждают обрабатывать его хранение и загрузку только для того, чтобы потом выкинуть его. Их компиляторам сложно доказать, что эти действия не нужны.

Однако в Rust мы можем просто сказать Set<Key> = Map<Key, ()>. Так Rust понимает, что любая загрузка и хранение бесполезны, и выделение памяти не нужно. В результате получаем, что мономорфный код - это обычная частая реализация HashSet без каких бы накладных расходов по поддержке значений.

Безопасному коду не надо волноваться о ТНР, но небезопасный должен быть очень аккуратен с последствиями, которые влекут типы без размера. В частности, смещение указателей это no-op, и стандартный распределитель памяти (включая jemalloc, который использует по умолчанию Rust) может вернуть nullptr если запрашивается выделение памяти под тип с нулевым размером, и это будет неотличимо от ситуации нехватки памяти.

Пустые типы

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


#![allow(unused)]
fn main() {
enum Void {} // Вариантов нет = Пусто
}

Пустые типы еще более маргинальны чем ТНР. Единственное назначение типа Void из примера выше - недостижимость на уровне типов. Предположим, API в общем случае должно возвращать Result, но какой-то частный случай никогда не возвращает ошибку. Здесь можно сообщить об этом на уровне типа, возвращая Result<T, Void>. Пользователи этого API могут спокойно делать unwrap такого Result, зная что статически невозможно получить Err в значении, так как пришлось бы предоставлять значение типа Void.

В принципе, Rust мог бы выполнять некоторый интересный анализ и оптимизацию, зная все это. Например, Result<T, Void> мог бы быть представлен просто как T, потому что случая Err на самом деле не существует. Код ниже мог бы компилироваться:

enum Void {}

let res: Result<u32, Void> = Ok(0);

// Err не существует, поэтому Ok полностью безошибочен.
let Ok(num) = res;

Но на данный момент ни один из этих трюков не работает, поэтому все, что дает вам тип void - это возможность быть уверенным в том, что некоторые ситуации статически невозможны.

И последний тонкий момент о пустых типах - сырые указатели на них создавать можно и это считается правильным, но разыменование таких указателей приведет к Неопределенному поведению, потому что никакого смысла в этом нет. Таким образом, вы можете смоделировать void * из Си с помощью *const Void, но это не обязательно даст выигрыш по сравнению с использованием, например, *const (), который безопасен по отношению к случайному разыменованию.