От переводчика
Статья — одна из серии постов, рассказывающих об использовании некоторых полезных библиотечных типажей и связанных с ними идиом 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<S: Into<String>>(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<S: Into>(name: S) -> Person — очень длинный синтаксис, то да, вы правы. Однако, важно заметить, что в выражении Into нет ничего особенного. Это просто названия типажа, который является частью стандартной библиотеки Rust. Вы сами могли бы его написать, если бы захотели. Вы можете реализовать похожие типажи, если посчитаете их достаточно полезными, и опубликовать на crates.io. Вся эта мощь, сосредоточенная в пользовательском коде, и делает Rust таким восхитительным языком.
Другой способ написать Person::new()
Можно использовать синтаксис where, который, возможно, будет проще читать, особенно если сигнатура функции становится более сложной:
struct Person {
name: String,
}
impl Person {
fn new<S>(name: S) -> Person where S: Into<String> {
Person { name: name.into() }
}
}
Что ещё почитать