% Строки

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

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

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


# #![allow(unused_variables)]
#fn main() {
let greeting = "Всем привет."; // greeting: &'static str
#}

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

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


# #![allow(unused_variables)]
#fn main() {
let s = "foo
    bar";

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

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


# #![allow(unused_variables)]
#fn main() {
let s = "foo\
    bar"; 

assert_eq!("foobar", s);
#}

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


# #![allow(unused_variables)]
#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_variables)]
#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_variables)]
#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_variables)]
#fn main() {
# let hachiko = "忠犬ハチ公";
let dog = hachiko.chars().nth(1); // что-то вроде hachiko[1]
#}

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

Срезы

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


# #![allow(unused_variables)]
#fn main() {
let dog = "hachiko";
let hachi = &dog[0..5];
#}

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


# #![allow(unused_variables)]
#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_variables)]
#fn main() {
let hello = "Hello ".to_string();
let world = "world!";

let hello_world = hello + world;
#}

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


# #![allow(unused_variables)]
#fn main() {
let hello = "Hello ".to_string();
let world = "world!".to_string();

let hello_world = hello + &world;
#}

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