Как добиться инкапсуляции полей структуры без заимствования структуры в целом

Мой вопрос уже несколько обсуждался здесь .

Проблема в том, что я хочу получить доступ к нескольким отдельным полям структуры, чтобы использовать их, но я не хочу работать с полями напрямую. Вместо этого я хотел бы инкапсулировать доступ к ним, чтобы получить больше гибкости.

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

Конечно, потеря гибкости для одновременного заимствования различных частей структуры как изменяемых является неприемлемой. Поэтому я хотел бы знать, есть ли какой-нибудь идиоматический способ сделать это в Rust.

Мой наивный подход заключался бы в том, чтобы написать макрос вместо функции, выполняющий (и инкапсулирующий) те же функции.

РЕДАКТИРОВАТЬ: Поскольку Frxstrem предположил, что вопрос, на который я ссылаюсь вверху, может ответить на мой вопрос, я хочу прояснить, что я не ищу какой-либо способ решить эту проблему. У меня вопрос: какое из предложенных решений (если есть) является правильным?


person PSteinhaus    schedule 25.12.2020    source источник


Ответы (1)


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

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

Допустим, у вас есть структура:

struct GraphState {
    nodes: Vec<Node>,
    edges: Vec<Edge>,
}

и вы пытаетесь заимствовать одну часть этой структуры изменчиво, а другую - неизменяемо:

// THIS CODE DOESN'T PASS THE BORROW-CHECKER

impl GraphState {
    pub fn edge_at(&self, edge_index: u16) -> &Edge {
        &self.edges[usize::from(edge_index)]
    }

    pub fn node_at_mut(&mut self, node_index: u8) -> &mut Node {
        &mut self.nodes[usize::from(node_index)]
    }

    pub fn remove_edge(&mut self, edge_index: u16) {
        let edge = self.edge_at(edge_index);    // first (immutable) borrow here
        // update the edge-index collection of the nodes connected by this edge
        for i in 0..2 {
                let node_index = edge.node_indices[i];
                self.node_at_mut(node_index).remove_edge(edge_index);   // second (mutable)
                                                                        // borrow here -> ERROR
            }
        }
    }
}

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

Чтобы обойти это, вы можете просто получить прямой доступ к полям:

impl GraphState {
    pub fn remove_edge(&mut self, edge_index: u16) {
        let edge = &self.edges[usize::from(edge_index)];
        for i in 0..2 {
            let node_index = edge.node_indices[i];
            self.nodes[usize::from(node_index)].remove_edge(edge_index);
        }
    }
}

Этот подход работает, но у него есть два основных недостатка:

  1. Доступные поля должны быть общедоступными (по крайней мере, если вы хотите разрешить доступ к ним из другой области). Если это детали реализации, которые вы бы предпочли сохранить в секрете, вам не повезло.
  2. Вам всегда нужно работать с полями напрямую. Это означает, что код типа usize::from(node_index) необходимо повторять повсюду, что делает этот подход хрупким и громоздким.

Так как же решить эту проблему?

А) Брать все сразу

Поскольку одновременное заимствование self несколько раз не разрешено, взаимное заимствование всех частей одновременно является одним из простых способов решить эту проблему:

pub fn edge_at(edges: &[Edge], edge_index: u16) -> &Edge {
    &edges[usize::from(edge_index)]
}
pub fn node_at_mut(nodes: &mut [Node], node_index: u8) -> &mut Node {
    &mut nodes[usize::from(node_index)]
}

impl GraphState {
    pub fn data_mut(&mut self) -> (&mut [Node], &mut [Edge]) {
        (&mut self.nodes, &mut self.edges)
    }

    pub fn remove_edge(&mut self, edge_index: u16) {
        let (nodes, edges) = self.data_mut();    // first (mutable) borrow here
        let edge = edge_at(edges, edge_index);    
        // update the edge-index collection of the nodes connected by this edge
        for i in 0..2 {
                let node_index = edge.node_indices[i];
                node_at_mut(nodes, node_index).remove_edge(edge_index);   // no borrow here
                                                                          // -> no error
            }
        }
    }
}

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

Б) Используйте макрос

Если повторное использование кода - это все, о чем вы беспокоитесь, а видимость для вас не проблема, вы можете написать такие макросы:

macro_rules! node_at_mut {
    ($this:ident, $index:expr) => {
        &mut self.nodes[usize::from($index)]
    }
}
macro_rules! edge_at {
    ($this:ident, $index:expr) => {
        &mut self.edges[usize::from($index)]
    }
}
...
pub fn remove_edge(&mut self, edge_index: u16) {
        let edge = edge_at!(self, edge_index);
        // update the edge-index collection of the nodes connected by this edge
        for i in 0..2 {
                let node_index = edge.node_indices[i];
                node_at_mut!(self, node_index).remove_edge(edge_index);
        }
    }
}

Если ваши поля в любом случае являются общедоступными, я бы, вероятно, выбрал это решение, так как оно мне кажется наиболее элегантным.

C) Иди небезопасно

То, что мы хотим здесь сделать, очевидно, безопасно. К сожалению, проверяющий заимствования не может этого увидеть, поскольку сигнатуры функций не говорят ему ничего, кроме того, что self заимствуется. К счастью, Rust позволяет нам использовать ключевое слово unsafe в таких случаях:

pub fn remove_edge(&mut self, edge_index: u16) {
    let edge: *const Edge = self.edge_at(edge_index);
    for i in 0..2 {
        unsafe {
            let node_index = (*edge).node_indices[i];
            self.node_at_mut(node_index).remove_edge(edge_index);   // first borrow here
                                                                    // (safe though since remove_edge will not invalidate the first pointer)
        }
    }
}

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

РЕДАКТИРОВАТЬ: Подумав, я понял, что мы знаем, что этот подход безопасен здесь, только потому что мы знаем о реализации. В другом варианте использования данные, о которых идет речь, могут фактически содержаться в одном поле (скажем, в карте), даже если кажется, что мы обращаемся к двум очень разным типам данных при вызове извне. Вот почему этот последний подход по праву небезопасен, поскольку программа проверки заимствований не может проверить, действительно ли мы заимствуем разные вещи, без раскрытия закрытых полей, что делает наши усилия бессмысленными.

Вопреки моему первоначальному мнению, это даже не может быть изменено расширением языка. Причина в том, что в некоторых способ, чтобы это работало.

Написав этот ответ, я также нашел это сообщение в блоге, который немного глубже рассматривает возможные решения (а также упоминает некоторые продвинутые методы, которые мне не приходили в голову, хотя ни один из них не применим повсеместно). Если вы знаете другое решение или способ улучшить предложенные здесь, сообщите мне.

person PSteinhaus    schedule 26.12.2020