Почему Rust не понимает, что ссылка больше не заимствована?

В Rust, когда я заимствую значение, компилятор это замечает, но когда я его заменяю, компилятор этого не замечает и выдает ошибку E0597.

Дана изменяемая переменная, содержащая ссылку x. Когда я заменяю его содержимое ссылкой на локальную переменную, и до того, как локальная переменная выйдет из области видимости, я заменяю ее на исходную.

Вот код, который показывает это:

struct X {payload : i32}

fn main() {
    let pl = X{payload : 44};
    {
        let mut x = &pl;
        {
            let inner = X{payload : 30};
            let tmp = std::mem::replace(&mut x, &inner);
            println! ("data  = {:?}", x.payload);
            let _f = std::mem::replace(&mut x, &tmp);
        }
        println! ("data  = {:?}", x.payload);
    }
}

Ошибка:

error[E0597]: `inner` does not live long enough
  --> src/main.rs:9:49
   |
9  |             let tmp = std::mem::replace(&mut x, &inner);
   |                                                 ^^^^^^ borrowed value does not live long enough
...
12 |         }
   |         - `inner` dropped here while still borrowed
13 |         println! ("data  = {:?}", x.payload);
   |                                 --------- borrow later used here

For more information about this error, try `rustc --explain E0597`.

Компилятор замечает, когда я присваиваю ссылку inner на x, но упускает из виду тот факт, что, пока inner еще жив, я снова заменяю эту ссылку исходной ссылкой на pl.

Ожидаемый результат должен быть:

data =30
data =44

Что я делаю неправильно?

Посмотрите на эту детскую площадку для более глубокого анализа, но не смог понять.

cafce25 04.12.2022 20:35
Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
4
1
96
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

При анализе кода Rust компилятор консервативен и отбрасывает программы, для которых он не может определить, есть ошибки памяти или нет.

В частности, когда компилятор проверяет let tmp = ... в вашем коде, он замечает, что вы сохраняете ссылку, указывающую на недолговечный экземпляр (inner), в ссылку (pl), которая используется после того, как inner выходит за рамки. На этом компилятор останавливается и сообщает об ошибке, сообщая, что вы, возможно, создали висячую ссылку. Не учитывается, что сразу после восстановления исходного pl. Абстракция времени жизни, которая в настоящее время используется для анализа, недостаточно точна.

В Rust вы должны структурировать свой код так, чтобы компилятору было легко его проверить. Это означает, например, что вы должны объявить inner во внешней области или что вместо использования ссылок вы используете такие типы, как Rc, которые заменяют проверки во время компиляции проверками во время выполнения, выполняемыми в библиотеке.

Если вы действительно хотите использовать небезопасный код, вы можете сделать следующее, но это крайне не рекомендуется. Вы должны вручную проверить, что код не имеет неопределенного поведения, что трудно сделать, поскольку в настоящее время нет полного описания того, что является неопределенным поведением. Из The Rust Reference:

Предупреждение. Следующий список не является исчерпывающим. Не существует формальной модели семантики Rust для того, что разрешено и что запрещено в небезопасном коде, поэтому небезопасным может быть больше действий. Следующий список — это то, что мы точно знаем, это неопределенное поведение. Пожалуйста, прочтите Рустономикон, прежде чем писать небезопасный код.

struct X {payload : i32}

fn main() {
    let pl = X{payload : 44};
    {
        let mut x = &pl;
        // SAFETY: ...(motivate why the following is ok)
        unsafe {
            let inner = X{payload : 30};
            x = std::mem::transmute::<_, &'static X>(&inner);
            println! ("data  = {:?}", x.payload);
            x = &pl;
        }
        println! ("data  = {:?}", x.payload);
    }
}

Rc использует кучу, поэтому это не одно и то же. Если компилятор слишком консервативен и неоправданно, то можете ли вы предоставить небезопасный способ достижения того же результата, используя только выделение стека? Нарушение времени жизни следует проверять при закрытии блока, когда "внутренний" умирает, а не при его присвоении.

E. Timotei 05.12.2022 14:39

Ваша идея с трансмутацией очень хороша, но ваш конкретный пример случайно сработал бы и без нее, посмотрите мой полный ответ.

E. Timotei 05.12.2022 21:33

Я решил это. К сожалению, это ошибка или ограничение компилятора.

Семантически эквивалентный код, кредиты за код достаются тому, кто опубликовал часть ответа, но затем удалил его.

// This one will yields error E0597
struct X {payload : i32}

fn main() {
    let pl = X{payload : 44};
    {
        let mut x = &pl;
        println! ("data  = {:?}", x.payload);
        {
            let inner = X{payload : 30};
            let tmp : &X = x;
            x = &inner;
            println! ("data  = {:?}", x.payload);
            x = tmp;
        }
        println! ("data  = {:?}", x.payload);
    }
}

Этот код выдаст ту же ошибку.

Однако небольшая настройка заставит его скомпилироваться без ошибок или предупреждений.

// Compiles without errors/warnings.
struct X {payload : i32}

fn main() {
    let pl = X{payload : 44};
    {
        let mut x = &pl;
        println! ("data  = {:?}", x.payload);
        {
            let inner = X{payload : 30};
            x = &inner;
            println! ("data  = {:?}", x.payload);
            x = &pl;
        }
        println! ("data  = {:?}", x.payload);
    }
}

Это заставляет меня поверить, что есть ошибка компилятора. Потому что теперь компилятор улавливает, что время жизни inner отделяется от времени жизни x.

