Основи 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!("Ukrainian hi {}", hi);
     },
     None => println!("couldn't find the greeting, Друже")
 };

Тут match складається з декількох шаблонів з збігається значенням після жирної стрілки, розділених комами. Зручно спочатку розвернути значення опції Option і прив’язати його до idx. Потрібно вказати всі можливості, тому нам доведеться працювати з None.

Коли звикнете (тобто кілька разів надрукуєте все це повністю), це здасться природнішим, ніж явна перевірка is_some, яка потребувала б додаткової змінної для зберігання опції.

Але якщо вас не цікавлять невдачі, то if let — ваш друг:

if let Some(idx) = multilingual.find('п') {
       println!("Ukrainian 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",
     };


Далі буде…

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

Айтівець Міноборони США понабирав кредитів і хотів продати рф секретну інформацію

32-річний розробник безпеки інформаційних систем Агентства національної безпеки Джарех Себастьян Далке отримав 22 роки в'язниці…

30.04.2024

Простий та дешевий. Українська Flytech запустила масове виробництво розвідувальних БПЛА ARES

Українська компанія Flytech представила розвідувальний безпілотний літальний апарат ARES. Основні його переваги — недорога ціна…

30.04.2024

Запрошуємо взяти участь у премії TechComms Award. Розкажіть про свій потужний PR-проєкт у сфері IT

MC.today разом з Асоціацією IT Ukraine і сервісом моніторингу та аналітики згадок у ЗМІ та…

30.04.2024

«Йдеться про потенціал мобілізації»: Україна не планує примусово повертати українців із ЄС

Україна не буде примусово повертати чоловіків призовного віку з-за кордону. Про це повідомила у Брюсселі…

30.04.2024

В ЗСУ з’явився жіночий підрозділ БПЛА — і вже можна проходити конкурсний відбір

В Збройних Силах України з'явився жіночий підрозділ з БПЛА. І вже проводиться конкурсний відбір до…

30.04.2024

GitHub на наступному тижні випустить Copilot Workplace — ШІ-помічника для розробників

GitHub анонсував Copilot Workspace, середовище розробки з використанням «агентів на базі Copilot». За задумкою, вони…

30.04.2024