- PVSM.RU - https://www.pvsm.ru -

String и &str в функциях Rust

КДПВ [1]
Этот пост посвящается всем тем, кого смущает необходимость использовать to_string(), чтобы заставить программы компилироваться. И я надеюсь пролить немного света на вопрос о том, почему в Rust два строковых типа String и &str.

Функции, которые принимают строки

Я хочу обсудить, как создавать интерфейсы, которые принимают строки. Я большой фанат гипермедии и увлечён созданием лёгких в использовании интерфейсов. Начнём с метода, который принимает String [2]. Наш поиск приведёт нас к типу 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 [3] и приведения типов. Пример позволит понять этот момент намного лучше.

Пример приведения с разыменованием

В этом примере строки создаются четырьмя разными способами, и все они работают с функцией 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 — это просто вектор восьмибайтных символов. Про приведение с разыменованием [4] (англ. [5]) можно подробнее почитать в книге «Язык программирования Rust [6]» (англ. [7]).

Использование структур

На данный момент мы должны уже быть свободны от лишних вызовов 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 — не допустить этого. Давайте заставим этот код компилироваться. Нам понадобится указать время жизни [8] (англ. [9]), или область видимости, так, чтобы 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 [10]. Структуре Encoder не нужно владеть переменной writer, которая реализует типаж std::fmt::Write [11], поэтому используется только заимствование. На самом деле String реализует Write. В этом примере при использовании функции encode [12] переменная типа 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, то есть живут на протяжении всей жизни программы — прим. перев.)

Что ещё почитать

Автор: kstep

Источник [1]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/107896

Ссылки в тексте:

[1] Image: http://habrahabr.ru/post/274485/

[2] String: https://doc.rust-lang.org/std/string/struct.String.html?search=String

[3] Deref: http://doc.rust-lang.org/stable/std/ops/trait.Deref.html

[4] приведение с разыменованием: http://kgv.github.io/rust_book_ru/src/deref-coercions.html

[5] англ.: https://doc.rust-lang.org/stable/book/deref-coercions.html

[6] Язык программирования Rust: https://www.gitbook.com/book/kgv/rust_book_ru/details

[7] англ.: https://doc.rust-lang.org/stable/book/

[8] время жизни: http://kgv.github.io/rust_book_ru/src/lifetimes.html

[9] англ.: http://doc.rust-lang.org/stable/book/lifetimes.html

[10] rustc_serialize: https://github.com/rust-lang/rustc-serialize/blob/b450079c18def61c654824d174660696d88112a2/src/json.rs#L567

[11] std::fmt::Write: http://doc.rust-lang.org/stable/std/fmt/trait.Write.html

[12] encode: https://github.com/rust-lang/rustc-serialize/blob/b450079c18def61c654824d174660696d88112a2/src/json.rs#L380

[13] Создание функции на Rust, которая принимает String или &str: http://habrahabr.ru/post/274455/

[14] оригинал: http://hermanradtke.com/2015/05/06/creating-a-rust-function-that-accepts-string-or-str.html

[15] Создание функций в Rust, которые возвращают &str или String: http://hermanradtke.com/2015/05/29/creating-a-rust-function-that-returns-string-or-str.html