Rusty Bytes – изучаем язык Rust
89 subscribers
1 photo
45 links
Канал про разработку на языке Rust: обучающие материалы, ссылки на полезные статьи, подборки интересных библиотек и фреймворков.
Download Telegram
🔤 Строки в Rust: Часть 1. String vs str.

Новички, знакомящиеся с языком Rust, порой недоумевают, видя такие строчки кода:

let s: String = "строка".to_string();


или подобные ошибки компиляции:

fn main() {
log("Program has been started");
}

fn log(message: String) {
println!("{message}");
}

// mismatched types: expected `String`, found `&str`


⁉️ Зачем строку, объявленную в кавычках, нужно дополнительно приводить к типу String и почему её нельзя передавать в метод, ожидающий на входе String?
В других языках обычно с такими проблемами никто не сталкивается.

Всё дело в том, что в Rust существует сразу несколько типов для работы со строками.
Самые популярные и часто используемые из них – это String и str, которые сегодня мы рассмотрим чуть более подробно.

str
Во многих языках программирования есть такое понятие, как литеральные строки, то есть строки, объявленные в коде в виде текста в кавычках:

let s = "это литеральная строка";


В Rust при компиляции литеральные строки попадают as is в бинарный файл, и программа может получить к ним доступ по определённому адресу.
Эту область памяти называют сегмент данных (data segment) и по своей сути она схожа со статическим сегментом памяти: данные доступны при старте программы и остаются неизменными на протяжении всей жизни программы.

Раз мы не можем каким-то образом изменить эти строки или повялить на их время жизни, Rust не даёт нам работать с типом str напрямую, а предоставляет только immutable ссылку – &str.
Таким образом, инициализируя переменную в коде литеральной строкой, мы получаем у этой переменной тип &str.

let s: &str = "это литеральная строка";


String
Если же мы каким-то образом получаем строку в процессе работы программы, например вычитывая данные из файла, мы можем сохранить их в переменную типа String.

В отличие от str, String не является примитивным типом, а представляет собой структуру-обёртку над динамическим массивом, где элементы массива – это символы строки в кодировке UTF-8:

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


Наличие вектора подсказывает нам ещё несколько отличий от str: String сохраняется в Heap, её длина не фиксированна и такие строки могут изменяться в процессе работы программы.

❗️ Кстати, изменяемость строк – ещё одна необычная особенность для разработчиков, привыкшим к неизменным строкам в таких языках, как Java, JavaScript, Python и т.д.

Последнее отличие – String является так называемым owned типом данных, то есть типом, которым может владеть объявленная нами переменная.
Как я уже отмечал выше, мы не можем владеть данными типа str, поэтому &str – заимствованный (borrowed) тип данных.

Что будет, если мы возьмём ссылку на String?
Тут есть несколько вариантов:

1️⃣ Мы можем получить данные типа &String, т.е. ссылку на структуру String в Heap, которая внутри хранит в себе ссылку на массив символов, расположенных тоже где-то в Heap.

let s = String::from("abcd");
let s1: &String = &s;


2️⃣ Мы можем получить слайс (срез) массива символов. Подробнее про слайсы можно почитать в документации.
Если коротко, то слайс – это ссылка на массив данных определённой длины. Мы можем получить срез всего массива или какой-то его части (подстроку). При этом слайс, в отличии String, не может быть изменён.

let s = String::from("abcd");
let s2 = &s[0..s.len()]; // вся строка
let s3 = &s[0..2]; // первые два символаы


А какой тип у нас обладает такими же характеристиками: неизменяемая ссылка на массив UTF-8 символов?

Правильно, &str!

Таким образом, типы переменных s2 и s3 из примера выше – &str.
А литеральные строки типа &str – это ничто иное, как слайс массива символов.

Продолжение ⬇️

#rust #rustlang #RustStringTypes
🔤 Строки в Rust: Часть 2. Преобразование String в &str и обратно.

Теперь, когда мы разобрались со String, &String, str и &str, посмотрим каким образом происходит преобразование этих типов друг в друга.

Глядя на то, как с помощью оператора & мы конвертируем тип String в другой тип &str, вы могли подумать, что для такой операции используется какая-то внутренняя магия компилятора.

Но всё гораздо проще: в Rust для типа String реализован трейт Deref в тип str.
А значит мы получаем такое преобразование с помощью неявного приведения типов:

let s1 = String::from("abcd");
let s2: &str = &s1;


💡 Отсюда следует хорошая практика:
в функциях, которые работают со ссылками на строки, лучше объявлять переменные с типом &str, а не &String, так как это позволяет одинаково удобно передавать в функцию обычные строки, строковые слайсы и литеральные строки:


fn log(message: &str) {
println!("{message}");
}

fn main() {
let message = String::from("message");
// вся строка
log(&message);
// подстрока
log(&message[0..4]);
// литеральная строка
log("another message");
}


Если заменить тип переменной message в функции log на &String, то 2й и 3й вариант вызова функции станет некорректным.

Если с преобразованием String в &str всё более или менее однозначно, то вот для конвертации &str в String существует целая россыпь способов:

1️⃣ to_owned()

let s = "abcd".to_owned();


Для типа &str реализован метод to_owned(), который позволяет из borrowed типа &str получить owned тип String.
Преобразование происходит путём копирования данных с помощью всяких низкоуровневых функций.

Обычно используется тогда, когда разработчик хочет подчеркнуть именно эту особенность конвертации: получение владения над строкой.

2️⃣ String::from()

let s = String::from("abcd");


Один из самых популярных способов – вызов метода from (благодаря реализации трейта From).
Внутри вызывается метод to_owned().

Обычно используется тогда, когда хотят подчеркнуть создание типа String.
При конвертации литеральных строк мне этот вариант не сильно нравится, так как длинный префикс немного отвлекает от самих данных, которые могут быть важнее, чем их тип.

3️⃣ to_string()

let s = "abcd".to_string();


Ещё один популярный способ – вызов метода трейта ToString.
Внутри вызывается String::from(), который в свою очередь вызывает внутри to_owned().

Я чаще всего использую именно этот способ, так как он и отображает моё намерение создать переменную типа String и не отвлекает от данных.

4️⃣ into()

let s: String = "abcd".into();


Раз для String реализован трейт From<&str>, значит и для &str реализован трейт Into<String>.

С точки зрения реализации тут та же самая "матрёшка", что и для метода to_string(): внутри вызывается from(), который в свою очередь вызывает to_owned().

Этот вариант может быть самым коротким, особенно если дальше переменная в явном виде используется как String, и Rust сможет вывести её тип самостоятельно без указания его при объявлении.
Но мне такой подход не очень нравится, так как теряется информация о типе.

Этот вариант я люблю использовать, когда передаю &str в какой-то метод и хочу показать, что мне не важно какой там дальше тип – String или какой-то другой, для которого реализован трейт Into.


fn save_data(data: String) {}

fn main() {
save_data("some data".into());
}


❗️ Как видите, какой бы вариант преобразования вы ни выбрали, всё в итоге сводится к вызову метода to_owned().
А значит выбор подхода целиком зависит от того, какие намерения вы хотите заложить в текст своей программы.

#rust #rustlang #RustStringTypes
2👍1🔥1