Это ошибка из-за специальных знаний компилятора о 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, потому что проверка чеков ведет себя по-другому.

Я даже могу "спрятать" RefCell в другом типе своего собственного, но компилятор все равно всегда делает правильные вещи (ошибки в случае использования RefCell). Как компилятор знает все это и как он работает? Указывает ли тип компилятора как "контейнер внутренней изменчивости" или что-то в этом роде?

Ответ 1

RefCell<T> содержит UnsafeCell<T>, который является специальным 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 вводит внутреннюю изменчивость, но 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);

или даже что-либо, что контравариантно или инвариантно в течение жизни '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 (позвоните ему 'x). Оба ограничены: предыдущее время жизни 'a должно быть короче, чем область s, в противном случае мы получим ссылку, живущую дольше, чем исходная строка. 'x должен быть больше, чем область bar, иначе мы могли бы получить доступ к оборванному указателю через bar (если у типа есть параметр lifetime, компилятор предполагает, что тип может получить доступ к значению с этим временем жизни).

С этими двумя основными ограничениями компилятор выполняет следующие шаги:

  • Тип bar равен Contravariant<&'x i32>.
  • Функция error принимает любой подтип Contravariant<&'a i32>, где 'a - время жизни этого выражения &mut s.
  • Таким образом, bar должен быть подтипом Contravariant<&'a i32>
  • Contravariant<T> контравариантно над T, т.е. если U <: T, то Contravariant<T> <: Contravariant<U>.
  • Таким образом, отношение подтипирования может быть выполнено, если &'x i32 является супертипом &'a i32.
  • Таким образом, '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

В обоих контравариантных и инвариантных случаях 'a переживает (или равно) 'x означает, что оператор s.len() должен быть включен в диапазон, вызывая ошибку заемщика.

Только в ковариантном случае мы могли бы сделать диапазон 'a короче, чем 'x, позволяя исключить временный объект &mut s до того, как будет вызван s.len() (что означает: at s.len(), s is не считаются заимствованными больше):

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