Основы 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", };
Продолжение следует…
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: