Как вы можете использовать неизменяемую опцию по ссылке, которая содержит изменяемую ссылку?

Вот Thing:

struct Thing(i32);

impl Thing {
    pub fn increment_self(&mut self) {
        self.0 += 1;
        println!("incremented: {}", self.0);
    }
}

А вот функция, которая пытается изменить Thing и возвращает либо true, либо false, в зависимости от того, Thing доступен:

fn try_increment(handle: Option<&mut Thing>) -> bool {
    if let Some(t) = handle {
        t.increment_self();
        true
    } else {
        println!("warning: increment failed");
        false
    }
}

Вот пример использования:

fn main() {
    try_increment(None);

    let mut thing = Thing(0);
    try_increment(Some(&mut thing));
    try_increment(Some(&mut thing));

    try_increment(None);
}

Как написано выше, все работает просто отлично (ссылка на площадку Rust). Выход ниже:

warning: increment failed
incremented: 1
incremented: 2
warning: increment failed

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

fn try_increment_twice(handle: Option<&mut Thing>) {
    try_increment(handle);
    try_increment(handle);
}

fn main() {
    try_increment_twice(None);

    let mut thing = Thing(0);
    try_increment_twice(Some(&mut thing));

    try_increment_twice(None);
}

Ошибка имеет смысл. Первый звонок try_increment(handle) дает право собственности на handle прочь и поэтому второй звонок незаконен. Как это часто бывает, компилятор Rust выдает разумное сообщение об ошибке:

   |
24 |     try_increment(handle);
   |                   ------ value moved here
25 |     try_increment(handle);
   |                   ^^^^^^ value used here after move
   |

В попытке решить эту проблему, я подумал, что имеет смысл передать handle по ссылке. Это должна быть незыблемая ссылка, ум, потому что я не хочу try_increment быть в состоянии изменить handle сам (присваивая None к нему, например), только чтобы иметь возможность вызывать мутации по его значению.

Моя проблема в том, что я не мог понять, как это сделать.

Вот самая близкая рабочая версия, которую я мог получить:

struct Thing(i32);

impl Thing {
    pub fn increment_self(&mut self) {
        self.0 += 1;
        println!("incremented: {}", self.0);
    }
}

fn try_increment(handle: &mut Option<&mut Thing>) -> bool {
    // PROBLEM: this line is allowed!
    // (*handle) = None;

    if let Some(ref mut t) = handle {
        t.increment_self();
        true
    } else {
        println!("warning: increment failed");
        false
    }
}

fn try_increment_twice(mut handle: Option<&mut Thing>) {
    try_increment(&mut handle);
    try_increment(&mut handle);
}

fn main() {
    try_increment_twice(None);

    let mut thing = Thing(0);
    try_increment_twice(Some(&mut thing));

    try_increment_twice(None);
}

Этот код работает, как и ожидалось, но Option теперь передается по изменяемой ссылке, и это не то, что я хочу:

  • Мне разрешено мутировать Option переназначая None к нему, нарушая все последующие мутации. (Раскомментируйте строку 12 ((*handle) = None;) например.)
  • Это грязно: есть много посторонних &mutврёт.
  • Это вдвойне грязно: Небеса знают только, почему я должен использовать ref mut в if let заявление в то время как соглашение заключается в использовании &mut где-либо еще.
  • Это лишает цели наличия сложных правил проверки заимствований и проверки изменчивости в компиляторе.

Есть ли способ на самом деле достичь того, что я хочу: передать неизменное Option где, по ссылке, и на самом деле в состоянии использовать его содержимое?

2 ответа

Решение

TL;DR: ответ - нет, я не могу.

После обсуждения с @Peter Hall и @Stargateur я понял, почему мне нужно использовать &mut Option<&mut Thing> везде. RefCell<> также было бы возможным обходным путем, но он не аккуратен и на самом деле не соответствует шаблону, который я изначально стремился реализовать.

Проблема заключается в следующем: если разрешить видоизменять объект, для которого имеется только неизменная ссылка на Option<&mut T> можно использовать эту власть, чтобы полностью нарушить правила заимствования. Конкретно, у вас может быть много изменяемых ссылок на один и тот же объект, потому что у вас может быть много таких неизменных ссылок.

Я знал, что была только одна изменчивая ссылка на Thing (принадлежит Option<>) но, как только я начал брать ссылки на Option<>компилятор больше не знал, что их не так много.

Лучший вариант шаблона выглядит следующим образом:

fn try_increment(handle: &mut Option<&mut Thing>) -> bool {
    if let Some(ref mut t) = handle {
        t.increment_self();
        true
    }
    else {
        println!("warning: increment failed");
        false
    }
}

fn try_increment_twice(mut handle: Option<&mut Thing>) {
    try_increment(&mut handle);
    try_increment(&mut handle);
}

fn main() {
    try_increment_twice(None);

    let mut thing = Thing(0);
    try_increment_twice(Some(&mut thing));

    try_increment_twice(None);
}

Заметки:

  1. Option<> содержит единственную существующую изменчивую ссылку на Thing
  2. try_increment_twice() вступает во владение Option<>
  3. try_increment() должен взять Option<> как &mut так что компилятор знает, что он имеет единственную изменяемую ссылку на Option<>во время разговора
  4. Если компилятор знает, что try_increment() имеет единственную изменчивую ссылку на Option<> который содержит уникальную изменяемую ссылку на ThingКомпилятор знает, что правила заимствования не были нарушены.

Еще один эксперимент

Проблема изменчивости Option<> остается, потому что можно позвонить take() и другие. на изменчивом Option<>нарушая все следующее.

Чтобы реализовать шаблон, который я хотел, мне нужно что-то вроде Option<> но, даже если он изменчив, он не может быть видоизменен. Что-то вроде этого:

struct Handle<'a> {
    value: Option<&'a mut Thing>,
}

impl<'a> Handle<'a> {
    fn new(value: &'a mut Thing) -> Self {
        Self {
            value: Some(value),
        }
    }

    fn empty() -> Self {
        Self {
            value: None,
        }
    }

    fn try_mutate<T, F: Fn(&mut Thing) -> T>(&mut self, mutation: F) -> Option<T> {
        if let Some(ref mut v) = self.value {
            Some(mutation(v))
        }
        else {
            None
        }
    }
}

Теперь я подумал, что могу обойти &mut Handleцелый день и знаю, что тот, кто имеет Handle может изменять только его содержимое, а не сам дескриптор. ( См. Детская площадка)

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

fn try_increment(handle: &mut Handle) -> bool {
    if let Some(_) = handle.try_mutate(|t| { t.increment_self() }) {
        // This breaks future calls:
        (*handle) = Handle::empty();

        true
    }
    else {
        println!("warning: increment failed");
        false
    }
}

Что все хорошо и хорошо.

Итог: просто используйте &mut Option<&mut T>

Вы не можете извлечь изменяемую ссылку из неизменной, даже ссылку на ее внутреннюю часть. Это своего рода точка! Допускается использование нескольких псевдонимов неизменяемых ссылок, поэтому, если Rust разрешил вам сделать это, вы можете столкнуться с ситуацией, когда два фрагмента кода могут одновременно изменять данные.

Rust предоставляет несколько аварийных люков для изменчивости интерьера, например RefCell:

use std::cell::RefCell;

fn try_increment(handle: &Option<RefCell<Thing>>) -> bool {
    if let Some(t) = handle {
        t.borrow_mut().increment_self();
        true
    } else {
        println!("warning: increment failed");
        false
    }
}

fn try_increment_twice(handle: Option<RefCell<Thing>>) {
    try_increment(&handle);
    try_increment(&handle);
}

fn main() {
    let mut thing = RefCell::new(Thing(0));
    try_increment_twice(Some(thing));
    try_increment_twice(None);
}
Другие вопросы по тегам