Вектор нулей в Rust: не так просто, как кажется
Казалось бы, что может быть проще, чем создать вектор, заполненный нулями? В Python это делается одной строкой: [0] * n. В JavaScript — Array(n).fill(0). Но когда вы переходите на Rust, даже такая тривиальная задача открывает целый мир системного программирования, управления памятью и нулевых затрат на абстракции.
Вопрос "Creating a vector of zeros for a specific size" на Stack Overflow собрал тысячи просмотров. И это не случайно: за простым запросом скрывается глубокое понимание того, как Rust работает с памятью, инициализацией и безопасностью. Сегодня мы заглянем под капот Rust и разберем все способы создать вектор нулей, а главное — поймем, что на самом деле происходит в оперативной памяти.
Почему это сложнее, чем в Python?
В Python или JavaScript вы просто создаете массив, и интерпретатор делает всю грязную работу за вас. Rust же — язык системного уровня. Он не позволяет оставлять память неинициализированной (если вы не используете unsafe). Каждый байт должен быть либо явно заполнен, либо помечен как "неопределенное поведение".
Представьте: вы заказываете 100 пустых коробок. В Python вам просто привезут 100 пустых коробок. В Rust же курьер спросит: "А что положить внутрь каждой?" — и если вы не скажете, он просто не приедет. Это гарантирует, что вы никогда случайно не прочитаете "мусор" из памяти, оставшийся от предыдущей программы.
Способ №1: Классический vec![0; size]
Самый очевидный и рекомендуемый способ — использовать макрос vec!:
let zeros: Vec<i32> = vec![0; 10];
println!("{:?}", zeros); // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Коротко, элегантно, понятно. Но что происходит под капотом? Давайте разберем по шагам.
Под капотом vec![0; 10]
Когда вы пишете vec![0; 10], Rust делает следующее:
- Выделяет память на куче (heap) под 10 элементов типа
i32. Каждыйi32занимает 4 байта, итого 40 байт. - Проверяет, что тип реализует трейт
Clone. Числаi32его реализуют, поэтому компилятор может скопировать значение0для каждого элемента. - Запускает цикл копирования внутри
alloc::vec::from_elem. Компилятор оптимизирует этот цикл доmemset(если тип — простой скалярный). - Возвращает
Vecс длиной 10 и емкостью 10.
Ключевой момент: память не остается неинициализированной. Rust гарантирует, что каждый байт будет заполнен нулями до того, как вы получите доступ к вектору.
Способ №2: vec![Default::default(); size]
Если ваш тип не является числом, но реализует трейт Default, вы можете использовать обобщенный подход:
#[derive(Default)]
struct Point {
x: f64,
y: f64,
}
let points: Vec<Point> = vec![Default::default(); 5];
println!("{:?}", points[0]); // Point { x: 0.0, y: 0.0 }
Для примитивных типов Default::default() дает 0, 0.0, false или '\0'. Для пользовательских структур — то, что вы определили в #[derive(Default)].
А что с производительностью?
Давайте напишем простой бенчмарк (в реальном коде используйте criterion):
use std::time::Instant;
fn main() {
let n = 1_000_000;
let start = Instant::now();
let v1 = vec![0u8; n];
let dur1 = start.elapsed();
let start = Instant::now();
let v2 = vec![0u64; n];
let dur2 = start.elapsed();
println!("u8: {:?}", dur1);
println!("u64: {:?}", dur2);
}
Вы удивитесь, но время будет практически одинаковым! Почему? Потому что memset (который вызывает Rust под капотом) оптимизирован на уровне процессора и работает с блоками по 8, 16 или 32 байта за раз. Размер элемента не имеет значения — имеет значение только общий объем памяти.
Способ №3: Vec::with_capacity + ручное заполнение
Иногда вам нужно создать вектор, а затем заполнить его не сразу, а по мере поступления данных. В этом случае используют Vec::with_capacity:
let mut zeros = Vec::with_capacity(100);
// Вектор пуст! len = 0, capacity = 100
zeros.resize(100, 0);
// Теперь len = 100, все элементы = 0
Или еще более гранулярно:
let mut zeros = Vec::with_capacity(100);
for _ in 0..100 {
zeros.push(0);
}
Разница: with_capacity выделяет память сразу, но не инициализирует ее. Вы можете "заполнить" ее позже. Это полезно, если вы знаете точный размер заранее и хотите избежать многократного перевыделения памяти при push.
Опасность: set_len и неинициализированная память
Продвинутые пользователи иногда используют set_len для "быстрого" создания вектора:
let mut zeros = Vec::with_capacity(100);
unsafe {
zeros.set_len(100);
}
// ВНИМАНИЕ! Память не инициализирована!
// Чтение любого элемента — неопределенное поведение!
Это опасный способ. Да, он быстрее (нет цикла memset), но вы получаете мусор в памяти. Rust не прощает таких вольностей. Если вы читаете неинициализированную память — это UB (undefined behavior), и программа может вести себя как угодно: от падения до утечки данных.
Способ №4: Vec::from_raw_parts (для экстремалов)
Самый низкоуровневый способ — использовать from_raw_parts. Он принимает сырой указатель, длину и емкость:
let mut v = Vec::from_raw_parts(
Box::into_raw(vec![0u8; 100].into_boxed_slice()) as *mut u8,
100,
100,
);
На практике этот способ используется редко — только при FFI (взаимодействии с C-кодом) или при работе с соб