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