这个错误是由于编译器对RefCell的特殊了解吗?

时间:2017-04-04 14:39:46

标签: rust borrow-checker

fn works<'a>(foo: &Option<&'a mut String>, s: &'a mut String) {}
fn error<'a>(foo: &RefCell<Option<&'a mut String>>, s: &'a mut String) {}

let mut s = "hi".to_string();

let foo = None;
works(&foo, &mut s);

// with this, it errors
// let bar = RefCell::new(None);
// error(&bar, &mut s);

s.len();

如果我在注释中输入两行,则会出现以下错误:

error[E0502]: cannot borrow `s` as immutable because it is also borrowed as mutable
  --> <anon>:16:5
   |
14 |     error(&bar, &mut s);
   |                      - mutable borrow occurs here
15 |     
16 |     s.len();
   |     ^ immutable borrow occurs here
17 | }
   | - mutable borrow ends here

works()errors()的签名看起来非常相似。但显然编译器知道你可以用RefCell欺骗它,因为借用检查器的行为不同。

我甚至可以将RefCell“隐藏”在我自己的另一种类型中,但编译器仍然总是做正确的事情(如果可以使用RefCell则出错)。编译器如何知道所有这些东西以及它是如何工作的?编译器是否将类型标记为“内部可变容器”或类似内容?

1 个答案:

答案 0 :(得分:8)

RefCell<T>包含UnsafeCell<T>,这是一个特殊的lang itemUnsafeCell导致错误。您可以查看:

fn error<'a>(foo: &UnsafeCell<Option<&'a mut String>>, s: &'a mut String) {}

...

let bar = UnsafeCell::new(None);
error(&bar, &mut s);

但错误不是由于编译器识别UnsafeCell引入内部可变性,而是T中的UnsafeCellinvariant。事实上,我们可以使用PhantomData重现错误:

struct Contravariant<T>(PhantomData<fn(T)>);

fn error<'a>(foo: Contravariant<&'a i32>, s: &'a mut String) {}

...

let bar = Contravariant(PhantomData);
error(bar, &mut s);

甚至是生命中{* 1}}

中任何逆变或不变的东西
'a

您无法隐藏RefCell的原因是因为方差是通过结构的字段得出的。一旦你在某处使用fn error<'a>(foo: Option<fn(&'a i32)>, s: &'a mut String) {} let bar = None; error(bar, &mut s); ,无论多深,编译器都会发现RefCell<T>是不变的。

现在让我们看看编译器如何确定E0502错误。首先,重要的是要记住编译器必须在这里选择两个特定的生命周期:表达式T&mut s)类型的生命周期和{{'a类型的生命周期1}}(让我们称之为bar)。两者都受到限制:前一生'x必须短于'a的范围,否则我们最终会得到一个比原始字符串更长的引用。 s必须大于'x的范围,否则我们可以通过bar访问悬空指针(如果类型具有生命周期参数,则编译器假定类型可以访问值那一辈子)。

有了这两个基本限制,编译器会执行以下步骤:

  1. bar类型为bar
  2. Contravariant<&'x i32>函数接受error的任何子类型,其中Contravariant<&'a i32>'a表达式的生命周期。
  3. 因此&mut s应该是bar
  4. 的子类型
  5. Contravariant<&'a i32>Contravariant<T>相反,即T,然后是U <: T
  6. 因此当Contravariant<T> <: Contravariant<U>&'x i32超类型时,可以满足子类型关系。
  7. 因此&'a i32 {/ 1}} <{1}}
  8. 同样,对于不变类型,派生关系为'x,对于可变,'a'a更长。

    现在,问题是'x类型中的生命周期一直持续到范围结束(根据上面提到的限制):

    'a == 'x

    在逆变和不变情况下,'x超过(或等于)'a意味着语句bar必须包含在范围内,从而导致借记错误。

    只有在协变的情况下,我们可以使 let bar = Contravariant(PhantomData); // <--- 'x starts here -----+ error(bar, // | &mut s); // <- 'a starts here ---+ | s.len(); // | | // <--- 'x ends here¹ --+---+ // | // <--- 'a ends here² --+ } // ¹ when `bar` goes out of scope // ² 'a has to outlive 'x 的范围缩短到'a,允许在调用'x之前删除临时对象s.len()(意思是:at 'a'x不再被视为借用了):

    &mut s