Основы 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;
Согласитесь, получилось красиво? В этом сила применения кортежей там, где они уместны.
Продолжение следует…

Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: