Как пользоваться типажами From и Into

оригинал: llogic • перевод: Сергей Ефремов • обучение • поддержите на Patreon

(Эта статья написана для Rust 1.4, и код по-прежнему работает в 1.7 без изменений)

Пришло время поговорить про типажи From и Into (и некоторые с ними связанные) и задать вопрос, где и когда их использовать. Заметьте, что есть ещё специфические типажи (например, IntoIterator) и более конкретные их варианты (например, FromStr); надо быть в курсе всего этого при написании кода на Rust.

From и Into зеркально похожи. From является абстракцией над исходным типом, а Into над целевым. Для каждого T, реализующего From<_>, есть общая форма реализации Into, поэтому если вы пишете библиотеку, то для ваших типов реализуйте From<_>, прибегая к реализации Into<_> только для тех типов, которым нельзя реализовать From из-за правила сироты (orphan rule) (оно очень бедно описано в официальной документации. Если хотите узнать больше, [Error E0117](https://doc.rust-lang.org/error-index.html#E0117) содержит его описание).

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

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

Предположим, что у нас есть очень скучная функция foo(bar: & Bar), в которой Bar является действительным типом (не типажом). Ещё у нас есть тип Blob, содержащий Bar, и тип Buzz, который можно преобразовать в Bar.

Мы ожидаем, что клиенты, использующие Blob, будут использовать функцию foo, поэтому реализуем From<& Blob> для & Bar и заодно, реализуем ещё From<& Buzz> для Bar (а также, наверное, и From. Если Buzz реализует Copy, мы можем опустить реализацию для & Buzz и положиться на неявное приведение типа при разыменовании). [Код](https://play.rust-lang.org/?gist=13e3caea0066b4864ec9&version=stable) выглядит так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
    /* какой-то дополнительный код */
}

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

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

1
2
3
4
5
6
// теперь foo обобщённый: он принимает все, что может превратиться в ссылку на Bar
// с любым временем жизни (что, помимо всего прочего, не позволит нам вернуть ссылку на Bar)
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()):

1
2
3
4
5
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 and & Bar. В примере попроще этого бы не произошло. Увы, в реальности все не так просто.

Распутье

Итак, у нас есть несколько вариантов решения этой проблемы. Можем реализовывать Copy для нашего Bar, дописав ему #[derive(Copy, Clone)], и поменять нашу реализацию From<& Blob> так, чтобы она создавала новый Bar без ссылки. Также придётся поменять foo(_) для приёма любых <B: Into> (удалить & ’b) и [получим](https://play.rust-lang.org/?gist=cea08895a3e1ce1e98db&version=stable):

1
2
3
4
5
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 будет создавать копию bar, содержащегося в любом blob, что для четырёх байт на стеке не так уж и дорого. Для других типов такой компромисс может и не подойти). From спас положение, и теперь мы можем вызывать foo для всех blob и buzz как только нашей душе угодно. Также пользователи нашего foo могут реализовать Into, чтобы спокойно использовать наш foo(_) с их собственными типами. Сплошь радуги и единороги!

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::convert::*;
use std::borrow::*;

// нам по-прежнему надо написать derive Clone или создать собственную реализацию ToOwned,
// хотя она нам здесь и не нужна.
#[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, цена во время компиляции может стать ничтожно малой.

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

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

Связываем все вместе

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

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

Бонус!

Участник Reddit flying-sheep попросил меня отметить, что реализация From очень полезна во время обработки ошибок (его пример идёт дальше):

обычная обёртка Error выглядит как-то так (если вам лень реализовывать std: error: Error):

1
2
3
4
5
6
#[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) }
}

тогда вы можете просто использовать try! (_) для оборачивания других ошибок в ваши.

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

Также участник reddit killercup рекомендовал контейнер quick-error, который позволяет очень быстро добавлять обработку перехвата ошибок.

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

Профиль автора статьи на GitHub