% Строки
Строки — важное понятие для любого программиста. Система обработки строк в Rust немного отличается от других языков, потому что это язык системного программирования. Работать со структурами данных с переменным размером довольно сложно, и строки — как раз такая структура данных. Кроме того, работа со строками в Rust также отличается и от некоторых системных языков, таких как C.
Давайте разбираться в деталях. string — это последовательность скалярных значений юникод, закодированных в виде потока байт UTF-8. Все строки должны быть гарантированно валидными UTF-8 последовательностями. Кроме того, строки не оканчиваются нулём и могут содержать нулевые байты.
В Rust есть два основных типа строк: &str
и String
. Сперва поговорим о
&str
— это «строковый срез». Строковые срезы имеют фиксированный размер и
не могут быть изменены. Они представляют собой ссылку на последовательность
байт UTF-8:
#![allow(unused)] fn main() { let greeting = "Всем привет."; // greeting: &'static str }
"Всем привет."
— это строковый литерал, его тип — &'static str
.
Строковые литералы являются статически размещенными строковыми срезами. Это
означает, что они сохраняются внутри нашей скомпилированной программы и
существуют в течение всего периода ее выполнения. Имя greeting
представляет
собой ссылку на эту статически размещенную строку. Любая функция, ожидающая
строковый срез, может также принять в качестве аргумента строковый литерал.
Строковые литералы могут состоять из нескольких строк. Такие литералы можно записывать в двух разных формах. Первая будет включать в себя перевод на новую строку и ведущие пробелы:
#![allow(unused)] fn main() { let s = "foo bar"; assert_eq!("foo\n bar", s); }
Вторая форма, включающая в себя \
, вырезает пробелы и перевод на новую строку:
#![allow(unused)] fn main() { let s = "foo\ bar"; assert_eq!("foobar", s); }
Но в Rust есть не только &str
. Тип String
представляет собой строку,
размещенную в куче. Эта строка расширяема, и она также гарантированно является
последовательностью UTF-8. String
обычно создаётся путем преобразования из
строкового среза с использованием метода to_string
.
#![allow(unused)] fn main() { let mut s = "Привет".to_string(); // mut s: String println!("{}", s); s.push_str(", мир."); println!("{}", s); }
String
преобразуются в &str
с помощью &
:
fn takes_slice(slice: &str) { println!("Получили: {}", slice); } fn main() { let s = "Привет".to_string(); takes_slice(&s); }
Это преобразование не происходит в случае функций, которые принимают какой-то
типаж &str
, а не сам &str
. Например, у метода
TcpStream::connect
есть параметр типа ToSocketAddrs
. Сюда можно
передать &str
, но String
нужно явно преобразовать с помощью &*
.
#![allow(unused)] fn main() { 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 в &str }
Представление String
как &str
— дешёвая операция, но преобразование &str
в String
предполагает выделение памяти. Не стоит делать это без необходимости!
Индексация
Поскольку строки являются валидными UTF-8 последовательностями, то они не поддерживают индексацию:
let s = "привет";
println!("Первая буква s — {}", s[0]); // ОШИБКА!!!
Как правило, доступ к вектору с помощью []
является очень быстрой операцией.
Но поскольку каждый символ в строке, закодированной UTF-8, может быть
представлен несколькими байтами, то при поиске вы должны перебрать n-ое
количество литер в строке. Это значительно более дорогая операция, а мы не хотим
вводить в заблуждение. Кроме того, «литера» — это не совсем то, что определено в
Unicode. Мы можем выбрать как рассматривать строку: как отдельные байты или как
кодовые единицы (codepoints):
#![allow(unused)] fn main() { let hachiko = "忠犬ハチ公"; for b in hachiko.as_bytes() { print!("{}, ", b); } println!(""); for c in hachiko.chars() { print!("{}, ", c); } println!(""); }
Этот код напечатает:
229, 191, 160, 231, 138, 172, 227, 131, 143, 227, 131, 129, 229, 133, 172,
忠, 犬, ハ, チ, 公,
Как вы можете видеть, количество байт больше, чем количество символов (char
).
Вы можете получить что-то наподобие индекса, как показано ниже:
#![allow(unused)] fn main() { let hachiko = "忠犬ハチ公"; let dog = hachiko.chars().nth(1); // что-то вроде hachiko[1] }
Это подчеркивает, что мы должны пройти по списку chars
от его начала.
Срезы
Вы можете получить срез строки с помощью синтаксиса срезов:
#![allow(unused)] fn main() { let dog = "hachiko"; let hachi = &dog[0..5]; }
Но заметьте, что это индексы байтов, а не символов. Поэтому этот код запаникует:
#![allow(unused)] fn main() { let dog = "忠犬ハチ公"; let hachi = &dog[0..2]; }
с такой ошибкой:
thread '<main>' panicked at 'index 0 and/or 2 in `忠犬ハチ公` do not lie on
character boundary'
Конкатенация
Если у вас есть String
, то вы можете присоединить к нему в конец &str
:
#![allow(unused)] fn main() { let hello = "Hello ".to_string(); let world = "world!"; let hello_world = hello + world; }
Но если у вас есть две String
, то необходимо использовать &
:
#![allow(unused)] fn main() { let hello = "Hello ".to_string(); let world = "world!".to_string(); let hello_world = hello + &world; }
Это потому, что &String
может быть автоматически приведен к &str
. Эта
возможность называется «Приведение при разыменовании».