Перейти к содержанию

Заметки о Rust - строка и срез строки

Posted on:28 июня 2023 г. at 10:26

Example Dynamic OG Image link Эта статья в блоге знакомит с понятиями типа String и заимствованного аналога &str, то есть срез строк, выделяя их различия. Далее приводится несколько внутренних деталей реализации.

String

String - это строка в кодировке UTF-8. Термин «владелец» означает, что переменная s типа String имеет право собственности на содержимое строки. Когда s выходит из области действия, строка владельца удаляется.


{ // Новая область
    let s: String = String::from("My string"); // "My string" принадлежит s
    ...
} // Конец области, «s» и строка владельца «My string» удаляются

В стандартной библиотеке строка определяется следующим образом:

pub struct String {
    vec: Vec<u8>,
}

Да, именно, строка - это просто вектор байтов (u8). Однако стандартная библиотечная реализация String гарантирует, что массив байтов, указанный vec, фактически содержит только действующие байты UTF-8.

&str

str - «строковый срез», последовательность байтов в памяти, которая, как известно, является допустимым текстом UTF-8. Это примитивный тип в Rust и большую часть времени используется в заимствованной форме &str.

Причина этого в том, что str не является размерным типом Sized, означает, что его размер не известен статически (во время компиляции), а компилятор Rust должен знать размер всех локальных переменных, чтобы гарантировать безопасность, предоставляемые языком Rust.

Фактически, если вы по пытаетесь создать собственную версию str:

fn main() {
    let s: &str = "My string";
    let owned_str: str = *s;
}

при построении предоставленного фрагмента кода (cargo build) компилятор возвращает следующую ошибку (протестирована на MacOS, Rust v1.70.0):

error[E0277]: the size for values of type `str` cannot be known at compilation time
 --> src/main.rs:3:9
  |
3 |     let owned_str = *s;
  |         ^^^^^^^^^ doesn't have a size known at compile-time
  |
  = help: the trait `Sized` is not implemented for `str`
  = note: all local variables must have a statically known size
  = help: unsized locals are gated as an unstable feature

For more information about this error, try `rustc --explain E0277`.

То есть str не реализует признак Size и поэтому его размер не известен статически во время компиляции.

Заимствованная форма &str вместо этого является просто ссылкой, и ее размер всегда известен статически во время компиляции. Более точно &str является «толстым указателем», т.е. ссылкой на тип динамического размера (DST) (str), который содержит как указатель на данные, так и некоторую информацию, которая делает DST «полным» (т.е. длину str).

Это можно увидеть на простом примере. Рассмотрим следующий код:

fn foo(s: &str) {
    println!("{}", s);
}

fn main() {
    let s = String::from("My string");
    let ss: &str = &s[0..5];
    foo(&ss);
}

и скомпилировать его в режиме отладки (cargo build).

Теперь мы можем запустить получившийся исполняемый файл в отладчике и разместить точку останова непосредственно перед вызовом функции foo (на снимке экрана ниже показан отладчик Ghidra, работающий на ноутбуке Apple MacBook pro с процессором ARM).

Строковый срез (&str) ss создается командой по адресу 0x1000056cc (bl < >::index) и помещается в стек (как и ожидалось, учитывая, что ss является локальной переменной). Как мы уже говорили выше, ss является толстым указателем и содержит как указатель на строковые данные, так и информацию о длине. Например, это можно наблюдать в консоли отладчика в левом нижнем углу изображения:

&String

Обратите внимание, что если s является переменной типа String, &s является только ссылкой на String (т. е. &String). Концептуально это то же самое, что происходит, когда у вас есть переменная v типа Vec<T> и вы берете ссылку &v. В итоге появляется ссылка на Vec<T>.

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

fn print_type_of<T>(_: &T) {
    println!("{}", std::any::type_name::<T>())
}

fn main() {
    let s: String = String::from("My string");
    let s_ref = &s;
    let s_str = s.as_str();

    print_type_of(&s);
    print_type_of(&s_ref);
    print_type_of(&s_str);
}

соответствующий выход на консоль:

alloc::string::String
&alloc::string::String
&str

Обратите внимание на & перед второй строкой вывода.

Ссылка на строку (&String) отличается от ссылки &str. Однако &String часто может автоматически принудительно преобразовываться в &str.

Примеры

// Создание типа String из строкового литерала
let s = String::from("My string");
// Создать пустой тип String
let empty_s = String::new();
// Создать строку из вектора байтов
let hello_vec = vec![72, 101, 108, 108, 111];
let hello_string = String::from_utf8(hello_vec).unwrap();
// Создание строки, принадлежащей владельцу, из среза строки
let s: String = "My string".to_owned()
// Строковый литерал (str), действительный в течение всей программы
let my_string: &'static str = "My string";
// То же самое, что и выше, но короче
let my_string = "My string";
// При наличии строки создать срез строки, содержащий всю строку.
let s = String::from("My string");
let ss: &str = s.as_str();

Ссылки