Основы Rust: векторы и итераторы

Игорь Грегорченко

В нашей последовательной серии материалов мы рассмотрим базовые основы новомодного языка Rust. А во второй части цикла на основе изученного попробуем написать самые простые смарт-контракты для таких блокчейн-проектов, как Solana. В этом туториале будет много примеров, мало теории и быстрый темп продвижения.

Этот пост — вольный перевод на русский вот этой оригинальной статьи (с нашими дополнениями в местах, где это показалось нужным), которую написал Стив Донован. Начало можно найти вот здесь, а оглавление всей серии – вот тут.

Векторы

Мы еще вернемся к методам срезов, которые обсудили в прошлом разделе, но сначала — векторы. Это массивы с изменяемым размером, которые ведут себя подобно Python List и C++ std::vector.

Тип Vec (произносится как «вектор») ведет себя очень похоже на срез; разница в том, что можно добавлять дополнительные значения в вектор — обратите внимание, что он должен быть объявлен как mutable.

// vec1.rs
fn main() {
    let mut v = Vec::new();
    v.push(10);
    v.push(20);
    v.push(30);

    let first = v[0];  // will panic if out-of-range
    let maybe_first = v.get(0);

    println!("v is {:?}", v);
    println!("first is {}", first);
    println!("maybe_first is {:?}", maybe_first);
}
// v is [10, 20, 30]
// first is 10
// maybe_first is Some(10)

Частая ошибка новичков — забыть про mut, и тогда вы получите полезное сообщение об ошибке:

3 |     let v = Vec::new();
  |         - use `mut v` here to make mutable
4 |     v.push(10);
  |     ^ cannot borrow mutably

Есть очень тесная связь между векторами и срезами:

// vec2.rs
fn dump(arr: &[i32]) {
    println!("arr is {:?}", arr);
}

fn main() {
    let mut v = Vec::new();
    v.push(10);
    v.push(20);
    v.push(30);

    dump(&v);

    let slice = &v[1..];
    println!("slice is {:?}", slice);
}

Этот маленький, но важный оператор заимствования & принудительно превращает вектор в срез. И в этом есть смысл, потому что вектор также заботится о массиве значений, с той разницей, что массив выделяется динамически.

Если вы пришли из динамического языка, то сейчас самое время поговорить об этом. В системных языках память программ бывает двух видов: стек и куча. Выделять данные в стеке очень быстро, но стек ограничен; обычно его размер составляет порядка мегабайта. Куча может быть гигабайтной, но ее выделение относительно дорого, и такая память должна быть освобождена позже.

В так называемых управляемых языках (Java, Go и «скриптовых» языках) эти детали скрыты от вас удобной встроенной утилитой, называемой сборщиком мусора. Как только система убедится, что данные больше не ссылаются на другие данные, они возвращаются в пул доступной памяти.

Это цена, которую стоит заплатить. Играть со стеком ужасно небезопасно, потому что если вы сделаете ошибку, то можете переопределить адрес возврата текущей функции.

Такая неверно написанная программа на Cи, запущенная на ПК под DOS, завесит весь компьютер. Unix-системы всегда вели себя лучше — в этом случае только один процесс умрет, испустив segfault. Почему это не хуже, чем паника программы на Rust (или Go)? Потому что паника происходит, когда возникает первичная проблема (и у нас еще есть шанс ее корректно обработать), а не когда программа безнадежно запуталась под валом нарастающих проблем и съела всю доступную память.

Паника безопасна для памяти, потому что происходит до любого незаконного доступа к ней. Это распространенная причина проблем безопасности в Cи, потому что все обращения к памяти потенциально небезопасны и хитрый злоумышленник может воспользоваться этой слабостью.

Паника звучит как что-то хаотичное и незапланированное, но паника в Rust структурирована — стек разворачивается так же, как и при исключениях. Все выделенные объекты сбрасываются, и генерируется обратная трассировка. Поэтому в Rust все обстоит не так драматично, как в других языках программирования.

Минусы сборки мусора? Первый — это неэкономное расходование памяти, что имеет значение для маленьких встроенных микросхем, которые все больше правят нашим миром. Второй заключается в том, что в самый неподходящий момент сборщик решит, что очистка должна произойти прямо сейчас. А эти встроенные системы должны реагировать на происходящее в тот момент, когда оно происходит («в режиме реального времени»), и терпеть не могут незапланированные вспышки уборки.

Итак, вернемся к векторам: когда вектор модифицируется или создается, он выделяет память из кучи и становится владельцем этой памяти. Слайс заимствует память у вектора. Когда вектор умирает или падает, он автоматически освобождает память.

