🦀 Rust

Métodos de cadenas — guía completa en Rust: guía detallada

Что это такое и зачем? (аналогия из реальной жизни)

Представьте, что вы получили посылку с набором кубиков LEGO. Внутри — не просто куча деталей, а уже собранная фигурка робота. Вы можете с ней играть, но что, если захочется что-то изменить? Например, заменить синий кубик на красный, добавить роботу шлем или разобрать его на части, чтобы построить космический корабль.

Строка в Rust — это и есть такой «робот из кубиков». Только кубики — это буквы, цифры, знаки препинания и пробелы. А методы строк — это набор инструментов (отвёрток, гаечных ключей и 3D-принтеров), которые позволяют вам:

  • Узнать, сколько «кубиков» в вашей строке (длину).
  • Найти, есть ли в ней определённая буква.
  • Вырезать часть строки (как будто отпилить кусок).
  • Заменить одни символы на другие.
  • Проверить, состоит ли строка только из цифр или только из букв.
💡 Важно: В Rust строки — это не просто массив символов. Это сложная структура, которая хранит данные в кодировке UTF-8. Это значит, что одна и та же строка может содержать и английские буквы, и русские, и даже иероглифы или смайлики 🦀. Методы строк — это мостик между вами и этой сложной структурой.

Зачем это нужно? Представьте, что вы пишете программу-калькулятор. Пользователь вводит «2 + 2». Компьютер получает это как строку. Чтобы вычислить результат, вам нужно разобрать эту строку: найти пробелы, найти знак «+», взять числа слева и справа. Без методов строк вам пришлось бы вручную перебирать каждую букву — это как искать иголку в стоге сена без магнита. Методы строк — ваш магнит.

Теория: как это работает изнутри

Давайте заглянем под капот. В Rust есть два основных типа строк, и их путаница — самая частая ошибка новичков.

Два типа строк: &str и String

Характеристика &str (строковый срез) String (владемая строка)
Аналогия Это как взгляд на книгу. Вы видите текст, но не можете вписать туда новое слово. Это как чистый лист в вашей тетради. Вы можете дописывать, вычёркивать и менять текст.
Где хранится? Обычно в самом коде программы (в статической памяти) или как часть другой строки. В куче (heap) — динамической памяти. Может расти и уменьшаться.
Можно ли изменить? Нет. Это неизменяемый (immutable) «кусочек» текста. Да. Это изменяемая (mutable) строка.
Как создать? let s: &str = "Привет"; let s: String = String::from("Привет");
или let s = "Привет".to_string();
⚠️ Золотое правило: Если вы пишете текст прямо в коде в кавычках ("текст") — это &str. Если вы получаете текст от пользователя, читаете файл или собираете строку из частей — скорее всего, это String. Большинство методов строк работают с обоими типами, но возвращают они чаще всего String или &str.

Как Rust хранит строку внутри?

Представьте, что строка "Привет" — это лента конвейера. Каждый символ — это ящик. Но в Rust ящики не одинакового размера! Английская буква P занимает 1 байт, а русская П — 2 байта. Смайлик 🦀 — целых 4 байта.

Поэтому Rust не позволяет просто взять и обратиться к символу по индексу, как в Python (s[0]). Почему? Потому что индекс 0 может указывать на середину буквы! Вместо этого Rust заставляет нас использовать методы, которые безопасно работают с этой «разношёрстной» кодировкой.

Синтаксис в Rust: все формы и варианты

Методы вызываются через точку после строки. Общий синтаксис:

// Для String
let mut s = String::from("пример");
s.метод(аргументы);

// Для &str
let s: &str = "пример";
s.метод(аргументы);

Некоторые методы возвращают новое значение, не меняя исходную строку. Другие (если строка объявлена как mut) изменяют её на месте.

Справочник основных методов

Метод Что делает Пример вызова Результат
len() Возвращает длину в байтах (не в символах!) "Привет".len() 12 (каждая русская буква — 2 байта)
is_empty() Проверяет, пуста ли строка "".is_empty() true
contains() Проверяет, содержит ли строка подстроку "Привет".contains("иве") true
starts_with() Проверяет, начинается ли строка с подстроки "Привет".starts_with("При") true
ends_with() Проверяет, заканчивается ли строка на подстроку "Привет".ends_with("вет") true
to_uppercase() Преобразует в ВЕРХНИЙ регистр "привет".to_uppercase() "ПРИВЕТ"
to_lowercase() Преобразует в нижний регистр "ПРИВЕТ".to_lowercase() "привет"
trim() Удаляет пробелы в начале и конце " привет ".trim() "привет"
replace() Заменяет одну подстроку на другую "кот".replace("к", "с") "сот"
split() Разбивает строку на части по разделителю "яблоко,груша".split(",") Итератор: "яблоко", "груша"
chars() Возвращает итератор по символам (char) "Привет".chars() Итератор: 'П', 'р', 'и'...
push_str() Добавляет строку в конец (только для String) s.push_str(" мир") Изменяет s
push() Добавляет один символ в конец (только для String) s.push('!') Изменяет s

