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

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

В моем последнем посте мы много говорили об использовании &str в качестве предпочтительного типа для функций, принимающих строковый аргумент. Ближе к концу этого поста было обсуждение того, когда использовать String vs &str в структуре. Я думаю, что это хороший совет, но бывают случаи, когда использование &str вместо 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(). Однако эргономика этой функции ниже, чем хотелось бы. Если мы используем строковый литерал, мы можем создать новый объект Person, например Person.new («Герман»). Если у нас уже есть String, нам нужно запросить ссылку на 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() всякий раз, когда есть строковый литерал.

В конверсии

Мы можем упростить работу вызывающей программы с помощью свойства Into. Эта трэйта может автоматически преобразовывать &str в строку. Если у нас уже есть 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() выглядит немного иначе. Мы используем Generics и Traits, чтобы сообщить Rust, что некоторый тип S должен реализовывать трейт Into для типа String. Тип String реализует Into как noop, потому что у нас уже есть 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() }
    }
}

Связанный