我可以将可变切片引用重新分配给自身的子切片吗?

Posted

技术标签:

【中文标题】我可以将可变切片引用重新分配给自身的子切片吗?【英文标题】:Can I reassign a mutable slice reference to a sub-slice of itself? 【发布时间】:2020-07-28 02:21:41 【问题描述】:

我正在实现一个类似堆栈的结构,其中该结构包含对切片的可变引用。

struct StackLike<'a, X> 
    data: &'a mut [X],

我希望能够从堆栈中弹出最后一个元素,例如:

impl<'a, X> StackLike<'a, X> 
    pub fn pop(&mut self) -> Option<&'a X> 
        if self.data.is_empty() 
            return None;
        
        let n = self.data.len();
        let result = &self.data[n - 1];
        self.data = &mut self.data[0..n - 1];
        Some(result)
    

这失败了:

error[E0495]: cannot infer an appropriate lifetime for lifetime parameter in function call due to conflicting requirements
  --> src/lib.rs:11:23
   |
11 |         let result = &self.data[n - 1];
   |                       ^^^^^^^^^^^^^^^^
   |
note: first, the lifetime cannot outlive the anonymous lifetime #1 defined on the method body at 6:5...
  --> src/lib.rs:6:5
   |
6  | /     pub fn pop(&mut self) -> Option<&'a X> 
7  | |         if self.data.is_empty() 
8  | |             return None;
9  | |         
...  |
13 | |         Some(result)
14 | |     
   | |_____^
note: ...so that reference does not outlive borrowed content
  --> src/lib.rs:11:23
   |
11 |         let result = &self.data[n - 1];
   |                       ^^^^^^^^^
note: but, the lifetime must be valid for the lifetime `'a` as defined on the impl at 5:6...
  --> src/lib.rs:5:6
   |
5  | impl<'a, X> StackLike<'a, X> 
   |      ^^
note: ...so that the expression is assignable
  --> src/lib.rs:13:9
   |
13 |         Some(result)
   |         ^^^^^^^^^^^^
   = note: expected  `std::option::Option<&'a X>`
              found  `std::option::Option<&X>`

即使a simplified version of pop 不返回值并且只缩小切片也不起作用。

impl<'a, X> StackLike<'a, X> 
    pub fn pop_no_return(&mut self) 
        if self.data.is_empty() 
            return;
        
        let n = self.data.len();
        self.data = &mut self.data[0..n - 1];
    

给了

error[E0495]: cannot infer an appropriate lifetime for lifetime parameter in function call due to conflicting requirements
  --> src/lib.rs:11:26
   |
11 |         self.data = &mut self.data[0..n - 1];
   |                          ^^^^^^^^^^^^^^^^^^^
   |
note: first, the lifetime cannot outlive the anonymous lifetime #1 defined on the method body at 6:5...
  --> src/lib.rs:6:5
   |
6  | /     pub fn pop_no_return(&mut self) 
7  | |         if self.data.is_empty() 
8  | |             return;
9  | |         
10 | |         let n = self.data.len();
11 | |         self.data = &mut self.data[0..n - 1];
12 | |     
   | |_____^
note: ...so that reference does not outlive borrowed content
  --> src/lib.rs:11:26
   |
11 |         self.data = &mut self.data[0..n - 1];
   |                          ^^^^^^^^^
note: but, the lifetime must be valid for the lifetime `'a` as defined on the impl at 5:6...
  --> src/lib.rs:5:6
   |
5  | impl<'a, X> StackLike<'a, X> 
   |      ^^
note: ...so that reference does not outlive borrowed content
  --> src/lib.rs:11:21
   |
11 |         self.data = &mut self.data[0..n - 1];
   |                     ^^^^^^^^^^^^^^^^^^^^^^^^

有没有办法让这个工作,或者我需要更明确地跟踪我感兴趣的切片的边界?

【问题讨论】:

【参考方案1】:

我稍微修改了 Masklinn 的代码,以允许在同一个堆栈上调用多个 .pop()s:

struct StackLike<'a, X> 
    data: &'a mut [X],


impl<'a, X> StackLike<'a, X> 
    pub fn pop(&mut self) -> Option<&'a mut X> 
        let data = std::mem::replace(&mut self.data, &mut []);
        if let Some((last, subslice)) = data.split_last_mut() 
            self.data = subslice;
            Some(last)
         else 
            None
        
    