Примеры кода — от простого к сложному

Пример 1: Приветствие и длина строки

// Самый простой пример: создаём строку и узнаём её длину
fn main() {
    // Создаём строку. Rust сам понимает, что это &str
    let имя = "Анна";
    
    // Вызываем метод len() — он считает БАЙТЫ
    let длина = имя.len();
    
    // Выводим результат
    println!("Привет, {}!", имя);
    println!("Длина строки в байтах: {}", длина);
    
    // А теперь посчитаем символы (char)
    let символы = имя.chars().count();
    println!("Количество символов: {}", символы);
}
// Вывод:
// Привет, Анна!
// Длина строки в байтах: 8
// Количество символов: 4

Объяснение: len() считает байты. Каждая русская буква занимает 2 байта, поэтому 4 буквы = 8 байт. chars().count() считает именно символы — их 4. Это важное различие!

Пример 2: Проверка пустоты и содержимого

fn main() {
    let пустая_строка = String::from("");
    let полная_строка = String::from("Ржавый язык");
    
    // Проверяем, пуста ли строка
    println!("Пустая строка пуста? {}", пустая_строка.is_empty());
    println!("Полная строка пуста? {}", полная_строка.is_empty());
    
    // Проверяем, содержит ли строка подстроку
    let есть_ли = полная_строка.contains("Ржа");
    println!("Содержит 'Ржа'? {}", есть_ли);
    
    // Проверяем начало и конец
    println!("Начинается на 'Ржа'? {}", полная_строка.starts_with("Ржа"));
    println!("Заканчивается на 'зык'? {}", полная_строка.ends_with("зык"));
}
// Вывод:
// Пустая строка пуста? true
// Полная строка пуста? false
// Содержит 'Ржа'? true
// Начинается на 'Ржа'? true
// Заканчивается на 'зык'? true

Объяснение: Методы contains, starts_with, ends_with очень полезны для валидации ввода. Например, проверка, что email содержит @.

Пример 3: Изменение регистра и удаление пробелов

fn main() {
    let грязный_ввод = "   ПриВет, МиР!   ";
    
    // trim() убирает пробелы по краям
    let чистый = грязный_ввод.trim();
    println!("После trim: '{}'", чистый);
    
    // to_uppercase() — всё заглавными
    let громко = чистый.to_uppercase();
    println!("Громко: '{}'", громко);
    
    // to_lowercase() — всё строчными
    let тихо = чистый.to_lowercase();
    println!("Тихо: '{}'", тихо);
}
// Вывод:
// После trim: 'ПриВет, МиР!'
// Громко: 'ПРИВЕТ, МИР!'
// Тихо: 'привет, мир!'

Объяснение: trim() — спасение при обработке пользовательского ввода. Люди часто случайно ставят пробелы. to_uppercase и to_lowercase корректно работают даже с русскими буквами.

Пример 4: Замена подстроки

fn main() {
    let предложение = String::from("Я люблю программировать на Python");
    
    // Заменяем "Python" на "Rust"
    let новое_предложение = предложение.replace("Python", "Rust");
    
    println!("Было: {}", предложение);
    println!("Стало: {}", новое_предложение);
    
    // Замена не изменяет исходную строку!
    println!("Исходная не изменилась: {}", предложение);
}
// Вывод:
// Было: Я люблю программировать на Python
// Стало: Я люблю программировать на Rust
// Исходная не изменилась: Я люблю программировать на Python

Объяснение: replace создаёт новую строку. Исходная остаётся нетронутой. Это безопасно и предсказуемо.

Пример 5: Разбиение строки на части (split)

fn main() {
    let данные = "яблоко,груша,банан,апельсин";
    
    // Разбиваем по запятой. split() возвращает итератор
    let фрукты: Vec<&str> = данные.split(",").collect();
    
    println!("Первый фрукт: {}", фрукты[0]);
    println!("Второй фрукт: {}", фрукты[1]);
    println!("Все фрукты: {:?}", фрукты);
    
    // Разбиваем по пробелу
    let слова = "один два три";
    for слово in слова.split(" ") {
        println!("Слово: {}", слово);
    }
}
// Вывод:
// Первый фрукт: яблоко
// Второй фрукт: груша
// Все фрукты: ["яблоко", "груша", "банан", "апельсин"]
// Слово: один
// Слово: два
// Слово: три

