【问题标题】:Emulate BTreeMap::pop_last in stable Rust 1.65 or older在稳定的 Rust 中模拟 BTreeMap::pop_last
【发布时间】:2022-03-16 00:55:35
【问题描述】:

在目前稳定的Rust中,有没有办法写一个相当于BTreeMap::pop_last的函数?

我能想到的最好的方法是:

fn map_pop_last<K, V>(m: &mut BTreeMap<K, V>) -> Option<(K, V)>
where
    K: Ord + Clone,
{
    let last = m.iter().next_back();
    if let Some((k, _)) = last {
        let k_copy = k.clone();
        return m.remove_entry(&k_copy);
    }
    None
}

它可以工作,但它要求密钥是可克隆的。 Rust nightly 中的 BTreeMap::pop_last 没有施加这样的约束。

如果我像这样删除克隆

fn map_pop_last<K, V>(m: &mut BTreeMap<K, V>) -> Option<(K, V)>
where
    K: Ord,
{
    let last = m.iter().next_back();
    if let Some((k, _)) = last {
        return m.remove_entry(k);
    }
    None
}

它导致

error[E0502]: cannot borrow `*m` as mutable because it is also borrowed as immutable
  --> ...
   |
.. |     let last = m.iter().next_back();
   |                -------- immutable borrow occurs here
.. |     if let Some((k, _)) = last {
.. |         return m.remove_entry(k);
   |                ^^------------^^^
   |                | |
   |                | immutable borrow later used by call
   |                mutable borrow occurs here

有没有办法在不对映射键和值类型施加额外限制的情况下解决此问题?

【问题讨论】:

  • 我认为不可能在安全的 Rust 中实现这一点。您也许可以从 std 复制源代码块,以获得相同的实现。
  • @PeterHall 不幸的是,source code 使用了 alloc::collections::btree::borrow::DormantMutRef,它无法通过公共 API 访问。您需要重新实现它或找到其他方法。
  • 更笼统地说,复制部分不稳定的源代码并不比每晚编译更安全,可能更安全。
  • @Anonyme2000 复制不稳定源的特定部分绝对比每晚使用更安全。
  • @Anonyme2000 是的,您需要复制它使用的所有内部类型,而不仅仅是函数体。

标签: rust


【解决方案1】:

有没有办法在不对映射键和值类型施加额外限制的情况下解决这个问题?

它在 safe Rust 中似乎不可行,至少在合理的算法复杂度下不可行。 (有关通过重新构建整个地图的解决方案,请参阅 Aiden4 的答案。)

但是,如果您被允许使用 unsafe,并且如果您有足够的决心想要深入研究它,那么这段代码可以做到:

// Safety: if key uses shared interior mutability, the comparison function
// must not use it. (E.g. it is not allowed to call borrow_mut() on a
// Rc<RefCell<X>> inside the key). It is extremely unlikely that such a
// key exists, but it's possible to write it, so this must be marked unsafe.
unsafe fn map_pop_last<K, V>(m: &mut BTreeMap<K, V>) -> Option<(K, V)>
where
    K: Ord,
{
    // We make a shallow copy of the key in the map, and pass a
    // reference to the copy to BTreeMap::remove_entry(). Since
    // remove_entry() is not dropping the key/value pair (it's returning
    // it), our shallow copy will remain throughout the lifetime of
    // remove_entry(), even if the key contains references.
    let (last_key_ref, _) = m.iter().next_back()?;
    let last_key_copy = ManuallyDrop::new(std::ptr::read(last_key_ref));
    m.remove_entry(&last_key_copy)
}

Playground

【讨论】:

  • 请注意,即使remove_entry 没有删除密钥,它也可能在返回之前在内部移动它,这会使引用在返回之前无效(尽管它可能不会尝试使用引用到那时再解释为什么 MIRI 不抱怨)。
  • @Jmb 否。它不会使引用无效,因为引用指向密钥的本地副本。我的评论所指的有效性是在密钥内部包含更多引用的情况下,这些引用会因它被删除而失效。 (但不会,因为它会转移到调用者。)
  • 哦,对了。所以它应该是安全的,除非密钥是某种奇异的类型,在它的比较函数中做了一些有趣(和不安全)的事情。
  • @Jmb 我想我现在想出了一个例子,其中安全代码导致不健全(调用我的函数时):play.rust-lang.org/… 诀窍是用RefCell&lt;Box&lt;i32&gt;&gt; 替换Box&lt;RefCell&lt;i32&gt;&gt;。然后self.rself.r 上的borrow_mut()other.r 被解耦并不要惊慌,它们的.as_mut() 将两个可变引用分配给同一个i32,即UB。跨度>
  • 稍作修改,我们可以让 miri 抱怨:play.rust-lang.org/…
【解决方案2】:

有没有办法在不对映射键和值类型施加额外限制的情况下解决此问题?

在安全防锈的情况下你不能有效地做到这一点,但它是可能的:

fn map_pop_last<K, V>(m: &mut BTreeMap<K, V>) -> Option<(K, V)>
where
    K: Ord,
{
    let mut temp = BTreeMap::new();
    std::mem::swap(m, &mut temp);
    let mut iter = temp.into_iter();
    let ret = iter.next_back();
    m.extend(iter);
    ret  
}

playground

这将完整遍历地图,但很安全。

【讨论】:

    【解决方案3】:

    我们甚至可以确定,使用安全 Rust 中当前提供的接口(在撰写本文时为 1.59 版)不可能就地改变映射:

    要提取密钥,我们需要查看地图的密钥。 “看”在这里表示借用。这给了我们一个&amp;K 引用,它也借用了整个地图。

    现在,出现了一个问题:要调用这些remove* 方法中的任何一个,我们需要再次可变地借用映射,我们不能这样做,因为我们仍然持有对我们将要提取的键的引用——这些生命周期重叠,它不会编译。

    另一种方法是尝试获取Entry,但要通过.entry 方法获取一个,我们也需要拥有一个K

    【讨论】:

      猜你喜欢
      • 2022-01-25
      • 2023-03-16
      • 2022-11-18
      • 1970-01-01
      • 1970-01-01
      • 2021-08-31
      • 1970-01-01
      • 2011-01-31
      • 1970-01-01
      相关资源
      最近更新 更多