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

Is this error due to the compiler's special knowledge about RefCell?

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 作弊,因为借用检查器的行为不同。

我什至可以 "hide" 我自己的另一种类型的 RefCell,但编译器仍然总是做正确的事情(如果可以使用 RefCell 则出错)。编译器如何知道所有这些东西以及它是如何工作的?编译器是否将类型标记为 "interior mutability container" 或类似的东西?

RefCell<T> 包含一个 UnsafeCell<T> which is a special lang item。是 UnsafeCell 导致了错误。你可以检查:

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

...

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

但错误不是由于编译器识别出 UnsafeCell 引入了内部可变性,而是 UnsafeCellinvariant in T. In fact, we could reproduce the error using 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);

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

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

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

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


现在让我们看看编译器是如何判断E0502错误的。首先,重要的是要记住,编译器必须在这里选择两个特定的生命周期:表达式 &mut s ('a) 类型的生命周期和 bar (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. 的子类型
  5. Contravariant<T>T 是逆变的,即如果 U <: T,则 Contravariant<T> <: Contravariant<U>
  6. 因此当&'x i32&'a i32超类型时可以满足子类型关系。
  7. 因此 '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 outlives (or equals to) 'x 表示语句 s.len() 必须包含在范围内,导致 borrowck 错误。

只有在协变的情况下,我们才能使 'a 的范围小于 'x,从而允许在调用 s.len() 之前删除临时对象 &mut s(意思是: 在 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 -------+