Объяснение: split — мощный инструмент для парсинга (разбора) данных. collect() собирает итератор в коллекцию (вектор).

Пример 6: Сборка строки (push и push_str)

fn main() {
    // Создаём пустую изменяемую строку
    let mut сообщение = String::new();
    
    // Добавляем строку
    сообщение.push_str("Привет");
    // Добавляем пробел
    сообщение.push(' ');
    // Добавляем ещё строку
    сообщение.push_str("мир");
    // Добавляем восклицательный знак
    сообщение.push('!');
    
    println!("Собранное сообщение: {}", сообщение);
    
    // Можно добавлять и другие строки
    сообщение.push_str(" Как дела?");
    println!("После добавления: {}", сообщение);
}
// Вывод:
// Собранное сообщение: Привет мир!
// После добавления: Привет мир! Как дела?

Объяснение: push_str добавляет строку, push — один символ. Работает только с String (изменяемой). Это эффективнее, чем склеивать через +.

Пример 7: Комбинирование методов (цепочка вызовов)

fn main() {
    let ввод = "   РуСТ — ЭТО КРУТО!   ";
    
    // Цепочка методов: trim -> to_lowercase -> replace
    let результат = ввод
        .trim()                    // Убираем пробелы
        .to_lowercase()            // Всё в нижний регистр
        .replace("круто", "замечательно"); // Замена слова
    
    println!("Результат: '{}'", результат);
    
    // Ещё пример: проверка и преобразование
    let email = "  ПОЛЬЗОВАТЕЛЬ@EXAMPLE.COM  ";
    let чистый_email = email
        .trim()
        .to_lowercase();
    
    println!("Обработанный email: '{}'", чистый_email);
    println!("Содержит '@'? {}", чистый_email.contains('@'));
}
// Вывод:
// Результат: 'руст — это замечательно!'
// Обработанный email: 'пользователь@example.com'
// Содержит '@'? true

Объяснение: Методы можно вызывать цепочкой — результат одного метода становится входом для следующего. Это делает код чистым и читаемым.

Сравнение с Python: чем похоже и чем отличается

Операция Python Rust Примечание
Длина строки len(s) s.len() В Python — функция, в Rust — метод. В Rust считает байты!
Доступ по индексу s[0] s.chars().nth(0) В Python — напрямую, в Rust — через итератор (безопасно)
Замена s.replace("a", "b") s.replace("a", "b") Почти одинаково
Разбиение s.split(",") s.split(",") Очень похоже, но в Rust возвращает итератор
Верхний/нижний регистр s.upper(), s.lower() s.to_uppercase(), s.to_lowercase() Rust использует более длинные имена
Проверка на пустоту not s или len(s) == 0 s.is_empty() В Rust — явный метод
Склейка строк "a" + "b" format!("{}{}", "a", "b") или push_str В Rust + работает сложнее (нужно владение)
Удаление пробелов s.strip() s.trim() Почти одинаково
💡 Главное отличие: Python делает вид, что строки — это просто списки символов. Rust честно говорит: «Строки — это сложно, давай делать всё безопасно». Из-за этого код на Rust может быть длиннее, но он никогда не упадёт из-за того, что вы обратились к середине символа.

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

Ошибка 1: Путаница между len() и количеством символов

fn main() {
    let s = "Привет";
    // Ошибка: думаем, что len() вернёт 6
    println!("Длина: {}", s.len()); // Выведет 12!
    
    // Правильно: используем chars().count()
    println!("Символов: {}", s.chars().count()); // Выведет 6
}

Ошибка 2: Попытка обратиться по индексу

fn main() {
    let s = String::from("Привет");
    // Ошибка компиляции! Rust не позволяет s[0]
    // let первый = s[0]; // НЕ РАБОТАЕТ!
    
    // Правильно:
    if let Some(первый) = s.chars().nth(0) {
        println!("Первый символ: {}", первый);
    }
}

Ошибка 3: Изменение неизменяемой строки

fn main() {
    let s = String::from("Привет");
    // Ошибка: s не объявлена как mut
    // s.push_str(" мир"); // НЕ РАБОТАЕТ!
    
    // Правильно:
    let mut s_mut = String::from("Привет");
    s_mut.push_str(" мир");
    println!("{}", s_mut);
}

Ошибка 4: Забыли, что replace не меняет исходную строку

