Как вернуть * необязательную * ссылку в содержимое RefCell

У меня есть тип, который хранит свои данные в контейнере за Rc<RefCell<>>, который по большей части скрыт от общедоступного API. Например:

struct Value;

struct Container {
    storage: Rc<RefCell<HashMap<u32, Value>>>,
}

impl Container {
    fn insert(&mut self, key: u32, value: Value) {
        self.storage.borrow_mut().insert(key, value);
    }

    fn remove(&mut self, key: u32) -> Option<Value> {
        self.storage.borrow_mut().remove(&key)
    }

    // ...
}

Однако для того, чтобы заглянуть внутрь контейнера, необходимо вернуть Ref. Этого можно добиться с помощью Ref::map() - для пример:

// peek value under key, panicking if not present
fn peek_assert(&self, key: u32) -> Ref<'_, Value> {
    Ref::map(self.storage.borrow(), |storage| storage.get(&key).unwrap())
}

Однако мне бы хотелось иметь версию peek без паники, которая вернула бы Option<Ref<'_, Value>>. Это проблема, потому что Ref::map требует, чтобы вы вернули ссылку на что-то, что существует внутри RefCell, поэтому даже если бы я хотел вернуть Ref<'_, Option<Value>>, это не сработало бы, потому что параметр, возвращаемый storage.get(), недолговечен.

Попытка использовать Ref::map для создания Ref из ранее найденного ключа также не компилируется:

// doesn't compile apparently the borrow checker doesn't understand that `v`
// won't outlive `_storage`.
fn peek(&self, key: u32) -> Option<Ref<'_, Value>> {
    let storage = self.storage.borrow();
    if let Some(v) = storage.get(&key) {
        Some(Ref::map(storage, |_storage| v))
    } else {
        None
    }
}

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

// works, but does lookup 2x
fn peek(&self, key: u32) -> Option<Ref<'_, Value>> {
    if self.storage.borrow().get(&key).is_some() {
        Some(Ref::map(self.storage.borrow(), |storage| {
            storage.get(&key).unwrap()
        }))
    } else {
        None
    }
}

Компилируемый пример на игровой площадке.

Связанные вопросы, такие как this one предполагают, что внутренняя ссылка всегда доступна, поэтому у них нет этой проблемы.

Я нашел Ref::filter_map(), который решить эту проблему, но в стабильной версии он еще не доступен, и это неясно как далеко до стабилизации. За исключением других вариантов, я бы согласился с решением, в котором используется unsafe, при условии, что оно надежно и основано на задокументированных гарантиях.


person user4815162342    schedule 24.04.2021    source источник


Ответы (3)


Вы можете использовать побочный эффект, чтобы сообщить, был ли поиск успешным, а затем вернуть произвольное значение из Ref::map, если у вас нет успешного значения.

impl Container {
    // ...

    fn peek(&self, key: u32) -> Option<Ref<'_, Value>> {
        let storage = self.storage.borrow();
        if storage.is_empty() {
            // The trick below requires the map to be nonempty, but if it's
            // empty, then we don't need to do a lookup.
            return None;
        }

        // Find either the correct value or an arbitrary one, and use a mutable
        // side channel to remember which one it is.
        let mut failed = false;
        let ref_maybe_bogus: Ref<'_, Value> = Ref::map(storage, |storage| {
            storage.get(&key).unwrap_or_else(|| {
                // Report that the lookup failed.
                failed = true;
                // Return an arbitrary Value which will be ignored.
                // The is_empty() check above ensured that one will exist.
                storage.values().next().unwrap()
            })
        });
        
        // Return the ref only if it's due to a successful lookup.
        if failed {
            None
        } else {
            Some(ref_maybe_bogus)
        }
    }
}

