Ассоциированные типы

Ассоциированные (связанные) типы — это мощная часть системы типов в Rust. Они связаны с идеей 'семейства типа', другими словами, группировки различных типов вместе. Это описание немного абстрактно, так что давайте разберем на примере. Если вы хотите написать типаж Graph, то нужны два обобщенных параметра типа: тип узел и тип ребро. Исходя из этого, вы можете написать типаж Graph<N, E>, который выглядит следующим образом:

trait Graph<N, E> {
    fn has_edge(&self, &N, &N) -> bool;
    fn edges(&self, &N) -> Vec<E>;
    // etc
}

Такое решение вроде бы достигает своей цели, но, в конечном счете, является неудобным. Например, любая функция, которая принимает Graph в качестве параметра, также должна быть обобщённой с параметрами N и E:

fn distance<N, E, G: Graph<N, E>>(graph: &G, start: &N, end: &N) -> u32 { ... }

Наша функция расчета расстояния работает независимо от типа Edge, поэтому параметр E в этой сигнатуре является лишним и только отвлекает.

Что действительно нужно заявить, это чтобы сформировать какого-либо вида Graph, нужны соответствующие типы E и N, собранные вместе. Мы можем сделать это с помощью ассоциированных типов:

trait Graph {
    type N;
    type E;

    fn has_edge(&self, &Self::N, &Self::N) -> bool;
    fn edges(&self, &Self::N) -> Vec<Self::E>;
    // etc
}

Теперь наши клиенты могут абстрагироваться от определенного Graph:

fn distance<G: Graph>(graph: &G, start: &G::N, end: &G::N) -> u32 { ... }

Больше нет необходимости иметь дело с типом E!

Давайте поговорим обо всем этом более подробно.

Определение ассоциированных типов

Давайте построим наш типаж Graph. Вот его определение:

trait Graph {
    type N;
    type E;

    fn has_edge(&self, &Self::N, &Self::N) -> bool;
    fn edges(&self, &Self::N) -> Vec<Self::E>;
}

Достаточно просто. Ассоциированные типы используют ключевое слово type, и расположены внутри тела типажа, наряду с функциями.

Эти объявления type могут иметь все то же самое, как и при работе с функциями. Например, если бы мы хотели, чтобы тип N реализовывал Display, чтобы была возможность печатать узлы, мы могли бы сделать следующее:

use std::fmt;

trait Graph {
    type N: fmt::Display;
    type E;

    fn has_edge(&self, &Self::N, &Self::N) -> bool;
    fn edges(&self, &Self::N) -> Vec<Self::E>;
}

Реализация ассоциированных типов

Типаж, который включает ассоциированные типы, как и любой другой типаж, для реализации использует ключевое слово impl. Вот простая реализация Graph:

# trait Graph {
#     type N;
#     type E;
#     fn has_edge(&self, &Self::N, &Self::N) -> bool;
#     fn edges(&self, &Self::N) -> Vec<Self::E>;
# }
struct Node;

struct Edge;

struct MyGraph;

impl Graph for MyGraph {
    type N = Node;
    type E = Edge;

    fn has_edge(&self, n1: &Node, n2: &Node) -> bool {
        true
    }

    fn edges(&self, n: &Node) -> Vec<Edge> {
        Vec::new()
    }
}

Это глупая реализация, которая всегда возвращает true и пустой Vec<Edge>, но она дает вам общее представление о том, как реализуются такие ​​вещи. Для начала нужны три struct, одна для графа, одна для узла и одна для ребра. В этой реализации используются struct для всех трех сущностей, но вполне могли бы использоваться и другие типы, которые работали бы так же хорошо, если бы реализация была более продвинутой.

Затем идет строка с impl, которая является такой же, как и при реализации любого другого типажа.

Далее мы используем знак =, чтобы определить наши ассоциированные типы. Имя типажа идет слева от знака =, а конкретный тип, для которого мы impl этот типаж, идет справа. Наконец, мы используем конкретные типы при объявлении функций.

Типажи-объекты и ассоциированные типы

Вот еще немного синтаксиса, о котором следует упомянуть: типажи-объекты. Если вы попытаетесь создать типаж-объект из ассоциированного типа, как в этом примере:

# trait Graph {
#     type N;
#     type E;
#     fn has_edge(&self, &Self::N, &Self::N) -> bool;
#     fn edges(&self, &Self::N) -> Vec<Self::E>;
# }
# struct Node;
# struct Edge;
# struct MyGraph;
# impl Graph for MyGraph {
#     type N = Node;
#     type E = Edge;
#     fn has_edge(&self, n1: &Node, n2: &Node) -> bool {
#         true
#     }
#     fn edges(&self, n: &Node) -> Vec<Edge> {
#         Vec::new()
#     }
# }
let graph = MyGraph;
let obj = Box::new(graph) as Box<Graph>;

Вы получите две ошибки:

error: the value of the associated type `E` (from the trait `main::Graph`) must
be specified [E0191]
let obj = Box::new(graph) as Box<Graph>;
          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
24:44 error: the value of the associated type `N` (from the trait
`main::Graph`) must be specified [E0191]
let obj = Box::new(graph) as Box<Graph>;
          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Мы не сможем создать типаж-объект, подобный этому, потому что у него нет информации об ассоциированных типах. Вместо этого, мы можем написать так:

# trait Graph {
#     type N;
#     type E;
#     fn has_edge(&self, &Self::N, &Self::N) -> bool;
#     fn edges(&self, &Self::N) -> Vec<Self::E>;
# }
# struct Node;
# struct Edge;
# struct MyGraph;
# impl Graph for MyGraph {
#     type N = Node;
#     type E = Edge;
#     fn has_edge(&self, n1: &Node, n2: &Node) -> bool {
#         true
#     }
#     fn edges(&self, n: &Node) -> Vec<Edge> {
#         Vec::new()
#     }
# }
let graph = MyGraph;
let obj = Box::new(graph) as Box<Graph<N=Node, E=Edge>>;

Синтаксис N=Node позволяет нам предоставлять конкретный тип, Node, для параметра типа N. То же самое и для E=Edge. Если бы мы не предоставляли это ограничение, то не могли бы знать наверняка, какая impl соответствует этому типажу-объекту.