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) Нет, пропускает спец-символы Нет, пропускает спец-символы

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()),
    }
}