УВЫ. Когда вы помещаете внутренний блок в отдельную функцию, проблема возвращается. Так что это был просто случай, когда у компилятора Rust был некоторый путь оптимизации кода, который улавливал крайний случай.

// Yields error E0597 again.
struct X {payload : i32}

fn inner_func(x : &mut &X) {
    let inner = X{payload : 30};
    let tmp : &X = *x;
    *x = &inner;
    println! ("data  = {:?}", (*x).payload);
    *x = &tmp;
}

fn main() {
    let pl = X{payload : 44};
    {
        let mut x : &X = &pl;
        inner_func(&mut &mut x);
        println! ("data  = {:?}", x.payload);
    }
}

Кредит принадлежит Фредерико за способ втиснуть бесконечное время жизни в время жизни inner, чтобы сделать его больше, даже если это означает использование небезопасного.

// This one compiles without errors/warnings.
struct X {payload : i32}

fn inner_func(x : &mut &X) {
    let inner = X{payload : 30};
    let tmp : &X = *x;
    unsafe {
        *x = std::mem::transmute::<_, &'static X>(&inner);
    }
    println! ("data  = {:?}", (*x).payload);
    *x = &tmp;
}

fn main() {
    let pl = X{payload : 44};
    {
        let mut x : &X = &pl;
        inner_func(&mut &mut x);
        println! ("data  = {:?}", x.payload);
    }
}
Ответ принят как подходящий

Кажется, вы упускаете из виду, что время жизни ссылок является частью их типа, а не какими-то динамически отслеживаемыми метаданными. Таким образом, в исходном коде время жизни, связанное с x, должно останавливаться в конце вложенного блока, потому что время жизни, связанное с ним, должно было «слиться» со временем жизни inner, которое там заканчивается. Неважно, что вы вернете его обратно, так как это не изменит тип.

Теперь о нюансах, обнаруженных в самоответе:

Однако небольшая настройка заставит его скомпилироваться без ошибок или предупреждений.

// Compiles without errors/warnings.
struct X {payload : i32}

fn main() {
    let pl = X{payload : 44};
    {
        let mut x = &pl;
        println! ("data  = {:?}", x.payload);
        {
            let inner = X{payload : 30};
            x = &inner;
            println! ("data  = {:?}", x.payload);
            x = &pl;
        }
        println! ("data  = {:?}", x.payload);
    }
}

Это заставляет меня поверить, что есть ошибка компилятора. Потому что теперь компилятор улавливает, что время жизни внутреннего элемента отделяется от времени жизни x.

УВЫ. Когда вы помещаете внутренний блок в отдельную функцию, проблема возвращается. Так что это был просто случай, когда у компилятора Rust был некоторый путь оптимизации кода, который улавливал крайний случай.

Критическое знание, позволяющее рассуждать о том, почему некоторые примеры работают, а некоторые нет, во многом связано с принципом, согласно которому средство проверки заимствования не делает выводов за пределами тела текущей функции.

Это видно в обоих направлениях:

  • если вы используете std::mem::replace:

    Средство проверки займа не знает, что делает std::mem::replace. Он только видит, что это функция, которая принимает &mut T, T и возвращает T. Таким образом, вы можете видеть, как он отклонит этот код, потому что не может понять, что x имеет исходное значение. Средство проверки заимствования не будет смотреть на то, как реализовано replace, чтобы рассуждать о локальном коде.

  • если вы выделите его в отдельную функцию:

    fn inner_func(x: &mut &X) {
        let inner = X { payload: 30 };
        let tmp: &X = *x;
        *x = &inner;
        println!("data  = {:?}", x.payload);
        *x = &tmp;
    }
    

    Затем средство проверки заимствования видит, что это явно неправильно: время жизни x по построению живет вне области действия inner_func, поэтому любые локальные переменные будут иметь меньшее время жизни и будут несовместимы. В его нынешнем виде это определенно неправильно, так как println! может запаниковать, и вызывающая сторона может это уловить и получить висячую ссылку. Опять же, средство проверки заимствования не смотрит на то, что делает main, чтобы рассуждать о локальном коде.

    В остальном это не работает так же, как и в другом примере с использованием переменной tmp. Средство проверки заимствования не считает, что это восстановит исходное время жизни. Назовите это упущенной возможностью, если хотите, но у компилятора нет причин обрабатывать эту последовательность изменений особым образом. Вы можете просто повторно заимствовать, если хотите сократить время жизни для использования во вложенной области.

Таким образом, это не ошибка компилятора, это просто то, как реализована проверка заимствования, и она работает по замыслу. Фактически, рабочим примером является гадкий утенок, и средство проверки заимствований позволяет это только потому, что оно знает всю жизнь и взаимодействия x и, таким образом, может вручную отмахнуться от того, что я написал в начале своего ответа.

Что я делаю неправильно?

Если у вас есть такой код в реальном сценарии использования, нет причин повторно использовать x, просто создайте новую ссылку во вложенной области видимости:

struct X { payload: i32 }

fn main() {
    let pl = X { payload: 44 };
    let x = &pl;
    {
        let mut x = x; // reborrow so that the new x can have a smaller scope
        let inner = X { payload: 30 };
        let tmp = std::mem::replace(&mut x, &inner);
        println!("data  = {:?}", x.payload);
        let _f = std::mem::replace(&mut x, &tmp);
    }
    println!("data  = {:?}", x.payload); // uses original x
}

Другие вопросы по теме