Strings

Links:

Статьи в разделе:

В Rust строки хранятся в формате UTF-8, где каждый символ может занимать от 1 до 4 байт. Поэтому индексация идёт не по символам напрямую, а по байтам или с учётом корректных границ символов (Unicode scalar values).

Пример строкового литерала:

let s = "Hello, Rust!"; // Обычная строка, строковый литерал
let raw = r#"Сырой текст с "кавычками""#; // Сырая строка без экранирования

String

Тип данных с владельцем. Имеет изменяемый размер, неизвестный в момент компиляции. Представляет собой векторную структуру:

pub struct String { vec: Vec<u8>; } // для ASCII символов

Поскольку структура содержит Vec, это значит, что есть указатель на массив памяти, размер строки size структуры и ёмкость capacity (сколько можно добавить к строке перед дополнительным выделением памяти под строку).

Работа с String: если у вас String, нужно сначала получить срез &str с помощью &

&str

Ссылка на часть, slice от String (куча), str (стек) или статической константы. Не имеет владельца, размер фиксирован, известен в момент компиляции.

  • &String можно неявно превращать в &str;
  • &str нельзя неявно превращать в &String.
fn main() {
    let s = "hello_world";
    let mut mut_string = String::from("hello");
    success(&mutable_string);
    fail(s); }

fn success(data: &str) { // неявный перевод &String -> &str
    println!("{}", data); }

fn fail(data: &String) { // ОШИБКА - expected &String, but found &str
    println!("{}", data); }
Warning

Пока существует &str её в области жизни нельзя менять содержимое памяти, на которое она ссылается, даже владельцем строки.

&String

Ссылка на String. Не имеет владельца, размер фиксирован, известен в момент компиляции.

fn change(mystring: &mut String) {
    if !mystring.ends_with("s") {
        mystring.push_str("s");   // добавляем "s" в конец исходной строки
    }

str

Набор символов (литералов), размещённых на стеке. Не имеет владельца, размер фиксирован, известен в момент компиляции. Можно превращать str в String через признак from:

let text = String::from("TEST"); // "TEST" :str

Строковые константы

const CONST_STRING: &'static str = "a constant string"; 

Примеры

Изменение строк

При наличии String, нужно передать ссылку &mut String для изменения:

fn main() {
 let mut mutable_string = String::from("hello ");
 do_mutation(&mut mutable_string);
 println!("{}", mutables_string); // hello world!
}

fn do_mutation(input: &mut String) {
 input.push_str("world!");
}

Строки с владением

Получение String с передачей владения нужно при получении строки от функции, передача в поток (thread):

fn main() {
    let s = "hello_world";
    println!("{}", do_upper(s)); } // HELLO_WORLD

fn do_upper(input: &str) -> String { // возврат String
    input.to_ascii_uppercase() }

Структуры

Если структуре надо владеть своими данными - использовать String. Если нет, можно использовать &str, но нужно указать время жизни (lifetime), чтобы структура не пережила взятую ей строку:

struct Owned { bla: String, }
struct Borrowed<'a> { bla: &'a str, }

fn main() {
    let o = Owned {
        bla: String::from("bla"), };
    let b = create_something(&o.bla); }

fn create_something(other_bla: &str) -> Borrowed {
    let b = Borrowed { bla: other_bla };
    b // при возврате Borrowed, переменная всё ещё в области действия!
}

Разница между to_lowercase(), to_ascii_lowercase(), make_ascii_lowercase() и upper-аналогов

Функцияto_uppercase()to_ascii_uppercase()make_ascii_uppercase()
ВозвращаетНовая StringНовая String() (меняет входную строку)
ТекстUnicodeтолько ASCIIтолько ASCII
ПамятьНовая строкаНовая строкаНе выделяет память
SpeedМедленноБыстроБыстрее всех
ОсобенностиОбработка спец-символов (ß→SS)Нет, пропускает спец-символыНет, пропускает спец-символы

Передача любых строк в функцию с переводом по ссылке

Можно воспользоваться встроенным типажом AsRef для перевода ссылок строки всех видов (String&strBox<str> и т.д.) в &str:

// AsRef<str> is a trait that allows cheap reference-to-reference conversion — here it guarantees we can get a `&str` from a value of type `N`:
fn hello<N: AsRef<str>>(name: N) {
    println!("Hello, {}!", name.as_ref());
    // .as_ref() is from the `AsRef` trait — it converts `&name` into a `&str`.
}

fn main() {
    let strings: Vec<String> = vec!["Alice".into(), "Bob".into()];
    // "Alice".into() равен String::from("Alice")
    
    strings.into_iter().for_each(hello);
    hello("Charlie");
    hello(&String::from("Debbie"));
    hello(String::from("Evan").as_str());
}

