Rust字符串方法完全指南——详细指南
Что это такое и зачем? (аналогия из реальной жизни)
Представьте, что вы получили посылку с набором кубиков 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!("Одинаковы (без учёта регистра)"); // Выведет это
}
}
Лучшие практики и советы профессионалов
- Всегда используйте
trim()для пользовательского ввода. Пользователи непредсказуемы. Пробелы в начале и конце — частая проблема. - Для сравнения строк без учёта регистра используйте
to_lowercase()илиto_uppercase()для обеих строк. Не надейтесь, что пользователь введёт текст так, как вы ожидаете. - Для сборки строк используйте
push_strилиformat!, а не+. Оператор+в Rust «забирает» владение первой строкой, что может привести к путанице. - Не используйте
len()для проверки на пустоту. Используйтеis_empty()— это понятнее и эффективнее. - При работе с большими текстами используйте итераторы (
chars(),split()), а не собирайте всё в вектор. Это экономит память. - Помните про кодировку. Если вы пишете программу, которая работает с разными языками, всегда проверяйте, как методы работают с Unicode.
- Используйте
as_str()для преобразованияStringв&str. Это часто нужно для передачи строки в функции, которые ожидают&str.
🔥 Совет профи: Если вы часто работаете со строками, создайте свою «библиотеку» вспомогательных функций. Например, функциюclean_input(), которая сразу делаетtrim().to_lowercase(). Это сократит код и уменьшит количество ошибок.
Вопросы для самопроверки
-
Вопрос: Чем отличается
&strотString?Ответ:
&str— это неизменяемый «взгляд» на строку (строковый срез).String— это изменяемая, владеемая строка, которая хранится в куче.&strнельзя изменить,Stringможно (если онаmut). -
Вопрос: Почему в Rust нельзя обратиться к символу строки по индексу, как в Python?
Ответ: Потому что Rust использует кодировку UTF-8, где символы занимают разное количество байт (1 для английских букв, 2 для русских, 4 для смайликов). Обращение по индексу может попасть на середину символа, что приведёт к ошибке.
-
Вопрос: Что вернёт
"Привет".len()и почему?Ответ: Вернёт 12. Потому что
len()считает байты. Каждая из 6 русских букв занимает 2 байта. 6 × 2 = 12. -
Вопрос: Как правильно посчитать количество символов в строке?
Ответ: Использовать
s.chars().count(). Методchars()возвращает итератор по символам (char), аcount()подсчитывает их количество. -
Вопрос: Какой метод удаляет пробелы в начале и конце строки?
Ответ:
trim(). Он возвращает новую строку без пробелов по краям. -
Вопрос: Как разбить строку "яблоко,груша,банан" на отдельные слова?
Ответ: Использовать
"яблоко,груша,банан".split(","). Метод возвращает итератор. Чтобы получить вектор, нужно вызвать.collect(). -
Вопрос: Как добавить текст в конец существующей строки
String?Ответ: Использовать метод
push_str()для добавления строки илиpush()для добавления одного символа. Строка должна быть объявлена какmut. -
Вопрос: В чём разница между
replaceв Rust и Python?Ответ: В синтаксисе они похожи. В обоих языках
replaceне изменяет исходную строку, а возвращает новую. В Rust нужно явно присвоить результат, иначе исходная строка не изменится. -
Вопрос: Как проверить, начинается ли строка с определённого слова?
Ответ: Использовать метод
starts_with(). Например,"Привет".starts_with("При")вернётtrue. -
Вопрос: Что такое «цепочка методов» и зачем она нужна?
Ответ: Это вызов нескольких методов подряд через точку. Результат одного метода становится входом для следующего. Это делает код компактным и читаемым. Пример:
" Текст ".trim().to_lowercase().replace("текст", "слово").
Итог и что изучить дальше
Мы прошли долгий путь от простой аналогии с LEGO до сложных цепочек методов. Теперь вы знаете:
- Что строки в Rust бывают двух типов:
&str(неизменяемый срез) иString(изменяемая строка). - Как использовать основные методы:
len(),is_empty(),contains()