1. 1. Introduction
  2. 2. Введение
  3. 3. C чего начать
  4. 4. Изучение Rust
    1. 4.1. Угадайка
    2. 4.2. Обедающие философы
    3. 4.3. Вызов кода на Rust из других языков
  5. 5. Синтаксис и семантика
    1. 5.1. Связывание имён
    2. 5.2. Функции
    3. 5.3. Простые типы
    4. 5.4. Комментарии
    5. 5.5. Конструкция `if`
    6. 5.6. Циклы
    7. 5.7. Владение
    8. 5.8. Ссылки и заимствование
    9. 5.9. Время жизни
    10. 5.10. Изменяемость
    11. 5.11. Структуры
    12. 5.12. Перечисления
    13. 5.13. Конструкция `match`
    14. 5.14. Шаблоны сопоставления `match`
    15. 5.15. Синтаксис методов
    16. 5.16. Вектора
    17. 5.17. Строки
    18. 5.18. Обобщённое программирование
    19. 5.19. Типажи
    20. 5.20. Типаж `Drop`
    21. 5.21. Конструкция `if let`
    22. 5.22. Типажи-объекты
    23. 5.23. Замыкания
    24. 5.24. Универсальный синтаксис вызова функций
    25. 5.25. Контейнеры и модули
    26. 5.26. `const` и `static`
    27. 5.27. Атрибуты
    28. 5.28. Псевдонимы типов
    29. 5.29. Приведение типов
    30. 5.30. Ассоциированные типы
    31. 5.31. Безразмерные типы
    32. 5.32. Перегрузка операций
    33. 5.33. Преобразования при разыменовании
    34. 5.34. Макросы
    35. 5.35. Сырые указатели
    36. 5.36. Небезопасный код
  6. 6. Эффективное использование Rust
    1. 6.1. Стек и куча
    2. 6.2. Тестирование
    3. 6.3. Условная компиляция
    4. 6.4. Документация
    5. 6.5. Итераторы
    6. 6.6. Многозадачность
    7. 6.7. Обработка ошибок
    8. 6.8. Выбор гарантий
    9. 6.9. Интерфейс внешних функций
    10. 6.10. Типажи `Borrow` и `AsRef`
    11. 6.11. Каналы сборок
    12. 6.12. Using Rust without the standard library
  7. 7. Нестабильные возможности Rust
    1. 7.1. Плагины к компилятору
    2. 7.2. Встроенный ассемблерный код
    3. 7.3. Без stdlib
    4. 7.4. Внутренние средства
    5. 7.5. Элементы языка
    6. 7.6. Продвинутое руководство по компоновке
    7. 7.7. Тесты производительности
    8. 7.8. Синтаксис упаковки и шаблоны `match`
    9. 7.9. Шаблоны `match` для срезов
    10. 7.10. Ассоциированные константы
    11. 7.11. Пользовательские менеджеры памяти
  8. 8. Глоссарий
  9. 9. Syntax Index
  10. 10. Библиография

Строки

Строки — важное понятие для любого программиста. Система обработки строк в Rust немного отличается от других языков, потому что это язык системного программирования. Работать со структурами данных с переменным размером довольно сложно, и строки — как раз такая структура данных. Кроме того, работа со строками в Rust также отличается и от некоторых системных языков, таких как C.

Давайте разбираться в деталях. string — это последовательность скалярных значений юникод, закодированных в виде потока байт UTF-8. Все строки должны быть гарантированно валидными UTF-8 последовательностями. Кроме того, строки не оканчиваются нулём и могут содержать нулевые байты.

В Rust есть два основных типа строк: &str и String. Сперва поговорим о &str — это «строковый срез». Строковые срезы имеют фиксированный размер и не могут быть изменены. Они представляют собой ссылку на последовательность байт UTF-8:

let greeting = "Всем привет."; // greeting: &'static strRun

