Основы Rust: еще раз о переменных и присвоении

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

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

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

Еще раз о переменных и присвоении

А теперь немного вернемся к основам и посмотрим на кое-что удивительное:

// move1.rs
fn main() {
    let s1 = "hello dolly".to_string();
    let s2 = s1;
    println!("s1 {}", s1);
}

Здесь мы получаем следующую ошибку:

error[E0382]: use of moved value: `s1`
 --> move1.rs:5:22
  |
4 |     let s2 = s1;
  |         -- value moved here
5 |     println!("s1 {}", s1);
  |                      ^^ value used here after move
  |
  = note: move occurs because `s1` has type `std::string::String`,
  which does not implement the `Copy` trait

Примечание: перемещение происходит потому, что `s1` имеет тип `std::string::String`, который не реализует признак `Copy`.

Rust ведет себя иначе, чем другие языки. В языке, где переменные всегда являются ссылками (например, Java или Python), s2 становится еще одной ссылкой на строковый объект, на который ссылается s1. В C++ s1 — это значение, и оно копируется в s2. Но Rust перемещает значение. Он не рассматривает строки как копируемые (технически правильней говорить: не реализует признак Copy).

Мы не увидим этого с «примитивными» типами, такими как числа, поскольку это просто значения. Им разрешено быть копируемыми, потому что их копировать «дешево». Но String выделил память, содержащую «Hello dolly», и копирование будет включать выделение еще некоторой памяти и копирование символов.

Рассмотрим String, содержащий весь текст «Моби Дика». Это небольшая структура, в ней только адрес текста в памяти, его размер и размер выделенного блока. Копирование будет дорогостоящим, потому что память выделяется в куче, а для копии потребуется собственный выделенный блок.

String
    | addr | ---------> Call me Ishmael.....
    | size |                    |
    | cap  |                    |
                                |
    &str                        |
    | addr | -------------------|
    | size |

    f64
    | 8 bytes |

Второе значение — это фрагмент строки (&str), который ссылается на ту же память, что и первая строка, с указанным размером — это просто имя рассказчика.

Третье значение — это f64 — всего 8 байт. Оно не ссылается ни на какую другую память, поэтому его так же дешево копировать, как и перемещать.

Копируемые значения определяются только их представлением в памяти, и когда Rust копирует, он просто копирует эти байты в другое место. Аналогично, значение, не являющееся копией, так же просто перемещается. В Rust в копировании и перемещении нет никакой хитрости, в отличие от C++.

Переписывание с помощью вызова функции приводит к точно такой же ошибке:

// move2.rs

fn dump(s: String) {
    println!("{}", s);
}

fn main() {
    let s1 = "hello dolly".to_string();
    dump(s1);
    println!("s1 {}", s1); // <---error: 'value used here after move'
}

Здесь у вас есть выбор. Вы можете передать ссылку на эту строку или явно скопировать ее с помощью метода clone. Как правило, первый способ лучше.

fn dump(s: &String) {
    println!("{}", s);
}

fn main() {
    let s1 = "hello dolly".to_string();
    dump(&s1);
    println!("s1 {}", s1);
}

Ну вот, ошибка исчезает. Но вы редко встретите такую ссылку на обычную строку, поскольку передача строкового литерала очень некрасива и требует создания временной строки:

dump(&"hello world".to_string());

Поэтому в целом лучший способ объявить эту функцию это:

fn dump(s: &str) {
    println!("{}", s);
}

А вот тут и dump(&s1), и dump("hello world") работают правильно. Здесь вступает в действие Deref, и Rust преобразует &String в &str за вас.

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

Область применения переменных

Итак, эмпирическое правило состоит в том, чтобы предпочитать сохранять ссылки на исходные данные — то есть «заимствовать» их.

Но ссылка не должна переживать возраст своего владельца!

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

{
    let a = 10;
    let b = "hello";
    {
        let c = "hello".to_string();
        // a,b and c are visible
    }
    // the string c is dropped
    // a,b are visible
    for i in 0..a {
        let b = &b[1..];
        // original b is no longer visible - it is shadowed.
    }
    // the slice b is dropped
    // i is _not_ visible!
}

Переменные цикла (например, i) немного отличаются, они видны только в блоке цикла. Создание новой переменной с тем же именем не является ошибкой (это так называемое «shadowing»), но это может сбить с толку.

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

Еще одна специфическая для Rust проблема заключается в том, что переменная может казаться находящейся в области видимости, но ее значение переместилось.

В примере ниже ссылка rs1 сделана на значение tmp, которое живет только в течение всего блока:

// ref1.rs
02 fn main() {
03    let s1 = "hello dolly".to_string();
04    let mut rs1 = &s1;
05    {
06        let tmp = "hello world".to_string();
07        rs1 = &tmp;
08    }
09    println!("ref {}", rs1);
10 }

Мы заимствуем значение s1, а затем заимствуем значение tmp. Но значение tmp не существует вне этого блока!

error: `tmp` does not live long enough
  --> ref1.rs:8:5
   |
7  |         rs1 = &tmp;
   |                --- borrow occurs here
8  |     }
   |     ^ `tmp` dropped here while still borrowed
9  |     println!("ref {}", rs1);
10 | }
   | - borrowed value needs to live until here

Где находится теперь tmp? Исчез, умер, вернулся в Большую кучу в небе. Rust спасает вас от страшной проблемы «висящего указателя» из языка C — ссылки, указывающей на устаревшие данные, которую надо контролировать и бояться.

Кортежи

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

// tuple1.rs

fn add_mul(x: f64, y: f64) -> (f64,f64) {
    (x + y, x * y)
}

fn main() {
    let t = add_mul(2.0,10.0);

    // can debug print
    println!("t {:?}", t);

    // can 'index' the values
    println!("add {} mul {}", t.0,t.1);

    // can _extract_ values
    let (add,mul) = t;
    println!("add {} mul {}", add,mul);
}
// t (12, 20)
// add 12 mul 20
// add 12 mul 20

Кортежи могут содержать различные типы, что является их основным отличием от массивов.

let tuple = ("hello", 5, 'c');

assert_eq!(tuple.0, "hello");
assert_eq!(tuple.1, 5);
assert_eq!(tuple.2, 'c');

Они появляются в некоторых методах итераторов. Здесь enumerate похож на одноименный генератор из Python:

for t in ["zero","one","two"].iter().enumerate() {
        print!(" {} {};",t.0,t.1);
    }
    //  0 zero; 1 one; 2 two;

zip объединяет два итератора в один итератор кортежей, содержащий значения из обоих:

let names = ["ten","hundred","thousand"];
    let nums = [10,100,1000];
    for p in names.iter().zip(nums.iter()) {
        print!(" {} {};", p.0,p.1);
    }
    //  ten 10; hundred 100; thousand 1000;

Согласитесь, получилось красиво? В этом сила применения кортежей там, где они уместны.

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

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

Обучение 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