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


Далі буде…

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

У Росії націоналізували одну з найбільших геймдев-компаній. Звинуватили в підтримці ЗСУ

Таганський суд Москви ухвалив рішення про передачу у власність держави 100% уставного капіталу IT-компанії «Леста…

04.06.2025

Adobe випустила бета-версію Photoshop для Android

Компанія Adobe оголосила про випуск бета-версії мобільного додатку Photoshop для платформи Android. Реліз стався через…

03.06.2025

Користувачам Windows дозволять видалили Microsoft Store і перестануть нав’язувати Edge — але не всім

Microsoft оголосила, що внесе у Windows деякі зміни щодо роботи сторонніх додатків та сервісів. Компанія…

03.06.2025

Salesforce скорочує найм програмістів. Причина в штучному інтелекті

Завдяки інструментам на базі штучного інтелекту американський IT-гігант Salesforce скоротив найм технічних працівників, у тому…

03.06.2025

OpenAI переписує інструмент кодування Codex CLI з TypeScript на Rust

OpenAI переписала свій інструмент кодування Codex CLI з TypeScript на Rust. Причиною названо підвищення продуктивності…

03.06.2025

В Microsoft Bing інтегрували безкоштовний генератор відео від Sora

Microsoft додала відеогенератор Sora від OpenAI у свій мобільний застосунок Bing. Це перший випадок, коли…

03.06.2025