Откуда ты::откуда
Перевод | Автор оригинала: Andre Bogus
А во что ты превратился::во что?
(Эта статья написана на Rust 1.4)
На этот раз давайте взглянем на трэйты From и Into (и некоторые связанные) и зададим вопрос, когда и для чего их использовать. Обратите внимание, что есть специализации (например, IntoIterator) и варианты (например, FromStr) этих черт; хорошо знать об этом при написании кода на Rust.
From и Into подобны зеркалам друг друга. От абстракции к исходному типу, тогда как в абстракции к целевому типу. Для каждого T есть бланкет Into
Но реализация 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
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
Другой вариант - пойти на пастбище и найти 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.