Итераторы

Мы так далеко зашли, не упомянув ключевую часть головоломки Rust — итераторы. В цикле for для диапазона использовался итератор (смотрите пример выше).

Итератор легко определить неформально. Это объект с методом next, который возвращает значение Option. Пока это значение не равно None, мы продолжаем вызывать next:

// iter1.rs
fn main() {
    let mut iter = 0..3;
    assert_eq!(iter.next(), Some(0));
    assert_eq!(iter.next(), Some(1));
    assert_eq!(iter.next(), Some(2));
    assert_eq!(iter.next(), None);
}

И это именно то, что делает конструкция for var in iter {}.

Это может показаться неэффективным способом определения цикла for, но rustc делает сумасшедшие оптимизации в режиме release, и это будет так же быстро, как цикл while.

Усложняем задачу, совмещая массивы и итераторы — вот наша первая попытка выполнить итерацию по массиву:

// iter2.rs
fn main() {
    let arr = [10, 20, 30];
    for i in arr {
        println!("{}", i);
    }
}

Это приводит к неудаче, но было полезно попытаться.

4 |     for i in arr {
  |     ^ the trait `std::iter::Iterator` is not implemented for `[{integer}; 3]`
  |
  = note: `[{integer}; 3]` is not an iterator; maybe try calling
   `.iter()` or a similar method
  = note: required by `std::iter::IntoIterator::into_iter`

Следуя последнему прямому совету rustc, поправим код, и следующая программа работает как ожидалось.

// iter3.rs
fn main() {
    let arr = [10, 20, 30];
    for i in arr.iter() {
        println!("{}", i);
    }

    // slices will be converted implicitly to iterators...
    let slice = &arr;
    for i in slice {
        println!("{}", i);
    }
}

На самом деле, итерация по массиву или срезу таким образом более эффективна, чем использование for i in 0..slice.len() {}, потому что Rust не нужно навязчиво проверять каждую операцию с индексом.

Ранее у нас был пример суммирования диапазона целых чисел. В нем использовались mut-переменная и цикл. А вот профессиональный способ суммирования, где на базе последних знаний мы делаем это короче и эффективнее:

// sum1.rs
fn main() {
    let sum: i32  = (0..5).sum();
    println!("sum was {}", sum);

    let sum: i64 = [10, 20, 30].iter().sum();
    println!("sum was {}", sum);
}

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

Еще один совет по документации: на правой стороне каждой страницы документа есть символ ‘[-]’, нажав на который, можно свернуть список методов. Затем можно развернуть детали всего, что выглядит интересным.

Метод windows дает вам итератор срезов — перекрывающихся окон значений:

// slice4.rs
fn main() {
    let ints = [1, 2, 3, 4, 5];
    let slice = &ints;

    for s in slice.windows(2) {
        println!("window {:?}", s);
    }
}
// window [1, 2]
// window [2, 3]
// window [3, 4]
// window [4, 5]

Или это же через chunks:

    for s in slice.chunks(2) {
        println!("chunks {:?}", s);
    }
// chunks [1, 2]
// chunks [3, 4]
// chunks [5]

Подробнее о векторах…

Есть полезный макрос vec! для инициализации вектора.

Обратите внимание, что можно удалять значения из конца вектора с помощью pop, а также расширять вектор с помощью любого совместимого итератора.

// vec3.rs
fn main() {
    let mut v1 = vec![10, 20, 30, 40];
    v1.pop();

    let mut v2 = Vec::new();
    v2.push(10);
    v2.push(20);
    v2.push(30);

    assert_eq!(v1, v2);

    v2.extend(0..2);
    assert_eq!(v2, &[10, 20, 30, 0, 1]);
}

Векторы сравниваются друг с другом и со срезами по значению.

Можно вставлять значения в вектор в произвольных позициях с помощью insert и удалять с помощью remove. Это не так эффективно, как push и popping, поскольку значения придется перемещать, чтобы освободить место, поэтому будьте осторожны с этими операциями на больших векторах.

Векторы имеют размер и емкость. Если вы очищаете вектор, его размер становится нулевым, но он сохраняет свою старую емкость. Поэтому заполнение его с помощью push и т.д. требует перераспределения только тогда, когда размер становится больше этой емкости.

Векторы можно сортировать, а затем удалять дубликаты (если хотите сначала создать копию, используйте clone).

// vec4.rs
fn main() {
    let mut v1 = vec![1, 10, 5, 1, 2, 11, 2, 40];
    v1.sort();
    v1.dedup();
    assert_eq!(v1, &[1, 2, 5, 10, 11, 40]);
}

Продолжение следует…

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023