Arc str против String

Link: https://www.youtube.com/watch?v=A4cKi7PTJSs&t=822s

Arc<[T]> или он же Arc<str> является предпочтительным вариантом перед Vec<T> (он же String), в случае неизменяемых данных:

  • Дешёвое клонирование, сложность O(1)
  • Меньше размер (16 байт против 24 байт)
  • Включает в себя Deref<[T]>, что позволяет использовать все те же возможности: len(), итерировать, индексировать

Другие особенности:

  • В случае синхронного кода без потоков ещё быстрее-удобнее Rc<str>;
  • Arc<String> не то же самое, что Arc<str> - более сложно и требует двойной указатель, чтобы добраться до данных.

Subsections of Strings

Chars and Substrings

Буквы (char)

Перевод букв в числа методом to_digit(RADIX) (RADIX=10 в десятичной системе) и их сумма:

fn main() {
    const RADIX: u32 = 10;
    let x = "134";
    println!("{}", x.chars().map(|c| c.to_digit(RADIX).unwrap()).sum::<u32>()); }

Перевод букв в коды ASCII и обратно:

println!("{}", 'a' as u8); // перевод символа в код ASCII
println!("{}", 97 as char); // число как символ

// перевод кода UTF-8 в символ (не все символы можно перевести):
println!("{:?}", std::char::from_u32(98).unwrap()); 

Первая и последняя буква в строке

Чтобы проверить или изменить 1-ую букву в строке (в том числе иероглиф или иной вариант алфавита), нужно строку переделать в вектор из букв:

    let char_vec: Vec<char> = text.chars().collect();
    if char_vec[0].is_lowercase() { .. }

Для последней буквы есть метод last(), вешается на итератор chars(), возвращает Option<char>, так как последней буквы может и не быть:

println!("{}", "foobar".chars().last().unwrap()); // 'r'

Гласные / согласные буквы

Проверку нужно написать в виде функции:

