Основи 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",
};
Далі буде…












Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: