Rust: 借用范围不应该总是词法

创建于 2013-05-10  ·  44评论  ·  资料来源: rust-lang/rust

如果您在if测试中不变地借用,则借用将持续整个if表达式。 这意味着子句中的可变借用将导致借用检查器失败。

这也可能发生在 match 表达式中借用时,并且需要在其中一个分支中进行可变借用。

有关 if 借用框的示例,请参见此处,这会导致最近的向上@mut冻结。 然后remove_child()需要借用可变冲突。

https://github.com/mozilla/servo/blob/master/src/servo/layout/box_builder.rs#L387 -L411

来自@Wyverald 的更新示例

fn main() {
    let mut vec = vec!();

    match vec.first() {
        None => vec.push(5),
        Some(v) => unreachable!(),
    }
}
A-borrow-checker NLL-fixed-by-NLL

最有用的评论

它尚未每晚播放,但我只想说现在可以编译:

#![feature(nll)]

fn main() {
    let mut vec = vec!();

    match vec.first() {
        None => vec.push(5),
        Some(v) => unreachable!(),
    }
}

所有44条评论

提名生产就绪

我会称之为定义明确或向后兼容。

经过一些讨论,很明显这里的真正问题是借用检查器不努力跟踪别名,而是始终依赖区域系统来确定借用何时超出范围。 我不愿意改变这一点,至少在短期内不会,因为我想首先解决许多其他未决问题,任何改变都将是对借用检查器的重大修改。 有关另一个相关示例和详细说明,请参阅问题 #6613。

我想知道我们是否可以改进错误消息以使其更清楚发生了什么? 词法作用域相对容易理解,但在我偶然发现的这个问题的例子中,发生了什么并不明显。

只是一个错误,删除里程碑/提名。

接受未来里程碑

分类碰撞

我对解决此问题的最佳方法有一些想法。 我的基本计划是我们会有一个值何时“逃逸”的概念。 将这个概念正式化需要一些工作。 基本上,当一个借用的指针被创建时,我们将跟踪它是否已经转义。 当指针死了,如果还没有逃脱,这可以考虑杀掉贷款。 这个基本思想涵盖了“让 p = &...; use-pa-bit-but-never-again; expect-loan-to-be-expired-here;” 之类的情况。 分析的一部分将是一条规则,该规则指示何时可以认为包含借用指针的返回值尚未转义。 这将涵盖诸如“匹配 table.find(...) { ... None => { expect-table-not-to-be-loaned-here; } }”之类的情况

当然,所有这些中最有趣的部分是转义规则。 我认为规则必须考虑函数的正式定义,特别是要利用生命周期给我们的知识绑定。 例如,大多数转义分析会考虑使用指针p来转义,如果他们看到类似foo(p)的调用。 但我们不一定必须这样做。 如果函数被声明为:

fn foo<'a>(x: &'a T) { ... }

那么事实上我们知道foo不会比生命周期a持有p更长的时间。 但是,像bar这样的函数必须被视为转义:

fn bar<'a>(x: &'a T, y: &mut &'a T)

因此,推测转义规则必须考虑绑定生存期是否出现在可变位置。 这实际上是一种基于类型的别名分析。 我认为类似的推理适用于函数返回值。 因此find应该被认为是返回一个非转义的结果:

fn find<'a>(&'a self, k: &K) -> Option<&'a V>

这里的原因是因为'a绑定在find ,所以它不能出现在SelfK类型参数中,因此我们知道它可以' t 存储在那些中,并且它不会出现在任何可变位置。 (请注意,我们可以应用与今天使用的相同的推理算法,该算法将用作 #3598 修复程序的一部分,以告诉我们生命周期是否出现在可变位置)

考虑这一点的另一种方式不是贷款提前_到期,而是贷款的范围开始(通常)与借用的 _variable_ 相关联,而不是完整的生命周期,并且只有当变量出现时才会被提升到完整的生命周期_逃脱_。

再借是一种轻微的复杂性,但可以通过多种方式处理。 重新借用是当您借用借用指针的内容时——它们_一直_发生,因为编译器会自动将它们插入到几乎每个方法调用中。 考虑一个借用的指针let p = &v和像let q = &*p这样的再借用指针。 如果q死了,你可以再次使用p就好了——如果pq都死了,你可以使用v再次(假设pq转义)。 这里的复杂之处在于,如果q转义,则p必须被视为已转义,直到q的生命周期到期。 但我认为这与我们今天处理它的方式_有点_自然地不同:也就是说,编译器注意到q已经借用了p用于(最初)生命周期“q”(即,变量本身),如果q应该转义,那将被提升到完整的词法生命周期。 我想棘手的部分是在数据流中,知道在哪里插入杀戮——我们不能在p被重新借用时立即为p插入杀戮。 哦,好吧,我不会在这上面浪费更多时间,这似乎可行,而且在最坏的情况下,有更简单的解决方案足以应对常见情况(例如,考虑p已逃脱整个生命周期q ,无论q贷款是否逃逸)。

无论如何,需要更多的思考,但我开始看到这是如何工作的。 在#2202 和#8624 修复之前,我仍然不愿意开始任何这样的扩展,这是borrowck 的两个已知问题。 在我们开始扩展系统之前,我还希望在健全性证明方面取得更多进展。 时间线上的另一个扩展名是#6268。

我相信我遇到了这个错误。 我的用例和解决方法尝试:

https://gist.github.com/toffaletti/6770126

这是此错误的另一个示例(我认为):

use std::util;

enum List<T> {
    Cons(T, ~List<T>),
    Nil
}

fn find_mut<'a,T>(prev: &'a mut ~List<T>, pred: |&T| -> bool) -> Option<&'a mut ~List<T>> {
    match prev {
        &~Cons(ref x, _) if pred(x) => {}, // NB: can't return Some(prev) here
        &~Cons(_, ref mut rs) => return find_mut(rs, pred),
        &~Nil => return None
    };
    return Some(prev)
}

我想写:

fn find_mut<'a,T>(prev: &'a mut ~List<T>, pred: |&T| -> bool) -> Option<&'a mut ~List<T>> {
    match prev {
        &~Cons(ref x, _) if pred(x) => return Some(prev),
        &~Cons(_, ref mut rs) => return find_mut(rs, pred),
        &~Nil => return None
    }
}

推断一旦我们完成对谓词的评估, x借用就会失效,但当然,借用现在扩展到整个匹配。

我有更多关于如何编码的想法。 我的基本计划是每笔贷款都有两个位:一个转义版本和一个非转义版本。 最初我们添加非转义版本。 当引用转义时,我们添加转义位。 当变量(或临时变量等)失效时,我们会杀死未转义的位——但保留转义位(如果设置)不变。 我相信这涵盖了所有主要的例子。

抄送@flaper87

这个问题包括这个吗?

use std::io::{MemReader, EndOfFile, IoResult};

fn read_block<'a>(r: &mut Reader, buf: &'a mut [u8]) -> IoResult<&'a [u8]> {
    match r.read(buf) {
        Ok(len) => Ok(buf.slice_to(len)),
        Err(err) => {
            if err.kind == EndOfFile {
                Ok(buf.slice_to(0))
            } else {
                Err(err)
            }
        }
    }
}

fn main() {
    let mut buf = [0u8, ..2];
    let mut reader = MemReader::new(~[67u8, ..10]);
    let mut block = read_block(&mut reader, buf);
    loop {
        //process block
        block = read_block(&mut reader, buf); //error here
}

抄送我

#9113 中的好例子

抄送我

我可能弄错了,但以下代码似乎也遇到了这个错误:

struct MyThing<'r> {
  int_ref: &'r int,
  val: int
}

impl<'r> MyThing<'r> {
  fn new(int_ref: &'r int, val: int) -> MyThing<'r> {
    MyThing {
      int_ref: int_ref,
      val: val
    }
  }

  fn set_val(&'r mut self, val: int) {
    self.val = val;
  }
}


fn main() {
  let to_ref = 10;
  let mut thing = MyThing::new(&to_ref, 30);
  thing.set_val(50);

  println!("{}", thing.val);
}

理想情况下,由调用 set_val 引起的可变借用将在函数返回后立即结束。 请注意,从结构(和相关代码)中删除“int_ref”字段会导致问题消失。 行为不一致。

@SergioBenitez我认为这不是同一个问题。 您明确要求&mut self引用的生命周期与结构的生命周期相同。

但是你不需要这样做。 你根本不需要set_val()的一生。

fn set_val(&mut self, val: int) {
    self.val = val;
}

我发现了另一个很难修复的案例:

/// A buffer which breaks chunks only after the specified boundary
/// sequence, or at the end of a file, but nowhere else.
pub struct ChunkBuffer<'a, T: Buffer+'a> {
    input:  &'a mut T,
    boundary: Vec<u8>,
    buffer: Vec<u8>
}

impl<'a, T: Buffer+'a> ChunkBuffer<'a,T> {
    // Called internally to make `buffer` valid.  This is where all our
    // evil magic lives.
    fn top_up<'b>(&'b mut self) -> IoResult<&'b [u8]> {
        // ...
    }
}

impl<'a,T: Buffer+'a> Buffer for ChunkBuffer<'a,T> {
    fn fill_buf<'a>(&'a mut self) -> IoResult<&'a [u8]> {
        if self.buffer.as_slice().contains_slice(self.boundary.as_slice()) {
            // Exit 1: Valid data in our local buffer.
            Ok(self.buffer.as_slice())
        } else if self.buffer.len() > 0 {
            // Exit 2: Add some more data to our local buffer so that it's
            // valid (see invariants for top_up).
            self.top_up()
        } else {
            {
                // Exit 3: Exit on error.
                let read = try!(self.input.fill_buf());
                if read.contains_slice(self.boundary.as_slice()) {
                    // Exit 4: Valid input from self.input. Yay!
                    return Ok(read)
                }
            }
            // Exit 5: Accumulate sufficient data in our local buffer (see
            // invariants for top_up).
            self.top_up()
        }
    }

…这使:

/path/to/mylib/src/buffer.rs:168:13: 168:17 error: cannot borrow `*self` as mutable more than once at a time
/path/to/mylib/src/buffer.rs:168             self.top_up()
                                                        ^~~~
/path/to/mylib/src/buffer.rs:160:33: 160:43 note: previous borrow of `*self.input` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `*self.input` until the borrow ends
/path/to/mylib/src/buffer.rs:160                 let read = try!(self.input.fill_buf());
                                                                            ^~~~~~~~~~
<std macros>:1:1: 3:2 note: in expansion of try!
/path/to/mylib/src/buffer.rs:160:28: 160:56 note: expansion site
/path/to/mylib/src/buffer.rs:170:6: 170:6 note: previous borrow ends here
/path/to/mylib/src/buffer.rs:149     fn fill_buf<'a>(&'a mut self) -> IoResult<&'a [u8]> {
...
/path/to/mylib/src/buffer.rs:170     }

这基本上相当于#12147。 变量read隐藏在内部作用域中,但returnread的生命周期绑定到整个函数的生命周期。 大多数明显的解决方法都失败了:

  1. 我不能两次调用input.fill_buf ,因为Buffer接口不能保证它返回我刚刚第二次验证的数据。 如果我 _do_ 尝试这个,代码在技术上是不正确的,但类型检查器很高兴地通过了它。
  2. 我对top_up无能为力,因为这是一段邪恶的代码,需要以复杂的方式改变一切。
  3. 我无法将有问题的 bind+test+return 移到另一个函数中,因为新 API 仍然存在所有相同的问题(除非if let允许我测试 _then_ bind?)。

几乎感觉好像'a约束在理想情况下不应该一直传播回read 。 但我在这里不知所措。 我接下来要试试if let

好吧, if let昨晚没有进入构建,但由于它应该只是 AST 重写,我想它可能以与match相同的方式失败(我已经也试过here)。

我不知道如何继续,没有使用unsafe

我目前的 hack 看起来是这样的:

impl<'a,T: Buffer+'a> Buffer for ChunkBuffer<'a,T> {
    fn fill_buf<'a>(&'a mut self) -> IoResult<&'a [u8]> {
        // ...

            { // Block A.
                let read_or_err = self.input.fill_buf();
                match read_or_err {
                    Err(err) => { return Err(err); }
                    Ok(read) => {
                        if read.contains_slice(self.boundary.as_slice()) {
                               return Ok(unsafe { transmute(read) });
                        }
                    }
                }
            }
            self.top_up()

这里的理论是,我放弃了read (绑定到self.input )的生命周期,并立即应用基于self新生命周期,它拥有self.input 。 理想情况下,我希望read的词法生命周期等于 Block A,并且我不希望它仅仅因为我将它传递给return就被提升到 _lexical_ 块级别。 显然生命周期检查器仍然需要证明结果具有与'a兼容的生命周期,但我不明白为什么这意味着 LIFETIME( read ) 需要与 LIFETIME( 'a )。

我完全有可能感到非常困惑,或者我的代码非常不安全。 :-) 但感觉这应该可行,因为我可以毫无问题地调用return self.input.fill_buf() 。 有没有办法将这种直觉形式化?

@emk所以这是 SEME 区域(即非词法区域)无法修复的“硬代码”,至少不能由它们自己修复。 我有一些关于如何在编译器中很好地修复它的想法,但它是对 SEME 区域的重要扩展。 通常有一种方法可以通过重构代码来解决此问题。 让我看看我是否可以玩弄它并产生一个很好的例子。

我想知道这是否会在 1.0.0 中重新考虑。 最近这件事一直在出现,我担心一旦 1.0 引起关注,这会从剪纸变成肉伤。 作为 Rust 最明显和最常被谈论的特性,借用被打磨和可用是非常重要的。

对此 RFC 是否有时间表?