fn is_vowel(c: char) -> bool {  
    c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' ||  
    c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U' }

let text = String::from("Aria");

Сортировка букв в словах

Есть функции sort() и sort_unstable() (работает быстрее, но равные элементы может перемешивать) для вектора символов Vec<char>. Обе функции делают сортировку на месте, возвращают ().

let input = "zxyabc"; // сортировка Unicode тоже работает
let mut char2vec_iter = input.chars()
    .collect::<Vec<char>>();
char2vec_iter.sort_unstable(); // сортировка на месте
// char2vec_iter.sort_by(|a,b| b.cmp(a)); // реверс-сортировка

// собираем назад String из Vec<char>
let sorted_string: String = char2vec_iter.into_iter().collect();
println!("{}", sorted_string); // "abcxyz"

chunks() и chunks_exact() разбиение строк на части

Аналогично массивам, строки можно разбить на части:

let text = "ПриветМир";
let chars = text.chars().collect::<Vec<char>>();

println!("Разбиваем строку на части по 3 символа:");
for chunk in chars.chunks(3) {
    let s: String = chunk.iter().collect();
    println!("{}", s); 
// При
// вет
// Мир

Строковые срезы

Срезы позволяют взять часть строки, указав диапазон байтовых индексов.

  • Синтаксис&string[start..end] (где start — начало, end — конец, не включительно).
  • Особенность: нужно вручную следить за границами байтов, иначе будет паника при попытке разрезать строку посреди многобайтового символа.
let text = "Hello, world!"; // тип &str
let text = String::from("Hello, world!"); // String
// Если у вас String, а не &str, нужно взять срез с помощью &:
let first_three = &text[0..3];        // первые 3 символа
let last_five = &text[text.len()-5..]; // последние 5 символов
println!("{}", first_three); // "Hel"
println!("{}", last_five);   // "orld!"

Отрицательные индексы в Rust не поддерживаются, поэтому нужно вычислять вручную. Если указать только начало ([start..]), берётся всё до конца:

let world_and_more = &text[7..];
println!("{}", world_and_more); // "world!"

Если строка содержит многобайтовые символы (например, кириллицу или эмодзи), простые байтовые срезы могут вызвать панику. Для работы с символами (Unicode scalar values) используйте метод .chars():

let text = "Привет, мир!";
let chars: Vec = text.chars().collect(); // преобразуем в вектор символов
let privet: String = chars[0..6].iter().collect(); // собираем первые 6 символов
println!("{}", privet); // "Привет"

Метод .chars() возвращает итератор по символам, а .collect() собирает их в нужный тип (например, String).

.get(start..end) для безопасного извлечения

Если нет уверенности в границах, и надо избежать паники, подойдёт метод .get() вместо прямого среза. Он возвращает Option<&str>:

let text = "Hello, world!";
if let Some(substr) = text.get(0..5) {
    println!("{}", substr); // "Hello"
} else {
    println!("Ошибка: неверные границы"); }

Библиотека substring 

Для более прямого аналога substr можно использовать стороннюю библиотеку substring из crates.io. Добавьте в Cargo.toml:

[dependencies]
substring = "1.4.5"

Пример использования:

use substring::Substring;

let text = "Hello, world!";
let hello = text.substring(0, 5);
println!("{}", hello); // "Hello"
let world = text.substring(7, 12);
println!("{}", world); // "world"
  • Преимущество: проще для новичков, не нужно беспокоиться о байтовых границах.
  • Недостаток: добавляет внешнюю зависимость.

Примеры

Разворот букв в словах

Дана строка с пробелами между словами. Необходимо развернуть слова в строке наоборот, при этом сохранить пробелы.

fn reverse_words_split(str: &str) -> String {  
    str.to_string()
    .split(" ") // при разделении split() множественные пробелы сохраняются
    .map(|x| x.chars().rev().collect::<String>()) // разворот слов+сбор в строку
    .collect::<Vec<String>>().                    // сбор всего в вектор
    .join(" ")                                    // превращение вектора в строку
}

fn main() {  
    let word: &str = "The   quick brown fox jumps over the lazy dog.";  
    println!("{}",reverse_words_split(&word));  
}

// ehT   kciuq nworb xof spmuj revo eht yzal .god

String Iterators

Отображение части строки

Передавать владельца не нужно, передаём в &str:

let s = String::from("Hello World!");
let word = first_word(&s);
println!("The first word is: {}", word);
}

fn first_word(s: &String) -> &str { // передача строки по ссылке
let word_count = s.as_bytes();

for (i, &item) in word_count.iter().enumerate() {
    if item == b' ' {
    return &s[..i]; // возврат части строки как &str
    }
}
&s[..]  // обязательно указать возвращаемое значение, если условие в цикле выше ничего не вернёт (например, строка не содержит пробелов = вернуть всю строку)

‘Проход’ по строке итератором

Можно пройти по строке итератором chars() и его методами взятия N-го символа nth() спереди или nth_back() сзади:

let person_name = String::from("Alice");  
println!("The last character of string is: {}", match person_name.chars().nth_back(0) {  // ищем 1-ый символ с конца строки
        Some(i) => i.to_string(),  // если находим - превращаем в строку
        None => "Nothing found!".to_string(),  // не находим = сообщаем
    });  

matches() и rmatches(),

Возвращают итератор с теми частями строки, которые совпадают с заданным шаблоном:

let v: Vec<&str> = "abcXXXabcYYYabc".matches("abc").collect();  
assert_eq!(v, ["abc", "abc", "abc"]); // вывод слева-направо
  
let v: Vec<&str> = "1abc2abc3".rmatches(char::is_numeric).collect();  
assert_eq!(v, ["3", "2", "1"]); // вывод справа-налево

find() и rfind()

Возвращает Option<байт индекс 1го символа в строке слева-направо>, совпадающий с шаблоном. Либо возвращает None, если символ отсутствует в строке. rfind

fn duplicate_encode2(word: &str) -> String {
    let s = String::from(word).to_lowercase();
    s.chars()
        .map(|c| if s.find(c) == s.rfind(c) { '(' } else { ')' })
        .collect() } // если у символа есть дубли => замена на '(', 
                     // иначе на ')'

fn main() {
    println!("{}", duplicate_encode("rEcede"));
}

Примеры

Повтор части строки n раз

Новый подход использует std::repeat

fn main() {
    let repeated = "Repeat".repeat(4);
    println!("{}", repeated); // RepeatRepeatRepeatRepeat
} 

Старый вариант через итератор - позволяет бесконечно отдавать любое значение (как generic):

use std::iter;

fn main() {
    let repeated: String = iter::repeat("Repeat").take(4).collect();
    println!("{}", repeated);
}

Удаление пробелов в строке String

Use split(' '), filter out empty entries then re-join by space:

s.trim()
    .split(' ')
    .filter(|s| !s.is_empty())
    .collect::<Vec<_>>()
    .join(" ")

// Using itertools:
use itertools::Itertools;
s.trim().split(' ').filter(|s| !s.is_empty()).join(" ")

// Using split_whitespace, allocating a vector & string
pub fn trim_whitespace_v1(s: &str) -> String {
    let words: Vec<_> = s.split_whitespace().collect();
    words.join(" ")
}

Озаглавить каждое слово в предложении

В заданной фразе озаглавить каждое слово. Если результат больше 140 символов или пустой, вернуть None:

fn capitalize_first_letter(s: &str) -> Option<String> {
    let res = s
        .split_whitespace()
        .map(capital) // каждое слово передать в функцию capital()
        .collect::<Vec<String>>() // собрать в вектор
        .join(" "); // потому что вектор можно собрать в string с join()
    if res.len() < 141 || !res.is_empty() { // проверка длины
        Some(res)
    } else { None } }

fn capital(word: &str) -> String {
    let mut lword = word.to_ascii_lowercase();
    // изменить НА МЕСТЕ - прямо в этой строке (быстрее всего):
    lword[0..1].make_ascii_uppercase(); 
    lword // вернуть итоговую строку
}

String Methods

Вывод строк

  • Макрос println! позволяет вывести строку в поток stdout;
// println!("Hello there!\n"); 
// раскрывается в такой код:
use std::io::{self, Write};
io::stdout().lock().write_all(b"Hello there!\n").unwrap();
  • Макрос dbg!() позволяет вывести переменные и структуры в поток stdout;

split()

Метод split: разбивает строку на части по указанному разделителю и возвращает итератор. Разделитель может быть символом, строкой или даже пробелом. В том числе можно делить по нескольким символам разом:

let text = String::from("the_stealth-warrior");
let parts = text.split(['-', '_']).collect::<Vec<&str>>();
// collect собирает в коллекцию типа вектор
for part in parts {
    println!("{}", part);

Другие методы разбивки

split_whitespace()

Разбивает по любым пробельным символам (пробелы, табы, переносы строк).

let text = "apple   banana\ncherry";
let words: Vec<&str> = text.split_whitespace().collect();
println!("{:?}", words); // ["apple", "banana", "cherry"]

splitn(n, delimiter)

Разбивает только на первые n частей:

let text = "a,b,c,d";
let parts: Vec<&str> = text.splitn(3, ",").collect();
println!("{:?}", parts); // ["a", "b", "c,d"]

Склеивание строк (конкатенация)

В Rust надо учитывать, что String владеет памятью, а &str — нет.

Оператор +

String + &str работает, но забирает владение у первой строки.

let s1 = String::from("hello");
let s2 = " world";
let res = s1 + s2; // res == "hello world", s1 больше нельзя использовать

push_str()

Метод push_str() добавляет &str к существующей String, не забирая владение (строка должна быть mut).

let mut s = String::from("hello");
s.push_str(" world"); // s == "hello world"

push()

Добавляет один символ:

let mut s = String::from("hello");
s.push('!'); // s == "hello!"

join()

Склеивает коллекцию (вектор) строк с разделителем:

let words = vec!["apple", "banana", "cherry"];
let res = words.join(", "); // res == "apple, banana, cherry"

format!()

Мощный способ объединять строки. Макрос format! позволяет сформировать строку и вернуть из функции;

fn output_string(t: &String) -> String {  
    format!("Hello, {}",t)   // возврат сформированной строки  
}

При этом format!() позволяет конвертировать формат чисел. Decimal -> HEX:

fn rgb(r: i32, g: i32, b: i32) -> String {
    format!(
        "{:02X}{:02X}{:02X}", // конвертация dec -> 2 символа UPPER hex
        // {:02x} => конвертация в lower hex. 
        r.clamp(0, 255), // clamp задаёт валидный диапазон чисел
        g.clamp(0, 255), // аналог == g.min(255).max(0)
        b.clamp(0, 255))}

fn main() {
    println!("{}", rgb(1, 2, 3)); // 010203
    println!("{}", rgb(255, 255, 255)); // FFFFFF
    println!("{}", rgb(-20, 275, 125)); // 00FF7D
}

Decimal -> Binary:

let b = format!("{:b}", 42);
println!("{}", b); // 101010

Другие популярные методы работы со строками

  • Метод len() выдаёт длину строки в байтах;
  • Метод is_empty() проверят, что строка непустая;
  • Метод contains() ищет одну строку в другой строке;
  • Метод replace(from,to) заменяет часть строки на другую и выдаёт результат;
fn main() {    
 let mut a = String::from("Wonderful RUST World");  
 println!("Hello{}!", output_string(&a));  // вывод строки  
 println!("String is empty? {}", a.is_empty());  
 println!("String length: {}", a.len());  
 println!("Does string contain 'Hello'? {}", a.contains("Hello")); 
}   

Пример задачи

Нахождение закономерностей в структурах со строками

В примере мы передаём вектор из строк. Далее, анализируем его по частям:

fn likes(names: &[&str]) -> String {
    match names {
        [] => "no one likes this".to_string(),
        [a] => format!("{} likes this", a),
        [a, b] => format!("{} and {} like this", a, b),
        [a, b, c] => format!("{}, {} and {} like this", a, b, c),
        [a, b, other @ ..] => format!("{}, {} and {} others like this", a, b, other.len()),
    }
}