Создание вектора нулей в Rust: магия под капотом

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Вектор нулей в 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 делает следующее:

  1. Выделяет память на куче (heap) под 10 элементов типа i32. Каждый i32 занимает 4 байта, итого 40 байт.
  2. Проверяет, что тип реализует трейт Clone. Числа i32 его реализуют, поэтому компилятор может скопировать значение 0 для каждого элемента.
  3. Запускает цикл копирования внутри alloc::vec::from_elem. Компилятор оптимизирует этот цикл до memset (если тип — простой скалярный).
  4. Возвращает 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-кодом) или при работе с соб

Блоги

Рекомендаций по книгам