Доработки:

  • Если тип Value может иметь постоянные экземпляры, вы можете вернуть один из них, вместо того, чтобы требовать, чтобы карта была непустой; приведенный выше метод является наиболее общим, который работает для любого определения Value, а не самым простым. (Это возможно, поскольку &'static Value удовлетворяет требованиям Ref - ссылка просто должна существовать достаточно долго, а не указывать на содержимое RefCell.)

  • Если тип Value может иметь постоянный экземпляр , отличный от любого значимого экземпляра, который можно найти на карте (контрольное значение), то вы можете проверить это значение в последнем if вместо проверки отдельная логическая переменная. Однако это не особо упрощает код; в основном это полезно, если у вас есть дозорный, который вы все равно используете для других целей, или если вам нравится «чисто функциональный» стиль кодирования, который позволяет избежать побочных эффектов.

И, конечно, это все спорный вопрос, если Ref::filter_map станет стабильным.

person Kevin Reid    schedule 24.04.2021
comment
Это действительно изобретательная идея, которая становится очевидной, когда на нее указывает кто-то другой. Реализация немного сложнее, чем хотелось бы, и требует комментариев, но она на 100% безопасна и отлично отвечает на вопрос, поэтому я приму ее, если не появится что-то еще лучшее (в чем я сомневаюсь). В производстве я буду использовать другой подход, но это отличное безопасное решение для тех, кому нужен peek(), который возвращает Фактическая ссылка, как описано в вопросе. - person user4815162342; 25.04.2021

Мне удалось это придумать:

fn peek<'a>(&'a self, key: u32) -> Option<Ref<'a, Value>> {
    // Safety: we perform a guarded borrow, then an unguarded one.
    // If the former is successful, so must be the latter.
    // Conceptually, they are the same borrow: we just take the pointer
    // from one and the dynamic lifetime guard from the other.
    unsafe {
        let s = self.storage.borrow();
        let u = self.storage.try_borrow_unguarded().unwrap();
        u.get(&key).map(|v| Ref::map(s, |_| &*(v as *const _)))
    }
}

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

Я думаю, что это правильно. Тем не менее, я бы с нетерпением ждал filter_map, просто на всякий случай.


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

fn peek<'a>(&'a self, key: u32) -> Option<Ref<'a, Value>> {
    // Safety: we convert the reference obtained from the guarded borrow
    // into a pointer. Dropping the reference allows us to consume the
    // original borrow guard and turn it into a new one (with the same
    // lifetime) that refers to the value inside the hashmap.
    let s = self.storage.borrow();
    s.get(&key)
        .map(|v| v as *const _)
        .map(|v| Ref::map(s, |_| unsafe { &*v }))
}
person user3840170    schedule 24.04.2021
comment
Это довольно элегантно, спасибо. Но если мы все равно конвертируем v в указатель, тогда, возможно, нам вообще не понадобится второй заимствование, мы можем делать все с первым, если мы делаем это в два этапа: 1) получаем значение из карты и конвертируем его на указатель (таким образом отделив его от заимствования), а затем вызывается Ref::map, закрытие которого преобразует указатель обратно в ссылку: play.rust-lang.org/ - person user4815162342; 24.04.2021
comment
Думаю, это тоже работает. Я пытался придумать ситуацию, в которой он может сломаться, но не мог придумать ни одной. Здесь вроде не хуже моего решения. - person user3840170; 24.04.2021

Вот решение, которое я использовал, пока Ref::filter_map() не стабилизировался. Он изменяет подпись peek() от указанной в вопросе, поэтому я не приму этот ответ, но он может быть полезен для других, кто наткнется на эту проблему.

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

fn peek<F: FnOnce(&Value) -> R, R>(&self, key: u32, examine: F) -> Option<R> {
    self.storage.borrow().get(&key).map(examine)
}

Где с peek(), как указано изначально, можно было бы написать:

if let Some(event) = container.peek() {
    if event.time() >= deadline {
        container.remove_next();
    }
}

... с peek() из этого ответа вместо этого можно было бы написать:

if let Some(event_time) = container.peek(|e| e.time()) {
    if event_time >= deadline {
        container.remove_next();
    }
}
person user4815162342    schedule 24.04.2021