Строки
Note
Если эта глава кажется вам трудной при первом прочтении, не пытайтесь понять её полностью. Вернитесь к ней после прочтения глав про Владение и Структуры.
Строки в Rust хранятся в виде буферов с символами в UTF-8 кодировке. При этом в Rust есть два основных типа для строк, которые отличаются тем, как они взаимодействуют с этим буфером: &str и String.
&str (строковый слайс)
Если мы напишем в коде программы строковый литерал (строку в двойных кавычках), то эта строка будет иметь тип &str.
#![allow(unused)]
fn main() {
let s: &str = "some text";
}
По сути &str — это слайс, ссылающийся на буфер с последовательностью символов в кодировке UTF-8.
В каком-то смысле такие строки похожи на const char* строки в языке C, с той разницей, что в отличие от C, &str, будучи слайсом, хранит не только начальный адрес буфера в памяти, но и его длину.
Когда компилятор находит в коде строковый литерал, он, как правило, помещает эту строку в сегмент данных (или в сегмент кода, в зависимости от целевой платформы), и там эта строка “живёт” от самого начала программы и до её конца.
Тип &str не занимается управлением памяти, в которой находится строка, он просто ссылается на данные в памяти. При этом эта память может принадлежать как сегменту данных, так и куче (и являться собственностью объекта String) или даже располагаться на стеке.
String
Если &str — это слайс, который ссылается на буфер со строкой, но никак не управляет этим буфером, то String, наоборот, является собственником буфера, в котором содержится строка.
Технически String является обёрткой над вектором Vec<u8>, который хранит последовательность символов в кодировке UTF-8. Поэтому String всегда единолично владеет буфером со своей строкой, и этот буфер всегда располагается в куче.
При этом для любого String всегда можно создать слайс &str, который будет ссылаться на строковый буфер, находящийся во владении String.
Есть 3 способа создать переменную типа String:
- При помощи конструктора
String::from - При помощи
String::newсоздать пустую строку и далее наполнить её отдельно - Из
&strпри помощи методаto_string()
Конструктор String::from
Наиболее понятный способ создания объекта String — функция-конструктор (о них мы поговорим в главе Структуры) String::from(&str), которая в качестве аргумента принимает слайс &str. Эта функция:
- создаёт объект
String, который как мы уже сказали, — просто обёртка надVec<u8>(вектором хранящим буфер с элементами типаu8) - далее из аргумента
&strкопирует строку в свежесозданный вектор - и после возвращает готовый, инициализированный объект
String
fn main() {
// слайс на статическую строку, находящуюся в сегменте даных
let slice: &str = "text";
// Создаст в куче буфер и скопирует в него "text".
// На стеке будет тройка значений, как у Vec:
// адрес буфера, общий размер буфера и количество заполненных строкой байт
let s = String::from(slice);
}
Note
Разумеется, гораздо короче и проще написать просто
String::from("text"), что в большинстве случаев и делают. В примере выше мы создали отдельную переменнуюsliceисключительно для наглядности.
Конструктор String::new
Функция-конструктор String::new просто создаёт новый объект String с пустым строковым буфером. Это может быть нужно, например, для того, чтобы передать объект String в функцию, которая заполнит его текстом.
Например, функция, которая читает строку с консоли, в качестве параметра принимает мутабельную ссылку на объект String, в который будет записан текст, считанный с консоли.
fn main() {
println!("Please enter some text and hit Enter button");
let mut buf = String::new(); // создаём пустую строку
std::io::stdin().read_line(&mut buf); // считываем текст с консоли в buf
println!("You have entered: {buf}");
}
Также в пустую строку (да и не только в пустую) можно добавлять символы при помощи метода push(char) или сразу строковые слайсы при помощи метода push_str(&str).
fn main() {
let mut s = String::new();
s.push('H');
s.push('e');
s.push('l');
s.push('l');
s.push('o');
s.push_str(" world!");
println!("{s}"); // Hello world!
}
Метод to_string()
Последний способ создания объекта String — вызов метода to_string() на объекте слайса &str. По сути, этот метод делает абсолютно то же самое, что и String::from(&str), только с другим синтаксисом.
fn main() {
let s: String = "text".to_string();
}
&str и String в памяти
Чтобы подытожить, как &str и String располагаются в памяти, давайте рассмотрим следующий пример, в котором мы:
- Создаём слайс
&strиз строкового литерала - Далее создаём
Stringиз этого слайса - Создаём слайс, ссылающийся на строковый буфер объекта
String
fn main() {
// Компилятор увидит константный строковый литерал и поместит
// такую строку в сегмент статических данных.
let a_slice_1: &str = "text";
// Создаём String из символов, на которые указывает слайс.
// Это приведёт к созданию копии символов строки в куче.
let a_string: String = String::from(a_slice_1);
// Создаём второй слайс, который указывает на буфер символов в хипе,
// принадлежащей String-строке.
let a_slice_2: &str = a_string.as_str();
}
Примерно так эти строки будут располагаться в памяти:
Макрос format!
Мы уже знакомы с макросом println!, который используется для вывода на консоль. Этот макрос в качестве аргумента принимает форматирующую строку, в которую при помощи {} можно “встраивать” значения.
Стандартная библиотека предлагает еще один макрос — format!. Он принимает на вход такую же форматирующую строку, как и println!, только, в отличие от последнего, он не печатает текст на консоль, а возвращает его в виде объекта String.
fn main() {
let s: String = format!("{} in the power of the 2 is {}", 3, 9);
println!("{s}"); // 3 in the power of the 2 is 9
}