fn main() 
    let mut data = [1, 2, 3, 4, 5];
    let mut stack = StackLike  data: &mut data ;

    let x = stack.pop().unwrap();
    let y = stack.pop().unwrap();
    println!("X: , Y: ", x, y);

这里棘手的部分是这一行(为了明确起见,我添加了类型注释):

let data: &'a mut [X] = std::mem::replace(&mut self.data, &mut []);

我们暂时将self.data 替换为一个空切片,以便我们可以拆分切片。如果你写得简单

let data: &'a mut [X] = self.data;

编译器会不高兴:

error[E0312]: lifetime of reference outlives lifetime of borrowed content...
  --> src/main.rs:7:33
   |
7  |         let data: &'a mut [X] = self.data;
   |                                 ^^^^^^^^^
   |
note: ...the reference is valid for the lifetime `'a` as defined on the impl at 5:6...
  --> src/main.rs:5:6
   |
5  | impl<'a,  X> StackLike<'a, X> 
   |      ^^
note: ...but the borrowed content is only valid for the anonymous lifetime #1 defined on the method body at 6:5
  --> src/main.rs:6:5
   |
6  | /     pub fn pop(&mut self) -> Option<&'a mut X> 
7  | |         let data: &'a mut [X] = self.data;
8  | |         if let Some((last, subslice)) = data.split_last_mut() 
9  | |             self.data = subslice;
...  |
13 | |         
14 | |     
   | |_____^

据我了解,问题在于self.data 是可变引用,而可变引用不是Copy(记住,一次只能有一个)。而且您不能离开self.data,因为self 是可变引用,而不是所有者。所以编译器试图做的是重新借用self.data,它用&amp;mut self的生命周期“感染”它。这是一条死胡同:我们希望引用 live for 'a,但它实际上只在 &amp;mut self 的生命周期内有效,而这些生命周期通常是不相关的(并且它们不需要相关),这让编译器感到困惑。

为了帮助编译器,我们使用std::mem::replace 将切片显式移出self.data,并暂时将其替换为空切片which can be any lifetime。现在我们可以用data 做任何事情,而不用纠结&amp;mut self 的生命周期。

【讨论】:

太棒了。正是我要建议的。值得注意的是,如果self.data 开始为空(因为当split_last_mut 返回None 时,您丢失了原始切片),切片的“临时”替换最终将是永久性的。实际上,这可能不是问题,因为所有空切片或多或少都是等价的,但如果您对数据的地址做一些棘手的事情,它可能会导致意外。 pop 的返回类型不需要持有可变引用,即:pub fn pop(&amp;mut self) -&gt; Option&lt;&amp;'a X&gt; 确实是我自己加的,不然data也可以是&amp;'a [X],问题就简单了。【参考方案2】:

对于子问题 2,您需要指出 &amp;mut self'a 之间的关系,否则它们被视为不相关。我不知道是否存在通过生命周期省略的捷径,但如果您指定 self'a 生存,那么您就可以了。

对于子问题 1,编译器不会“看穿”函数调用(包括对函数调用去糖的索引),因此它不知道 &amp;self.data[n - 1]&amp;mut self.data[0..n-1] 不重叠。您需要使用split_mut_last

struct StackLike<'a, X> 
    data: &'a mut [X],


impl<'a, X> StackLike<'a, X> 
    pub fn pop(&'a mut self) -> Option<&'a X> 
        if let Some((last, subslice)) = self.data.split_last_mut() 
            self.data = subslice;
            Some(last)
         else 
            None
        
    

playground

【讨论】:

这样做的一个主要缺点是因为pop 必须采用&amp;'a mut self,您似乎无法调用.pop() 两次,因为它需要一个在整个生命周期内的可变引用的堆栈。 Example

以上是关于我可以将可变切片引用重新分配给自身的子切片吗?的主要内容,如果未能解决你的问题,请参考以下文章

为啥扩展切片分配不如常规切片分配灵活?

如何对 Rust 数组的 2 个可变切片进行操作?

为啥可以将切片分配给空接口但不能将其强制转换为相同的空接口

为啥我不能将任意迭代分配给步长为 -1 的扩展切片?

Cython:将单个元素分配给多维内存视图切片

Python 切片分配内存使用情况