Основы 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",
     };

 

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

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

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