[Перевод] Создание функции на Rust, которая принимает String или &str

От переводчика


КДПВСтатья — одна из серии постов, рассказывающих об использовании некоторых полезных библиотечных типажей и связанных с ними идиом Rust на примере строковых типов данных. Информация бесспорно полезная как для начинающих программистов на Rust, так и для тех, что уже успел себя немного попробовать в этом языке, но ещё не совсем освоился с богатой библиотекой типажей. Оригинальный пост содержит несколько неточностей и опечаток в коде, которые я постарался исправить в процессе перевода, однако в общем и целом описанные подходы и мотивация правильные, подходящие под понятие «лучших практик», а потому заслуживают внимания.

В моём последнем посте мы много говорили об использовании &str как предпочтительного типа для функций, принимающих строковые аргументы. Ближе к концу поста мы обсудили, когда лучше использовать String, а когда &str в структурах (struct). Хотя я думаю, что в целом совет хорош, но в некоторых случаях использование &str вместо String не оптимально. Для таких случаев нам понадобится другая стратегия.

Структура со строковыми полями типа String


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

struct Person {
    name: String,
}


Теперь нам нужно реализовать метод new(). Следуя совету из предыдущего поста, мы предпочтём тип &str:

impl Person {
    fn new(name: &str) -> Person {
        Person { name: name.to_string() }
    }
}


Пример заработает, только если мы не забудем о вызове .to_string() в методе new() (На самом деле здесь лучше использовать метод to_owned(), поскольку метод to_string() для размещения строки в памяти использует довольно тяжёлую библиотеку форматирования текста, а to_owned() просто копирует строковый срез &str напрямую в новый объект String — прим. перев.). Однако, удобство использования функции оставляет желать лучшего. Если использовать строковый литерал, то мы можем создать новую запись Person так: Person::new("Herman"). Но если у нас уже есть владеющая строка String, то нам нужно получить ссылку на неё:

let name = "Herman".to_string();
let person = Person::new(name.as_ref());


Похоже, как будто бы мы ходим кругами. Сначала у нас есть String, затем мы вызываем as_ref() чтобы превратить её в &str, только затем, чтобы потом превратить её обратно в String внутри метода new(). Мы могли бы вернуться к использования String, вроде fn new(name: String) -> Person, но тогда нам пришлось бы заставлять пользователя постоянно вызывать .to_string(), если тот захочет создать Person из строкового литерала.

Конверсии с помощью Into


Мы можем сделать нашу функцию проще в использовании с помощью типажа Into. Этот типаж будет автоматически конвертировать &str в String. Если у нас уже есть String, то конверсии не будет.

struct Person {
    name: String
}

impl Person {
    fn new>(name: S) -> Person {
        Person { name: name.into() }
    }
}

fn main() {
    let person = Person::new("Herman");
    let person = Person::new("Herman".to_string());
}


Синтаксис сигнатуры new() теперь немного другой. Мы используем обобщённые типы (англ.) и типажи (англ.), чтобы объяснить Rust, что некоторый тип S должен реализовать типаж Into для типа String. Тип String реализует Into как пустую операцию, потому что String уже имеется на руках. Тип &str реализует Into с использованием того же .to_string() (на самом деле нет — прим. перев.), который мы использовали с самого начала в методе new(). Так что мы не избегаем необходимости вызывать .to_string(), а убираем необходимость делать это пользователю метода. У вас может возникнуть вопрос, не вредит ли использование Into производительности, и ответ — нет. Rust использует статическую диспетчеризацию (англ.) и мономорфизацию для обработки всех деталей во время компиляции.

Такие слова, как статическая диспетчеризация или мономорфизация могут немного сбить вас с толку, но не волнуйтесь. Всё, что вам нужно знать, так это то, что показанный выше синтаксис позволяет функциям принимать и String, и &str. Если вы думаете, что fn new>(name: S) -> Person — очень длинный синтаксис, то да, вы правы. Однако, важно заметить, что в выражении Into нет ничего особенного. Это просто названия типажа, который является частью стандартной библиотеки Rust. Вы сами могли бы его написать, если бы захотели. Вы можете реализовать похожие типажи, если посчитаете их достаточно полезными, и опубликовать на crates.io. Вся эта мощь, сосредоточенная в пользовательском коде, и делает Rust таким восхитительным языком.

Другой способ написать Person: new ()


Можно использовать синтаксис where, который, возможно, будет проще читать, особенно если сигнатура функции становится более сложной:

struct Person {
    name: String,
}

impl Person {
    fn new(name: S) -> Person where S: Into {
        Person { name: name.into() }
    }
}


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


© Habrahabr.ru