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

Luk*_*odt 10 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();
Run Code Online (Sandbox Code Playgroud)

如果我在注释中添加两行,则会发生以下错误:

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
Run Code Online (Sandbox Code Playgroud)

签名works()errors()外观相似.但显然编译器知道你可以用a欺骗它RefCell,因为借用检查器的行为不同.

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

ken*_*ytm 8

RefCell<T>包含UnsafeCell<T>一个特殊的郎项.这是UnsafeCell导致错误的原因.您可以查看:

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

...

let bar = UnsafeCell::new(None);
error(&bar, &mut s);
Run Code Online (Sandbox Code Playgroud)

但错误是不是由于编译器识别的UnsafeCell引入内部可变性,但一个UnsafeCell不变的T.实际上,我们可以重现使用错误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);
Run Code Online (Sandbox Code Playgroud)

甚至是生命中任何逆变或不变的东西'a:

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

let bar = None;
error(bar, &mut s);
Run Code Online (Sandbox Code Playgroud)

您无法隐藏RefCell的原因是因为方差是通过结构的字段派生的.一旦你使用了RefCell<T>某个地方,无论多深,编译器都会发现它T是不变的.


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

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

  1. 类型barContravariant<&'x i32>.
  2. 所述error函数接受的任何亚型Contravariant<&'a i32>,其中'a是的寿命&mut s表达式.
  3. 因此bar应该是一个子类型Contravariant<&'a i32>
  4. Contravariant<T>逆变T,即如果U <: T,那么Contravariant<T> <: Contravariant<U>.
  5. 因此,当子类型关系可以得到满足&'x i32&'a i32.
  6. 因此'x应该'a,即'a应该 寿命更长'x.

类似地,对于不变量类型,所导出的关系'a == 'x,以及用于convariant,'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
Run Code Online (Sandbox Code Playgroud)

在逆变和不变的情况下,'aoutlives(或等于)'x意味着语句s.len()必须包含在范围内,从而导致借入错误.

只有在协变的情况下,我们可以使范围'a更短'x,允许临时对象&mut ss.len()被调用之前被丢弃(意思是:at s.len(),s不再被认为是借用的):

    let bar = Covariant(PhantomData);       // <--- 'x starts here -----+
                                            //                          |
    error(bar,                              //                          |
          &mut s);                          // <- 'a starts here --+    |
                                            //                     |    |
                                            // <- 'a ends here ----+    |
    s.len();                                //                          |
}                                           // <--- 'x ends here -------+
Run Code Online (Sandbox Code Playgroud)