fn main() {
    let mut s = String::from("кот");
    s.replace("к", "с"); // Результат не присвоен!
    println!("{}", s); // Всё ещё "кот"!
    
    // Правильно:
    s = s.replace("к", "с");
    println!("{}", s); // "сот"
}

Ошибка 5: Сравнение строк без учёта регистра

fn main() {
    let s1 = "Привет";
    let s2 = "привет";
    
    // Ошибка: сравнение учитывает регистр
    if s1 == s2 {
        println!("Одинаковы");
    } else {
        println!("Разные"); // Выведет это
    }
    
    // Правильно: приводим к одному регистру
    if s1.to_lowercase() == s2.to_lowercase() {
        println!("Одинаковы (без учёта регистра)"); // Выведет это
    }
}

Лучшие практики и советы профессионалов

  1. Всегда используйте trim() для пользовательского ввода. Пользователи непредсказуемы. Пробелы в начале и конце — частая проблема.
  2. Для сравнения строк без учёта регистра используйте to_lowercase() или to_uppercase() для обеих строк. Не надейтесь, что пользователь введёт текст так, как вы ожидаете.
  3. Для сборки строк используйте push_str или format!, а не +. Оператор + в Rust «забирает» владение первой строкой, что может привести к путанице.
  4. Не используйте len() для проверки на пустоту. Используйте is_empty() — это понятнее и эффективнее.
  5. При работе с большими текстами используйте итераторы (chars(), split()), а не собирайте всё в вектор. Это экономит память.
  6. Помните про кодировку. Если вы пишете программу, которая работает с разными языками, всегда проверяйте, как методы работают с Unicode.
  7. Используйте as_str() для преобразования String в &str. Это часто нужно для передачи строки в функции, которые ожидают &str.
🔥 Совет профи: Если вы часто работаете со строками, создайте свою «библиотеку» вспомогательных функций. Например, функцию clean_input(), которая сразу делает trim().to_lowercase(). Это сократит код и уменьшит количество ошибок.

Вопросы для самопроверки

  1. Вопрос: Чем отличается &str от String?

    Ответ: &str — это неизменяемый «взгляд» на строку (строковый срез). String — это изменяемая, владеемая строка, которая хранится в куче. &str нельзя изменить, String можно (если она mut).

  2. Вопрос: Почему в Rust нельзя обратиться к символу строки по индексу, как в Python?

    Ответ: Потому что Rust использует кодировку UTF-8, где символы занимают разное количество байт (1 для английских букв, 2 для русских, 4 для смайликов). Обращение по индексу может попасть на середину символа, что приведёт к ошибке.

  3. Вопрос: Что вернёт "Привет".len() и почему?

    Ответ: Вернёт 12. Потому что len() считает байты. Каждая из 6 русских букв занимает 2 байта. 6 × 2 = 12.

  4. Вопрос: Как правильно посчитать количество символов в строке?

    Ответ: Использовать s.chars().count(). Метод chars() возвращает итератор по символам (char), а count() подсчитывает их количество.

  5. Вопрос: Какой метод удаляет пробелы в начале и конце строки?

    Ответ: trim(). Он возвращает новую строку без пробелов по краям.

  6. Вопрос: Как разбить строку "яблоко,груша,банан" на отдельные слова?

    Ответ: Использовать "яблоко,груша,банан".split(","). Метод возвращает итератор. Чтобы получить вектор, нужно вызвать .collect().

  7. Вопрос: Как добавить текст в конец существующей строки String?

    Ответ: Использовать метод push_str() для добавления строки или push() для добавления одного символа. Строка должна быть объявлена как mut.

  8. Вопрос: В чём разница между replace в Rust и Python?

    Ответ: В синтаксисе они похожи. В обоих языках replace не изменяет исходную строку, а возвращает новую. В Rust нужно явно присвоить результат, иначе исходная строка не изменится.

  9. Вопрос: Как проверить, начинается ли строка с определённого слова?

    Ответ: Использовать метод starts_with(). Например, "Привет".starts_with("При") вернёт true.

  10. Вопрос: Что такое «цепочка методов» и зачем она нужна?

    Ответ: Это вызов нескольких методов подряд через точку. Результат одного метода становится входом для следующего. Это делает код компактным и читаемым. Пример: " Текст ".trim().to_lowercase().replace("текст", "слово").

Итог и что изучить дальше

Мы прошли долгий путь от простой аналогии с LEGO до сложных цепочек методов. Теперь вы знаете:

  • Что строки в Rust бывают двух типов: &str (неизменяемый срез) и String (изменяемая строка).
  • Как использовать основные методы: len(), is_empty(), contains()