Откуда ты::откуда

Перевод | Автор оригинала: Andre Bogus

А во что ты превратился::во что?

(Эта статья написана на Rust 1.4)

На этот раз давайте взглянем на трэйты From и Into (и некоторые связанные) и зададим вопрос, когда и для чего их использовать. Обратите внимание, что есть специализации (например, IntoIterator) и варианты (например, FromStr) этих черт; хорошо знать об этом при написании кода на Rust.

From и Into подобны зеркалам друг друга. От абстракции к исходному типу, тогда как в абстракции к целевому типу. Для каждого T есть бланкет Into impl с соответствующей реализацией From<>, поэтому, если вы являетесь автором библиотеки, вам следует подумать о реализации From<> для своих типов и прибегать только к реализациям Into <_> для типов. вы не можете реализовать From для из-за правила сиротства (которое, к сожалению, недостаточно документировано. Ошибка E0117 содержит товары, если вы хотите читать дальше).

Но реализация From / Into - это только полдела. Другая половина - знать, где их использовать. Если вы какое-то время программируете на Rust, вы заметите, что мы, Rustaceans, широко используем систему типов; следовательно, существует много типов, а это означает, что существует большое количество мест, где необходимы преобразования в другие типы.

Время для примера!

Допустим, у нас есть довольно скучная функция foo(bar: &Bar), где Bar - это фактический тип (а не трэйта). В нашем случае у нас также есть тип Blob, который содержит Bar. Также модуль имеет тип Buzz, который можно преобразовать в панель.

Поскольку мы ожидаем, что наш Blob, использующий клиентов, будет использовать функцию foo, мы реализуем From<&Blob> для &Bar, а пока мы находимся в нем, реализуем From<&Buzz> для Bar (возможно, также Buzz без ссылки. Если Buzz - это Copy, мы можем опустить реализация &Buzz и полагаться на принуждение deref). Код такой:

use std::convert::*;

struct Bar { num: i32 }

struct Blob { bar: Bar }

struct Buzz { num: i16 }

impl<'a> From<&'a Blob> for &'a Bar {
    fn from(blob: &'a Blob) -> &'a Bar { &blob.bar }
}

impl<'a> From<&'a Buzz> for Bar {
    fn from(buzz: &Buzz) -> Bar { Bar{ num: buzz.num as i32 } }
}

fn foo(bar: &Bar) {
    let _num = bar.num;
    /* some code here */
}

Теперь у наших клиентов, вызывающих foo(_), есть эти два вызова: foo(&blob.into()) и foo(buzz.into()). Если у нас более двух экземпляров, эти вызовы .into() становятся довольно часто повторяющимися. Если все вызовы выполняются в рамках нашего собственного кода, мы можем сделать вывод, что эта стоимость приемлема, и двигаться дальше. Однако для библиотечных API мы, вероятно, хотим сделать лучше.

Можем ли мы переместить .into() в нашу функцию foo? Возможно, используя трэйту Into? Давай попробуем. Мы меняем foo(_) на следующее:

// foo is now generic: It takes anything that we can turn into a Bar reference 
// of any lifetime (which btw. precludes us from returning the Bar reference)
fn foo<'b, B: Into<&'b Bar>>(bar: B) {
    let _num = bar.into().num;
    /* some code here */
}

Теперь мы можем изменить наши вызовы foo(blob.into()) на foo(blob), что неплохо. К сожалению для нас, мы получаем ошибку при вызове foo(buzz.into()):

conv.rs:30:15: 30:21 error: unable to infer enough type information about `_`; type annotations or generic parameter binding required [E0282]
conv.rs:30     foo(&buzz.into());
                         ^~~~~~
conv.rs:30:15: 30:21 help: run `rustc --explain E0282` to see a detailed explanation
error: aborting due to previous error

Хорошо, rustc не нашел подходящей реализации Into, потому что для этого потребовалось бы автоматическое создание ссылок, что не реализовано в Rust (я мог бы добавить, что по уважительной причине). Проблема здесь в различии между Bar и &Bar. В более простом примере этого даже не произошло бы. Увы, в действительности все не так просто.

Перекресток

Теперь у нас есть несколько вариантов решения этой проблемы. Учитывая, что наш Bar может быть Copy, мы можем просто #[derive(Copy, Clone)] для него и изменить нашу реализацию From<&Blob>, чтобы создать новый Bar без ссылки. Нам также нужно изменить foo(_), чтобы взять любой <B: Into > (удалив &’b), чтобы получить:

impl<'a> From<&'a Blob> for Bar {
    fn from(blob: &'a Blob) -> Bar { blob.bar }
}

fn foo<B: Into<Bar>(bar: B) { .. }

Теперь мы также можем удалить вызов into() из вызовов foo для больших двоичных объектов, и все станет блестящим (обратите внимание, что наш foo фактически создаст копию полосы, содержащейся в любом большом двоичном объекте, но при четырех байтах в стеке это обходится довольно дешево. другие типы, компромисс может быть другим). From спас положение, и мы можем обмануть все кляксы и жужжания по своему желанию. Также пользователи нашего foo могут реализовать Into , чтобы свободно использовать наш foo(_) со своими типами. Радуги и единороги в изобилии!

Другой вариант - пойти на пастбище и найти std::заимствовать::корову (помните?), Что означает внесение небольшой платы за время выполнения для большей гибкости. Здесь нам это не нужно, но для более сложных баров это может окупиться:

use std::convert::*;
use std::borrow::*;

// we still need to derive Clone here or create our own ToOwned implementation, 
// though we don't actually need it here.
#[derive(Clone)]
struct Bar {
    num: i32,
}

struct Blob { bar: Bar }

struct Buzz { num: i16 }

impl<'a> From<&'a Blob> for Cow<'a, Bar> {
    fn from(blob: &'a Blob) -> Cow<'a, Bar> { Cow::Borrowed(&blob.borrow().bar) }
}

impl<'a, 'b> From<&'a Buzz> for Cow<'b, Bar> {
    fn from(buzz: &Buzz) -> Cow<'b, Bar> { Cow::Owned(Bar{ num: buzz.num as i32 }) }
}

fn foo<'b, B: Into<Cow<'b, Bar>>>(bar: B) { .. }

Теперь код обобщается во время компиляции по типу, который, в свою очередь, обобщает во время выполнения, может ли тип превратиться в заимствованный или принадлежащий Bar. В зависимости от того, насколько сложен foo(_) и насколько велик Bar, затраты времени выполнения могут быть незначительными.

(Кстати, doener redditor напоминает мне, что было бы неплохо использовать наш новый foo(..) в качестве оболочки для предыдущей функции foo(_: Bar). Это обеспечит клонирование минимально необходимого кода во время мономорфизация, при которой универсальная функция создается для каждого типа, с которым она вызывается)

Наконец, мы могли отказаться от Into и создать свой собственный трейт AsBar, который мы реализуем как для &Blob, так и для &Buzz, что даст нам максимальную гибкость. Однако это означает, что другим крэйтам, желающим повторно использовать наш foo(_), также понадобятся их типы для реализации нашего трейта AsBar, поэтому нам нужно сделать его общедоступным и задокументировать. Я оставлю эту версию в качестве упражнения читателю.

Подведение итогов

Что мы узнали? Into может сделать наши функции намного более гибкими и предоставить нашим клиентам более удобный интерфейс при реализации библиотеки. Обратной стороной является то, что универсальные шаблоны усложняют документацию, но улучшенная простота использования, вероятно, того стоит - особенно если нашу функцию можно повторно использовать со многими типами, и мы не хотим иметь специальные функции для каждого типа ввода.

Повторное использование From / Into избавляет нас от части работы по созданию, документированию и тестированию наших собственных черт для выполнения общих преобразований в наших методах, и они делают наши интерфейсы очень удобными. Только когда нам понадобится еще больше гибкости, мы можем реализовать для этого свои собственные трэйты.

Бонус!

Redditor летающая овца хотел бы, чтобы я упомянул, что реализация From очень полезна для обработки ошибок (его пример следует):

обычная ошибка упаковки выглядит примерно так (если вы ленивы и не применяете std::error::Error):

#[derive(Debug)]
pub enum Error { TooLong(usize), Io(io::Error) }

impl From<io::Error> for Error {
    fn from(e: io::Error) -> Error { Error::Io(e) }
}

then you’re able to simply use try!(_) to wrap another error into yours.

fn foobar() -> Result<String, Error> {
    let foo = try!(some_io_operation());
    match foo.len() {
        0..12 => Ok(foo),
        n => Err(Error::TooLong(n)),
    }
}

Также redditor killercup порекомендовал крэйт с быстрой ошибкой, который дает нам макрос, позволяющий в спешке танцевать перенос ошибок.

Обсудите на /r/rust и/или rust-users.