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

Перевод | Автор оригинала: Herman J. Radtke III

Для всех людей, разочарованных необходимостью использовать to_string() для получения программ для компиляции, этот пост для вас. Для тех, кто не совсем понимает, почему в Rust есть два строковых типа String и &str, я надеюсь пролить немного света на этот вопрос.

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

Я хочу обсудить, как создавать интерфейсы, принимающие строки. Я заядлый фанат гипермедиа и одержим созданием простых в использовании интерфейсов. Начнем с метода, который принимает String. Наш поиск подсказывает, что std::string::String - здесь хороший выбор.

fn print_me(msg: String) {
    println!("the message is {}", msg);
}

fn main() {
    let msg = "hello world";
    print_me(msg);
}

Это дает ошибку компилятора:

expected `collections::string::String`,
    found `&'static str`

Таким образом, строковый литерал имеет тип &str и несовместим с типом String. Мы можем изменить тип сообщения на String и успешно скомпилировать: let message = "hello world" .to_string() ;. Это работает, но аналогично использованию clone() для обхода ошибок владения / заимствования. Вот три причины изменить print_me, чтобы вместо этого принимать &str:

Пример принуждения Deref

В этом примере строки создаются четырьмя различными способами, которые работают с функцией print_me. Ключом к тому, чтобы все это работало, является передача значений по ссылке. Вместо того, чтобы передавать own_string как String в print_me, мы передаем его как &string. Когда компилятор видит, что &string передается функции, которая принимает &str, он переводит &string в &str. Такое же принуждение имеет место для строк с подсчетом ссылок и строк с атомарными ссылками. Строковая переменная уже является ссылкой, поэтому нет необходимости использовать & при вызове print_me (string). Зная это, нам больше не нужно, чтобы вызовы .to_string() засоряли наш код.

fn print_me(msg: &str) { println!("msg = {}", msg); }

fn main() {
    let string = "hello world";
    print_me(string);

    let owned_string = "hello world".to_string(); // or String::from_str("hello world")
    print_me(&owned_string);

    let counted_string = std::rc::Rc::new("hello world".to_string());
    print_me(&counted_string);

    let atomically_counted_string = std::sync::Arc::new("hello world".to_string());
    print_me(&atomically_counted_string);
}

Вы также можете использовать приведение Deref с другими типами, такими как Vector. В конце концов, String - это просто вектор из 8-байтовых символов. Подробнее о приведении Deref читайте в книге Rust lang.

Введение в структуру

На этом этапе у нас не должно быть посторонних вызовов 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 все-таки удалось пережить имя, мы рискуем вывести нашу программу из строя. Вся суть 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 не должна пережить имя. Итак, нам нужно позаботиться о нашей жизни в структуре Person. Некоторый поиск укажет нам на <'a> - это синтаксис для объявления времени жизни.

struct Person<'a> {
    name: &'a str,
}

fn main() {
    let _person = Person { name: "Herman" };
}

Это компилируется! Однако мы обычно реализуем методы в наших структурах. Давайте добавим функцию приветствия в наш класс Person.

struct Person<'a> {
    name: &'a str,
}

impl Person {
    fn greet(&self) {
        println!("Hello, my name is {}", 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 имеет параметр времени жизни, поэтому он должен быть и в нашей реализации. Давайте объявим нашу жизнь реализацией 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> {. Теперь наша программа компилируется. Вот полный рабочий код:

struct Person<'a> {
    name: &'a str,
}

impl<'a> Person<'a> {
    fn greet(&self) {
        println!("Hello, my name is {}", self.name);
    }
}

fn main() {
    let person = Person { name: "Herman" };
    person.greet();
}

Строка или &str в структуре

Теперь вопрос в том, использовать ли в вашей структуре String или &str. Другими словами, когда мы должны использовать ссылку на другой тип в структуре? Мы должны использовать ссылку, если нашей структуре не требуется владение переменной. Эта концепция может быть немного расплывчатой, но есть несколько правил, которые я использую, чтобы получить ответ.

struct Person {
    name: String,
}

impl Person {
    fn greet(&self) {
        println!("Hello, my name is {}", self.name);
    }
}

fn main() {
    let name = String::from_str("Herman");
    let person = Person { name: name };
    person.greet();
    println!("My name is {}", name); // move error
}

Я должен использовать здесь ссылку, так как мне нужно будет использовать переменную позже. Вот реальный пример в rustc_serialize. Структуре Encoder не нужно владеть записывающей переменной, которая реализует std::fmt::Write, просто используйте (заимствуйте) на некоторое время. Фактически String реализует Write. В этом примере с использованием функции кодирования переменная типа String передается в кодировщик, а затем возвращается вызывающей стороне кодирования.

Теперь мы должны иметь возможность создавать функции, которые принимают строки, независимо от того, являются ли они &str, String или подсчитываются ссылки на событие. Мы также можем создавать структуры, которые могут иметь переменные, являющиеся ссылками. Время жизни структуры связано с этими ссылочными переменными, чтобы гарантировать, что структура не переживет указанную переменную и не приведет к плохим вещам в нашей программе. У нас также есть начальное понимание того, должны ли переменные в нашей структуре быть типами или ссылками на типы.

А как насчет "статического"

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

struct Person {
    name: &'static str,
}

impl Person {
    fn greet(&self) {
        println!("Hello, my name is {}", self.name);
    }
}

fn main() {
    let person = Person { name: "Herman" };
    person.greet();
}

Статическое время жизни действительно для всей программы. Возможно, вам не понадобится Человек или имя, чтобы прожить так долго.

Связанный