@nikomatsakis我有一个现实世界的简单例子,它在 Entry API 上工作失败了,如果有帮助的话:

use std::collections::SmallIntMap;

enum Foo<'a>{ A(&'a mut SmallIntMap<uint>), B(&'a mut uint) }

fn main() {
    let mut map = SmallIntMap::<uint>::new();
    do_stuff(&mut map);
}

fn do_stuff(map: &mut SmallIntMap<uint>) -> Foo {
    match map.find_mut(&1) {
        None => {},  // Definitely can't return A here because of lexical scopes
        Some(val) => return B(val),
    }
    return A(map); // ERROR: borrowed at find_mut???
}

围栏

@bstrie @pcwalton@zwarich 都花了一些时间尝试实际实施这项工作(可能会同时推出 RFC)。 他们遇到了一些意想不到的复杂性,这意味着需要做的工作比预期的要多得多。 我认为每个人都同意你的观点,这些限制很重要,可能会影响语言的第一印象,但很难在已经安排的向后不兼容的更改之间取得平衡。

我觉得如果 1.0 没有解决这个问题,那么这种事情会导致人们完全归咎于借用检查方法,而这个问题并不是借用检查 AFAIK 固有的无法解决的问题。

@blaenk很难不责怪借用检查器,我每天都遇到过类似的情况(如@Gankro)。 当通常的解决方案是旋转(例如变通)/或注释来重构您的代码以使其更加“不可变”,功能性等时,这令人沮丧。

@mtanski是的,但错误

对于这种情况:“让 p = &...; use-pa-bit-but-never-again; expect-loan-to-be-expired-here;” 我现在认为可以接受 kill(p) 指令来手动声明该借用的范围结束。 如果不需要此指令,以后的版本可以简单地忽略它,或者如果在它之后检测到 p 的重用,则将其标记为错误。

/* (wanted) */
/*
fn main() {

    let mut x = 10;

    let y = &mut x;

    println!("x={}, y={}", x, *y);

    *y = 11;

    println!("x={}, y={}", x, *y);
}
*/

/* had to */
fn main() {

    let mut x = 10;
    {
        let y = &x;

        println!("x={}, y={}", x, *y);
    }

    {
        let y = &mut x;

        *y = 11;
    }

    let y = &x;

    println!("x={}, y={}", x, *y);
}

前奏中有 drop() 方法可以做到这一点。 但是好像没有
帮助处理可变借款。

在Sun,2015年4月5日,下午1时41分axeoth [email protected]写道:

/* (想要) _//_fn main() { let mut x = 10; 让 y = &mut x; println!("x={}, y={}", x, _y); *y = 11; println!("x={}, y={}", x, *y);}_/
/* 必须 */fn main() {

let mut x = 10;
{
    let y = &x;

    println!("x={}, y={}", x, *y);
}

{
    let y = &mut x;

    *y = 11;
}

let y = &x;

println!("x={}, y={}", x, *y);

}


直接回复此邮件或在 GitHub 上查看
https://github.com/rust-lang/rust/issues/6393#issuecomment -89848449。

@metajack你的原始代码的链接是 404。你能把它内联给阅读这个 bug 的人吗?

或者更确切地说,这是我提交此错误时使用的解决方法。 更改之前的原始代码似乎是这样的:
https://github.com/servo/servo/blob/7267f806a7817e48b0ac0c9c4aa23a8a0d288b03/src/servo/layout/box_builder.rs#L387 -L399

我不确定这些特定示例现在的相关性如何,因为它们是 Rust 1.0 之前的版本。

@metajack在这个问题的顶部有一个超简单(1.0 后)的例子会很棒。 这个问题现在是https://github.com/rust-lang/rfcs/issues/811 的一部分

fn main() {
    let mut nums=vec![10i,11,12,13];
    *nums.get_mut(nums.len()-2)=2;
}

我想我抱怨的是这样的:
https://is.gd/yfxUfw

这个特殊情况现在似乎有效。

@vitiral我认为今天 Rust 中的一个例子适用:

fn main() {
    let mut vec = vec!();

    match vec.first() {
        None => vec.push(5),
        Some(v) => unreachable!(),
    }
}

None分支失败了borrowck。

奇怪的是,如果您不尝试在Some分支中捕获int (即使用Some(_) ),它会编译。

@wyverland哦,是的,我昨天刚碰到那个,很烦人。

@metajack您可以编辑第一篇文章以包含该示例吗?

它尚未每晚播放,但我只想说现在可以编译:

#![feature(nll)]

fn main() {
    let mut vec = vec!();

    match vec.first() {
        None => vec.push(5),
        Some(v) => unreachable!(),
    }
}
此页面是否有帮助?
0 / 5 - 0 等级