Глава 10: Работа со строками: разбиение и склеивание

Содержание: Введение Типы строк в Rust Разбиение строк (Splitting) Склеивание строк (Concatenation) Извлечения подстроки из строки (аналог substr) Различия между String и &str Типичные ошибки новичков Примеры для практики Упражнение

Введение

Строки — один из самых распространённых типов данных в программировании. В Rust есть два основных типа строк: &str (строковый срез) и String (владеющая строка).

Разбиение строк нужно, чтобы разделить текст на части (например, слова или поля в CSV). Склеивание — чтобы объединить части в одно целое (например, создать предложение из слов).

Эта глава научит вас основам работы со строками в Rust, включая их разделение и объединение, с примерами для новичков.


Типы строк в Rust

&str: неизменяемый срез строки, обычно используется для литералов (например, "hello").

String: динамическая строка, которая может расти и изменяться, хранится на куче.

Пример:

let literal = "hello"; // это &str
let owned = String::from("hello"); // это String

Разбиение строк (Splitting)

Метод split: разбивает строку на части по указанному разделителю и возвращает итератор.

Разделитель может быть символом, строкой или даже пробелом.

Пример: разбить предложение на слова:

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

Важно: collect() собирает итератор в коллекцию, например, Vec<&str>.

Другие методы разбиения:

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

let owned = String::from("one,two,three");
let parts: Vec<&str> = owned.split(",").collect();
println!("{:?}", parts); // ["one", "two", "three"]

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

В Rust строки нельзя просто складывать с помощью + бездумно, потому что String владеет памятью, а &str — нет. Нужно учитывать правила владения.

Способы склеивания:

  1. Оператор +:

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

    let s1 = String::from("hello");
    let s2 = " world";
    let result = s1 + s2; // s1 больше нельзя использовать
    println!("{}", result); // "hello world"
  2. Метод push_str: добавляет &str к существующей String, не забирая владение.
  3. let mut s = String::from("hello");
    s.push_str(" world");
    println!("{}", s); // "hello world"
  4. Метод push: добавляет один символ.
  5. let mut s = String::from("hello");
    s.push('!');
    println!("{}", s); // "hello!"
  6. Метод join: склеивает коллекцию строк с разделителем.
  7. let words = vec!["apple", "banana", "cherry"];
    let result = words.join(", ");
    println!("{}", result); // "apple, banana, cherry"
  8. Форматирование с format!: мощный способ объединять строки.
  9. let name = "Alice";
    let greeting = format!("Hello, {}!", name);
    println!("{}", greeting); // "Hello, Alice!"

Извлечения подстроки из строки (аналог substr)

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

Использование срезов ([start..end])

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

Пример:

let text = "Hello, world!";
let hello = &text[0..5]; // от 0 до 5 (не включительно)
println!("{}", hello);    // "Hello"

let world = &text[7..12]; // от 7 до 12
println!("{}", world);    // "world"

Если указать только начало ([start..]), берётся всё до конца:

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

Работа с String

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

let mut owned = String::from("Hello, world!");
let hello = &owned[0..5];
println!("{}", hello); // "Hello"

Учёт символов, а не байтов

Если строка содержит многобайтовые символы (например, кириллицу или эмодзи), простые байтовые срезы могут вызвать панику. Для работы с символами (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).

Чтобы взять подстроку по количеству символов, нужно:

  1. Преобразовать строку в .chars().
  2. Взять срез итератора.
  3. Собрать обратно в строку.

Метод .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"

Пример: замена substr

let text = "Hello, world!";
let first_three = &text[0..3];        // первые 3 символа
let last_five = &text[text.len()-5..]; // последние 5 символов
println!("{}", first_three); // "Hel"
println!("{}", last_five);   // "orld!"

Итог


Различия между String и &str

&str неизменяемый, его нельзя модифицировать (например, с помощью push_str).

String можно изменять, но для этого она должна быть объявлена с mut.

Пример:

let mut s = String::from("start");
s.push_str(" end");
println!("{}", s); // "start end"

Типичные ошибки новичков

Попытка использовать + с двумя String напрямую:

let s1 = String::from("hello");
let s2 = String::from("world");
// let result = s1 + s2; // Ошибка! Нужен &s2
let result = s1 + &s2;

Забывание про mut:

let s = String::from("hello");
// s.push_str(" world"); // Ошибка! s не mut
let mut s = String::from("hello");
s.push_str(" world"); // Работает

Примеры для практики

  1. Разбить строку "name:age:city" по ":" и вывести каждую часть:
    let data = "Alice:25:Berlin";
    let parts: Vec<&str> = data.split(":").collect();
    println!("Name: {}, Age: {}, City: {}", parts[0], parts[1], parts[2]);
  2. Склеить слова в предложение:
    let words = vec!["Rust", "is", "awesome"];
    let sentence = words.join(" ");
    println!("{}", sentence); // "Rust is awesome"

Упражнение

Напишите программу, которая:

  1. Принимает строку "first,second,third".
  2. Разбивает её по запятым.
  3. Склеивает части обратно в строку с разделителем " - ".

Ожидаемый результат: "first - second - third".