Эта статья в блоге знакомит с понятиями типа
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
является толстым указателем и содержит как указатель на строковые данные, так и информацию о длине. Например, это можно наблюдать в консоли отладчика в левом нижнем углу изображения:
- Указатель стека, при считывании регистра
sp
, указывает на0x000000016bddad60
; - Информация о длине среза, распечатанная с помощью
x/1gx $sp+0x10
, равна0x5
; - Указатель на строковые данные, напечатанные с помощью
x/1gx $sp+0x8
, указывает на0x00006000004ac010
; - Мы можем распечатать значение, указанное этим адресом, в памяти
-s1 -fc -c 9 0x00006000004ac010
и убедиться, что этот адрес на самом деле является расположением ожидаемой строки.
&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();