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

Этот пост — вольный перевод на русский вот этой оригинальной статьи (с нашими дополнениями в местах, где это показалось нужным), которую написал Стив Донован. Начало можно найти вот здесь, а оглавление всей серии – вот тут.
В этой части поговорим о строках и матчинге.
Строки
Строки в Rust немного сложнее, чем в других языках. Тип String, как и Vec, выделяется динамически и имеет возможность изменения размера (таким образом, он похож на std::string в C++, но не похож на неизменяемые строки в Java и Python). Но программа может содержать много строковых литералов (например, «hello»), и системный язык должен уметь хранить их статически в самом исполняемом файле.
Во встроенных микросхемах это может означать размещение их в дешевом ПЗУ, а не в дорогой оперативной памяти (для маломощных устройств оперативная память также дорога с точки зрения энергопотребления).
Поэтому системный язык должен иметь два типа строк: динамически выделенные и статические.
Так что строка «hello» не имеет типа String. Она имеет тип &str (произносится как «string slice»). Это похоже на различие между const char* и std::string в C++, только &str гораздо более интеллектуальный. На самом деле, &str и String имеют очень похожее отношение друг к другу, как &[T] к Vec<T>.
// string1.rs
fn dump(s: &str) {
println!("str '{}'", s);
}
fn main() {
let text = "hello dolly"; // the string slice
let s = text.to_string(); // it's now an allocated string
dump(text);
dump(&s);
}
Опять же, оператор borrow может превратить String в &str, так же, как Vec<T> может быть превращен в &[T].
По сути, String — это Vec<u8>, а &str — &[u8], но эти байты должны представлять собой правильный текст UTF-8. Как и в векторе, в String можно вставить символ и вытащить его из конца:
// string5.rs
fn main() {
let mut s = String::new();
// initially empty!
s.push('H');
s.push_str("ello");
s.push(' ');
s += "World!"; // short for `push_str`
// remove the last char
s.pop();
assert_eq!(s, "Hello World");
}
Можно преобразовать многие типы в строки с помощью to_string (если можно отобразить их с помощью '{}', то можно и преобразовать).
Макрос format! — очень полезный способ построения более сложных строк с использованием тех же форматированных строк, что и в println!
// string6.rs
fn array_to_str(arr: &[i32]) -> String {
let mut res = '['.to_string();
for v in arr {
res += &v.to_string();
res.push(',');
}
res.pop();
res.push(']');
res
}
fn main() {
let arr = array_to_str(&[10, 20, 30]);
let res = format!("hello {}", arr);
assert_eq!(res, "hello [10,20,30]");
}
Обратите внимание на & перед v.to_string() — оператор определен для string slice, а не для самой строки, поэтому его нужно сопоставить немного иначе.
Нотация, используемая для фрагментов, работает и со строками:
// string2.rs
fn main() {
let text = "static";
let string = "dynamic".to_string();
let text_s = &text[1..];
let string_s = &string[2..4];
println!("slices {:?} {:?}", text_s, string_s);
}
// slices "tatic" "na"
Но строки нельзя индексировать! Это связано с тем, что они используют единственную истинную кодировку UTF-8, где каждый символ может быть несколькими байтами.
// string3.rs
fn main() {
let multilingual = "Hi! ¡Hola! привет!";
for ch in multilingual.chars() {
print!("'{}' ", ch);
}
println!("");
println!("len {}", multilingual.len());
println!("count {}", multilingual.chars().count());
let maybe = multilingual.find('п');
if maybe.is_some() {
let hi = &multilingual[maybe.unwrap()..];
println!("Russian hi {}", hi);
}
}
// 'H' 'i' '!' ' ' '¡' 'H' 'o' 'l' 'a' '!' ' ' 'п' 'р' 'и' 'в' 'е' 'т' '!'
// len 25
// count 18
// Russian hi привет!
Теперь давайте разжуем — есть 25 байт, но только 18 символов! Однако, если вы используете метод типа find, то получите правильный индекс (если он найден), а затем любой string slice будет в порядке. Тип char в Rust — это 4-байтовая кодовая строчка Unicode.
Строки здесь — это не массивы символов!
String slice может «взорваться», как векторная индексация, потому что она использует смещения байтов. В этом случае строка состоит из двух байтов, поэтому попытка вытащить первый байт вызовет ошибку Unicode. Поэтому будьте осторожны: работайте со string slice только с использованием корректных смещений, полученных из строковых методов. Вот пример того, как легко можно ошибиться:
let s = "¡";
println!("{}", &s[0..1]); <-- bad, first byte of a multibyte character
Разбиение строк — еще одно популярное и полезное занятие. Метод string split_whitespace возвращает итератор, и мы выбираем, что с ним делать. Чаще всего требуется создать вектор разбитых подстрок.
Метод collect является очень общим и поэтому нуждается в некоторых подсказках о том, что он собирает — отсюда и явный тип.
let text = "the red fox and the lazy dog";
let words: Vec<&str> = text.split_whitespace().collect();
// ["the", "red", "fox", "and", "the", "lazy", "dog"]
Также можно написать это иначе, передав итератор в метод extend:
let mut words = Vec::new();
words.extend(text.split_whitespace());
В большинстве языков нам пришлось бы создавать эти отдельные строки, тогда как здесь каждый фрагмент вектора заимствуется из исходной строки. Все, что мы выделяем, — это место для хранения фрагментов.
Посмотрите на эту симпатичную двухстрочную строку ниже — там мы получаем итератор по символам и берем только те символы, которые не являются пробелом. Опять же, collect нуждается в подсказке (возможно, нам нужен вектор символов, например):
let stripped: String = text.chars()
.filter(|ch| ! ch.is_whitespace()).collect();
// theredfoxandthelazydog
Метод filter принимает closure, что в Rust-стиле означает лямбды или анонимные функции. Здесь тип аргумента ясен из контекста, поэтому явное правило смягчается.
Да, вы можете сделать это как явный цикл по символам, заталкивая возвращаемые фрагменты в изменяемый вектор, но решение выше — короче, хорошо читается (если вы привыкли к такому, конечно) и работает быстро. Но и использовать цикл — не грех, и я рекомендую вам в качестве упражнения написать и такую альтернативную версию.
Интерлюдия: Получение аргументов командной строки
До сих пор наши программы жили в блаженном неведении относительно внешнего мира, но теперь пришло время снабдить их данными.
std::env::args — это способ доступа к аргументам командной строки. Он возвращает итератор по аргументам в виде строк, включая имя программы.
// args0.rs
fn main() {
for arg in std::env::args() {
println!("'{}'", arg);
}
}
src$ rustc args0.rs
src$ ./args0 42 'hello dolly' frodo
'./args0'
'42'
'hello dolly'
'frodo'
Может быть, лучше было бы вернуть Vec? Достаточно легко использовать collect для создания вектора, используя метод skip итератора для перехода к имени программы. Переписываем:
let args: Vec<String> = std::env::args().skip(1).collect();
if args.len() > 0 { // we have args!
...
}
Тоже смотрится вполне нормально — именно так это делается в большинстве языков.
Более подходящий для Rust подход к чтению одного аргумента (вместе с разбором целочисленного значения):
// args1.rs
use std::env;
fn main() {
let first = env::args().nth(1).expect("please supply an argument");
let n: i32 = first.parse().expect("not an integer!");
// do your magic
}
nth(1) дает вам второе значение итератора, а expect — это как unwrap с читаемым сообщением. Преобразование строки в число — дело несложное, но вам необходимо указать тип значения.
Эта программа может «запаниковать», но решение вполне подходит для небольших тестовых программ. Главное, не стоит слишком увлекаться этой удобной привычкой в сложных разработках.
Матчинг
Код в string3.rs, откуда мы извлекаем русскоязычное приветствие, написан не так, как обычно. Введите критерий для матчинга:
match multilingual.find('п') {
Some(idx) => {
let hi = &multilingual[idx..];
println!("Russian hi {}", hi);
},
None => println!("couldn't find the greeting, Товарищ")
};
Здесь match состоит из нескольких шаблонов с совпадающим значением после жирной стрелки, разделенных запятыми. Удобно сначала развернуть значение из опции Option и привязать его к idx. Надо указать все возможности, поэтому нам придется работать с None.
Когда привыкнете (то есть несколько раз напечаете все это полностью), это покажется более естественным, чем явная проверка is_some, которая потребовала бы дополнительной переменной для хранения опции.
Но если вас не интересуют неудачи, то if let — ваш друг:
if let Some(idx) = multilingual.find('п') {
println!("Russian hi {}", &multilingual[idx..]);
}
Это удобно, если надо провести сравнение и вас интересует только один возможный результат.
match также может работать как известный оператор switch в C и, как и другие конструкции Rust, может возвращать значение:
let text = match n {
0 => "zero",
1 => "one",
2 => "two",
_ => "many",
};
Значение _ подобно значению default в C. Если вы его не предоставите, rustc посчитает это ошибкой. Кстати говоря, в C++ в этой ситуации худшее, что вы можете ожидать — это предупреждение, что многое говорит об обсуждаемых нами языках.
Match-операторы Rust также могут соответствовать диапазонам. Обратите внимание, что эти диапазоны имеют три точки и являются инклюзивными диапазонами, так что первое условие будет соответствовать «3»:
let text = match n {
0...3 => "small",
4...6 => "medium",
_ => "large",
};
Продолжение следует…

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