Этот пост посвящается всем тем, кого смущает необходимость использовать
to_string()
, чтобы заставить программы компилироваться. И я надеюсь пролить немного света на вопрос о том, почему в Rust два строковых типа String
и &str
.Функции, которые принимают строки
Я хочу обсудить, как создавать интерфейсы, которые принимают строки. Я большой фанат гипермедии и увлечён созданием лёгких в использовании интерфейсов. Начнём с метода, который принимает
String
. Наш поиск приведёт нас к типу std::string::String
, что для начала совсем не плохо.fn print_me(msg: String) {
println!("сообщение: {}", msg);
}
fn main() {
let message = "привет, мир";
print_me(message);
}
Получаем ошибку компиляции:
expected `collections::string::String`,
found `&'static str`
Получается, что строковый литерал типа
&str
не совместим с типом String
. Нам нужно поменять тип переменной message
на String
, чтобы компиляция удалась: let message = "привет, мир".to_string();
. Так заработает, но это всё равно что использовать clone()
для починки ошибок владения-наследования. Вот три причины, чтобы поменять тип аргумента print_me
на &str
:- Символ
&
обозначает ссылочный тип, то есть мы даём переменную взаймы. Когдаprint_me
заканчивает работу с переменной, право владения возвращается к её изначальному владельцу. Если у нас нет хорошей причины, для передачи владения переменнойmessage
в нашу функцию, нам следует использовать заимствование. - Использование ссылки более эффективно. Использование
String
дляmessage
означает, что программа должна скопировать значение. При использовании ссылки, такой как&str
, копирования не происходит. - Тип
String
может волшебным образом превращаться в&str
с использованием типажаDeref
и приведения типов. Пример позволит понять этот момент намного лучше.
Пример приведения с разыменованием
В этом примере строки создаются четырьмя разными способами, и все они работают с функцией
print_me
. Основной момент, благодаря которому всё это работает, — передача значений по ссылке. Вместо того, чтобы передавать владеющую строку owned_string
как String
, мы передаём её как указатель &String
. Когда компилятор видит, что &String
передаётся в функцию, которая принимает &str
, он приводит &String
к &str
. Точно такая же конверсия используется при использовании строк с обычным и атомарным счётчиком ссылок. Переменная string
уже является ссылкой, поэтому нет необходимости использовать &
при вызове print_me(string)
. Обладая этим знанием, нам больше не нужно постоянно вызывать .to_string()
по нашему коду.fn print_me(msg: &str) {
println!("msg = {}", msg);
}
fn main() {
let string = "привет, мир";
print_me(string);
let owned_string = "привет, мир".to_string(); // или String::from_str("привет, мир")
print_me(&owned_string);
let counted_string = std::rc::Rc::new("привет, мир".to_string());
print_me(&counted_string);
let atomically_counted_string = std::sync::Arc::new("привет, мир".to_string());
print_me(&atomically_counted_string);
}
Вы так же можете использовать приведение с разыменованием с другими типами, такими как вектор
Vec
. Всё таки String
— это просто вектор восьмибайтных символов. Про приведение с разыменованием (англ.) можно подробнее почитать в книге «Язык программирования Rust» (англ.).Использование структур
На данный момент мы должны уже быть свободны от лишних вызовов
to_string()
. Однако, у нас могут возникнуть некоторые проблемы при использовании структур. Используя имеющиеся знания, мы могли бы создать такую структуру:struct Person {
name: &str,
}
fn main() {
let _person = Person { name: "Herman" };
}
Мы получим такую ошибку:
<anon>:2:11: 2:15 error: missing lifetime specifier [E0106]
<anon>:2 name: &str,
Rust пытается удостовериться, что
Person
не может пережить ссылку на name
. Если Person
переживёт name
, то есть риск падения программы. Основная цель Rust — не допустить этого. Давайте заставим этот код компилироваться. Нам понадобится указать время жизни (англ.), или область видимости, так, чтобы Rust смог нам обеспечить безопасность. Обычно время жизни называют так: 'a
. Я не знаю, откуда пошла такая традиция, но мы ей последуем.struct Person {
name: &'a str,'
}
fn main() {
let _person = Person { name: "Herman" };
}
При попытке скомпилировать получим такую ошибку:
<anon>:2:12: 2:14 error: use of undeclared lifetime name `'a` [E0261]
<anon>:2 name: &'a str,
Давайте поразмыслим. Мы знаем, что хотим как-то донести до компилятора Rust мысль, что структура
Person
не должна пережить поле name
. Так что нам нужно объявить время жизни структуры Person
. Недолгие поиски приводят нас к синтаксису для объявления времени жизни: <'a>
.struct Person<'a> {
name: &'a str,
}
fn main() {
let _person = Person { name: "Herman" };
}
Это компилируется! Обычно мы реализуем на структурах некоторые методы. Давайте добавим к нашему классу
Person
метод greet
.struct Person<'a> {
name: &'a str,
}
impl Person {
fn greet(&self) {
println!("Привет, меня зовут {}", self.name);
}
}
fn main() {
let person = Person { name: "Herman" };
person.greet();
}
Теперь мы получим такую ошибку:
<anon>:5:6: 5:12 error: wrong number of lifetime parameters: expected 1, found 0 [E0107]
<anon>:5 impl Person {
У нашей структуры
Person
есть параметр времени жизни, так что наша реализация должны тоже его иметь. Давайте объявим время жизни 'a
в реализации Person
вот так: impl Person<'a> {
. Увы, теперь мы получим такую странную ошибку компиляции:<anon>:5:13: 5:15 error: use of undeclared lifetime name `'a` [E0261]
<anon>:5 impl Person<'a> {
Чтобы нам объявить время жизни, нам нужно указать время жизни сразу после
impl
вот так: impl<'a> Person {
. Компилируем снова, получаем ошибку:<anon>:5:10: 5:16 error: wrong number of lifetime parameters: expected 1, found 0 [E0107]
<anon>:5 impl<'a> Person {
Уже понятнее. Давайте добавим параметр времени жизни в описании структуры
Person
её в реализации вот так: impl<'a> Person<'a> {
. Теперь программа cкомпилируется. Вот полный рабочий код:struct Person<'a> {
name: &'a str,
}
impl<'a> Person<'a> {
fn greet(&self) {
println!("Привет, меня зовут {}", self.name);
}
}
fn main() {
let person = Person { name: "Herman" };
person.greet();
}
String или &str в структурах
Теперь возникает вопрос: когда стоит использовать
String
, а когда &str
в структурах? Другими словами, когда следует использовать ссылку на другой тип в структуре? Нам следует использовать ссылки, если наша структура не требует владения переменной. Смысл может быть немного размыт, так что я использую несколько правил, для ответа на этот вопрос.- Нужно ли использовать переменную вне структуры? Вот немного надуманный пример:
struct Person {
name: String,
}
impl Person {
fn greet(&self) {
println!("Привет, меня зовут {}", self.name);
}
}
fn main() {
let name = String::from_str("Herman");
let person = Person { name: name };
person.greet();
println!("Меня зовут {}", name); // move error
}
Здесь мне стоит использовать ссылку, так как мне нужно будет использовать переменную до помещения в структуру. Пример из реальной жизни — rustc_serialize. Структуре
Encoder
не нужно владеть переменной writer
, которая реализует типаж std::fmt::Write
, поэтому используется только заимствование. На самом деле String
реализует Write
. В этом примере при использовании функции encode
переменная типа String
передаётся в Encoder
и затем возвращается обратно в encode
.- Мой тип большой? Если тип большой, то передача по ссылке позволит сберечь память. Помните, передача по ссылке не приводит к копированию переменных. Представьте буфер типа
String
с большим количеством данных. Его копирование при каждой передаче в другую функцию может значительно замедлить программу.
Теперь мы может создать функцию, которая принимает строки в виде
&str
, String
или даже со счётчиком ссылок. Мы так же можем создавать структуры, которые содержат ссылки. Время жизни структуры связано с содержащимися в ней ссылками, так что структура не может пережить переменные, на которые она ссылается, и тем самым привести к ошибкам в программе. А ещё теперь у нас есть базовое понимание того, когда стоит использовать ссылки внутри структур, а когда нет.По поводу 'static
Думаю, что стоит обратить внимание на ещё один момент. Мы можем использовать статическое время жизни
'static
(как в первом примере), чтобы заставить наш пример скомпилироваться, но я бы не советовал так делать:struct Person {
name: &'static str,
}
impl Person {
fn greet(&self) {
println!("Привет, меня зовут {}", self.name);
}
}
fn main() {
let person = Person { name: "Herman" };
person.greet();
}
Статическое время жизни
'static
валидно на протяжении всей жизни программы. Вы вряд ли захотите, чтобы Person
или name
жили так долго. (Например статические строковые литералы, вкомпилированные в саму программу, обладают типом &'static str
, то есть живут на протяжении всей жизни программы — прим. перев.)Что ещё почитать
Комментарии (6)
beduin01
04.01.2016 16:27-7Я не верю, что язык сможет быть массовым имея такие сложности на пустом месте. На этом фоне даже корявый С++ кажется куда более простым и понятным.
kstep
04.01.2016 16:41+7В C++ сложности точно такие же, только неявные. Здесь тебе компилятор говорит, что ты не прав, в подобном случае С++ промолчит и даст выстрелить в ногу. Опять же, если всё делать правильно, то в C/C++ ты передашь вместо среза пару указателей или итераторов, либо указатель и длину строки. Здесь всегда одна сущность — срез. ИМХО, в расте как раз проще.
JIghtuse
04.01.2016 21:49+1К счастью, в очередном стандарте C++ ожидается string_view aka string_ref, который как раз для подобных целей создан. Но, как это бывает, стандарт распространится нескоро и никто (за исключением, возможно, GSL) не будет бить по рукам за использование view на несуществующий объект. Реализация
&str
в Rust подобных недостатков лишена. Компилятор просто не даст такой трюк совершить.
biziwalker
> Использование String для message означает, что программа должна скопировать значение.
В Rust по умолчанию операция перемещения (move), операция копирования (clone) же достигается явным указанием.
kstep
Перемещение при передаче значения в другую функцию, это побайтное копирование структуры в стек (что может быть выоптимизированно llvm, но не самим растом). Понятно, однако, что в случае String будут копированы только указатели всякие и служебная инфа, данные на куче останутся на месте.
Но тут речь не про это. Если вы читали внимательно, в статье говорится про строки, и для того, чтобы передать строковый литерал в функцию, которая принимает String, его нужно разместить в куче преобразовав в String (to_string(), to_owned(), into() — что больше нравится), а это копирование данных. Про это речь.