В нашей последовательной серии материалов мы рассмотрим базовые основы новомодного языка 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;
Согласитесь, получилось красиво? В этом сила применения кортежей там, где они уместны.
Продолжение следует…
Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…
Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…
Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…
В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…
Скрипт (англ. — сценарий), — это небольшая программа, как правило, для веб-интерфейса, выполняющая определенную задачу.…
Дедлайн (от англ. deadline — «крайний срок») — это конечная дата стачи проекта или задачи…