"Всем привет." — это строковый литерал, его тип — &'static str. Строковые литералы являются статически размещенными строковыми срезами. Это означает, что они сохраняются внутри нашей скомпилированной программы и существуют в течение всего периода ее выполнения. Имя greeting представляет собой ссылку на эту статически размещенную строку. Любая функция, ожидающая строковый срез, может также принять в качестве аргумента строковый литерал.

Строковые литералы могут состоять из нескольких строк. Такие литералы можно записывать в двух разных формах. Первая будет включать в себя перевод на новую строку и ведущие пробелы:

let s = "foo
    bar";

assert_eq!("foo\n        bar", s);Run

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

let s = "foo\
    bar"; 

assert_eq!("foobar", s);Run

Но в Rust есть не только &str. Тип String представляет собой строку, размещенную в куче. Эта строка расширяема, и она также гарантированно является последовательностью UTF-8. String обычно создаётся путем преобразования из строкового среза с использованием метода to_string.

let mut s = "Привет".to_string(); // mut s: String
println!("{}", s);

s.push_str(", мир.");
println!("{}", s);Run

String преобразуются в &str с помощью &:

fn takes_slice(slice: &str) {
    println!("Получили: {}", slice);
}

fn main() {
    let s = "Привет".to_string();
    takes_slice(&s);
}Run

Это преобразование не происходит в случае функций, которые принимают какой-то типаж &str, а не сам &str. Например, у метода TcpStream::connect есть параметр типа ToSocketAddrs. Сюда можно передать &str, но String нужно явно преобразовать с помощью &*.

use std::net::TcpStream;

TcpStream::connect("192.168.0.1:3000"); // параметр &str

let addr_string = "192.168.0.1:3000".to_string();
TcpStream::connect(&*addr_string); // преобразуем addr_string в &strRun

Представление String как &str — дешёвая операция, но преобразование &str в String предполагает выделение памяти. Не стоит делать это без необходимости!

Индексация

Поскольку строки являются валидными UTF-8 последовательностями, то они не поддерживают индексацию:

let s = "привет";

println!("Первая буква s — {}", s[0]); // ОШИБКА!!!Run

Как правило, доступ к вектору с помощью [] является очень быстрой операцией. Но поскольку каждый символ в строке, закодированной UTF-8, может быть представлен несколькими байтами, то при поиске вы должны перебрать n-ое количество литер в строке. Это значительно более дорогая операция, а мы не хотим вводить в заблуждение. Кроме того, «литера» — это не совсем то, что определено в Unicode. Мы можем выбрать как рассматривать строку: как отдельные байты или как кодовые единицы (codepoints):

let hachiko = "忠犬ハチ公";

for b in hachiko.as_bytes() {
    print!("{}, ", b);
}

println!("");

for c in hachiko.chars() {
    print!("{}, ", c);
}

println!("");Run

Этот код напечатает:

229, 191, 160, 231, 138, 172, 227, 131, 143, 227, 131, 129, 229, 133, 172, 
忠, 犬, ハ, チ, 公, 

Как вы можете видеть, количество байт больше, чем количество символов (char).

Вы можете получить что-то наподобие индекса, как показано ниже:

let dog = hachiko.chars().nth(1); // что-то вроде hachiko[1]Run

Это подчеркивает, что мы должны пройти по списку chars от его начала.

Срезы

Вы можете получить срез строки с помощью синтаксиса срезов:

let dog = "hachiko";
let hachi = &dog[0..5];Run

Но заметьте, что это индексы байтов, а не символов. Поэтому этот код запаникует:

let dog = "忠犬ハチ公";
let hachi = &dog[0..2];Run

с такой ошибкой:

thread '<main>' panicked at 'index 0 and/or 2 in `忠犬ハチ公` do not lie on
character boundary'

Конкатенация

Если у вас есть String, то вы можете присоединить к нему в конец &str:

let hello = "Hello ".to_string();
let world = "world!";

let hello_world = hello + world;Run

Но если у вас есть две String, то необходимо использовать &:

let hello = "Hello ".to_string();
let world = "world!".to_string();

let hello_world = hello + &world;Run

Это потому, что &String может быть автоматически приведен к &str. Эта возможность называется «Приведение при разыменовании».