Как реализовать трейт для &str и &[&str]

Перевод | Автор оригинала: Pascal Hertleif

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

tl;dr «Чувак, это какое-то злоупотребление системой типов, если я когда-либо видел это. Мне это очень нравится! Если мне когда-нибудь понадобится, будет полезно узнать об этом. (/u/mgattozzi на Reddit)

Примечание: этот пост предполагает общее понимание Rust. Также будут подписи с волосатым шрифтом - не бойтесь! Просто пропустите части, которые вам непонятны.

Итак, приступим! Наша цель такова: мы хотим иметь функцию, которая может принимать как фрагменты строки, так и фрагменты строк:

foo("Hello World");
foo(&["Hello", "World"]);

Фактический вариант использования заключается в том, что фрагмент строки может быть внутренне разделен на фрагменты - или пользователь может сделать это сам (чтобы убедиться, что он правильный, или сделать это программно).

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

trait ToFoo {
    fn to_foo(&self) -> Vec<String>;
}

Первая попытка

Звучит достаточно просто, правда? Запишем (манеж):

trait ToFoo<'a> {
    fn to_foo(&'a self) -> Vec<String>;
}

impl<'a> ToFoo<'a> for &'a str {
    fn to_foo(&'a self) -> Vec<String> { unimplemented!() }
}

impl<'a, 'b> ToFoo<'a> for &'a [&'b str] {
    fn to_foo(&'a self) -> Vec<String> { unimplemented!() }
}

fn main() {
    println!("{:?}", "yay".to_foo());
    println!("{:?}", (&["yay"]).to_foo());
}

Извините за весь шум2! Пожалуйста, проигнорируйте это минутку!

Но подождите - это не компилируется!

не найден метод с именем to_foo для типа &[&'static str; 1] в текущей области

К сожалению, мы реализовали наш трейт на срезе (это &[]), но дали ему &[; 1]. Разница? &[_; 1] - это ссылка на массив известного размера. У нас есть два варианта:

  1. Используйте &["foo"] [..], чтобы создать фрагмент с открытым диапазоном, то есть со всеми элементами.
  2. Реализуйте ToFoo для этого типа массива.

Первый вариант вполне допустим, если это вы пишете, пишет, что foo(&["bar"] [..]), но я нацелен здесь на то, чтобы предоставить пользователю этой теоретической библиотеки хороший API. И я не хочу говорить людям, чтобы они добавляли волшебных персонажей в конце аргументов, если мне не нужно!

К сожалению, начиная с Rust 1.163 нам нужно было написать реализации для всех типов массивов, которые мы хотим поддерживать, где тип также содержит длину массива! Итак, один для &[; 1], другой для &[; 2] и так далее. Мы могли бы сделать это в макросе, но он просто сгенерирует целую кучу кода, и это будет не очень элегантно.

Кроме того, разве не должно быть тривиальным представление некоторых &[_, n] в виде фрагментов? И есть места, где это работает! Почему не здесь? /u/dbaupp дал прекрасное объяснение этому на Reddit: это потому, что мы хотим использовать метод &self для &[&str], что означает, что мы имеем дело с &&[&str]. А поскольку мы начинаем с &[&str; 1], мы можем полагаться только на принуждение для ссылки, а не на внутренний [&str; 1].

Мы могли бы реализовать ToFoo на [&str], однако, чтобы использовать тот факт, что ссылка в &["foo"] будет запускать приведение deref, что означает, что он находит наш impl. К сожалению, это не работает для функций или методов, которые принимают &T, где T: ToFoo. Итак, хотя мы можем выполнять ["lorem"]. To_foo(), мы не можем выполнять foo(&["lorem"]) или даже ToFoo::to_foo(&["yay"]) - что именно то, что мы хотим использовать это для…

Так что давайте вместо этого попробуем что-нибудь другое!

Вторая попытка

В Rust есть довольно хорошая коллекция трэйтов преобразования (см. Std::convert), и одна из них - AsRef, которая выполняет «преобразование ссылки в ссылку». По сути, вы даете ему &x, и он возвращает вам & y совместимого типа:

pub trait AsRef<T> where T: ?Sized {
    fn as_ref(&self) -> &T;
}

Обратите внимание, что это не ... для &T, а ... для T, а затем имеет метод, который принимает &self. Хорошо, вот упрощенная новая версия (манеж):

trait ToFoo {}

impl<'a> ToFoo for &'a str {}
impl<'a, T> ToFoo for T where T: AsRef<[&'a str]> {}

Ааа и не компилируется:

error[E0119]: conflicting implementations of trait `ToFoo` for type `&str`:
 --> <anon>:4:1
  |
3 | impl<'a> ToFoo for &'a str {}
  | ----------------------------- first implementation here
4 | impl<'a, T> ToFoo for T where T: AsRef<[&'a str]> {}
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `&str`

Какие? «Конфликтная реализация для &str»? Где? Охххх ... В стандартном вводе есть следующее:

impl<'a, T, U> AsRef<U> for &'a T where
    T: AsRef<U> + ?Sized, U: ?Sized

Итак, Rust достаточно умен, чтобы видеть, что и &, и &[] соответствуют этой реализации AsRef, но недостаточно умен, чтобы различать impl AsRefss, чтобы понять, что наш второй impl ToFoo всегда должен работать только для &[_].

Итак, проблема в &, не так ли?

В третий раз очарование

Прежде всего, давайте еще раз повторим нашу сигнатуру, чтобы не прокручивать вверх:

trait ToFoo {
    fn to_foo(&self) -> Vec<String>;
}

Теперь давайте реализуем наш трейт для str вместо &str. Кстати: str - это тип, размер которого мы не знаем, но давайте не будем зацикливаться на этом сейчас.

impl ToFoo for str {
    fn to_foo(&self) -> Vec<String> { unimplemented!() }
}

Видите ли, мы в любом случае используем только &str, поскольку наш метод принимает &self. Не о чем беспокоиться.

Далее: реализуйте трейт для каждого T, где ссылка на него реализует AsRef <[&str]>:

impl<'a, T> ToFoo for T where T: AsRef<[&'a str]> {
    fn to_foo(&self) -> Vec<String> { unimplemented!() }
}

Мне потребовалось довольно много времени, чтобы добраться до этого момента. Теперь мы можем использовать это так (манеж):

fn foo<'a, T: ToFoo + ?Sized>(_x: &'a T) {
    unimplemented!()
}

(Параметр «? Sized» предназначен для разрешения str.)

Хороший!

Наконец, вы можете найти реальный код, который использует этот шаблон в этой фиксации.

  1. Для получения информации о трэйтах характера прочтите этот пост, эту главу книги или эту главу, второе издание книги, которое готовится.

  2. Если вы не привыкли к Rust: так выглядит большая часть кода на Rust. Для чего нужны эти «галочки»? Я рада, что вы спросили! Одна из определяющих особенностей Rust заключается в том, что он может гарантировать, что ссылки на x (&x) действительны только до тех пор, пока действителен ресурс x. Это предотвращает некоторые довольно серьезные ошибки! Синтаксис 'a позволяет нам дать имя этим временам жизни, чтобы мы могли, например, определить ссылки &' ax и &'by и указать, что' a действительно (по крайней мере) до тех пор, пока 'b', написав 'a : 'б. И обычно для этого есть довольно приятные правила вывода; Однако в определениях трэйтов Rust требует, чтобы мы были явными.

  3. rustc 1.16.0 (30cf806ef 10.03.2017)

Спасибо за чтение.