Rust: объем заимствования не всегда должен быть лексическим

Созданный на 10 мая 2013  ·  44Комментарии  ·  Источник: rust-lang/rust

Если в тесте if вы заимствуете неизменяемо, заимствование сохраняется на все выражение if . Это означает, что изменяемые заимствования в предложениях приведут к сбою средства проверки заимствований.

Это также может произойти при заимствовании в выражении match и необходимости изменяемого заимствования в одном из ответвлений.

См. Здесь пример, где if заимствует поля, что приводит к зависанию ближайшего восходящего @mut . Затем remove_child() который необходимо заимствовать взаимно конфликтует.

https://github.com/mozilla/servo/blob/master/src/servo/layout/box_builder.rs#L387 -L411

Обновленный пример от @Wyverald

fn main() {
    let mut vec = vec!();

    match vec.first() {
        None => vec.push(5),
        Some(v) => unreachable!(),
    }
}
A-borrow-checker NLL-fixed-by-NLL

Самый полезный комментарий

Это еще не популярно в ночное время, но я просто хочу сказать, что теперь это компилируется:

#![feature(nll)]

fn main() {
    let mut vec = vec!();

    match vec.first() {
        None => vec.push(5),
        Some(v) => unreachable!(),
    }
}

Все 44 Комментарий

номинирование на производство готово

Я бы назвал это либо четко определенным, либо обратным соответствием.

Код был реорганизован, но вот конкретное умиротворение средства проверки заимствований, которое мне пришлось сделать:

https://github.com/metajack/servo/commit/5324cabbf8757fa68b1aa36548b992041be94ef9

https://github.com/metajack/servo/commit/7234635aa580c8a821003882e77d8e043d247687

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

Интересно, можем ли мы улучшить сообщения об ошибках, чтобы было понятнее, что происходит? Лексические области относительно легко понять, но в примерах этой проблемы, которые я наткнулся, отнюдь не было очевидно, что происходит.

просто баг, удаление вехи / номинации.

принято для вехи в далеком будущем

сортировочная шишка

У меня были мысли о том, как это исправить. Мой основной план состоит в том, чтобы иметь представление о том, когда значение «ускользает». Чтобы формализовать это понятие, потребуется определенная работа. По сути, когда создается заимствованный указатель, мы затем отслеживаем, ускользнул ли он. Когда указатель мертв , если он не ускользнул, это можно рассматривать как уничтожение ссуды. Эта основная идея охватывает такие случаи, как «let p = & ...; use-pa-bit-but-never-again; ожидаем-ссуду-срок-срок-здесь-истек»; Частью анализа будет правило, указывающее, когда можно считать, что возвращаемое значение, содержащее заимствованный указатель, еще не экранировалось. Это будет охватывать такие случаи, как "match table.find (...) {... None => {expect-table-not-to-be-lened-here;}}"

Самая интересная часть всего этого, конечно же, правила побега. Я думаю, что правила должны будут учитывать формальное определение функции и, в частности, использовать преимущества знаний, связанных с временами жизни, которые нам дают. Например, большинство анализов escape будут рассматривать указатель p для выхода, если они видят вызов типа foo(p) . Но нам не обязательно это делать. Если бы функция была объявлена ​​как:

fn foo<'a>(x: &'a T) { ... }

тогда фактически мы знаем, что foo не удерживает p дольше, чем время жизни a . Однако такая функция, как bar , должна рассматриваться как экранирующая:

fn bar<'a>(x: &'a T, y: &mut &'a T)

Таким образом, по-видимому, правила экранирования должны будут учитывать, появилось ли связанное время жизни в изменяемом месте или нет. По сути, это форма анализа псевдонимов на основе типов. Я думаю, что аналогичные рассуждения применимы к значениям, возвращаемым функцией. Следовательно, find следует рассматривать как возвращающий результат без экранирования:

fn find<'a>(&'a self, k: &K) -> Option<&'a V>

Причина здесь в том, что поскольку 'a привязан к find , он не может появляться в параметрах типа Self или K , и, следовательно, мы знаем, что это может ' t храниться в них, и он не появляется ни в каких изменяемых местах. (Обратите внимание, что мы можем применить тот же алгоритм вывода, который используется сегодня и который будет использоваться как часть исправления для # 3598, чтобы сообщить нам, появляется ли время жизни в изменяемом месте)

Другой способ думать об этом заключается не в том, что срок ссуды истек _ рано_, а в том, что объем ссуды начинается (обычно) как привязанный к заимствованной _ переменной_, а не к полному сроку жизни, и продвигается до полного срока службы только тогда, когда переменная _escapes_.

Повторный забор - небольшое осложнение, но с ним можно справиться по-разному. Повторный заимствование - это когда вы заимствуете содержимое заимствованного указателя - они происходят _все время_, потому что компилятор автоматически вставляет их почти в каждый вызов метода. Рассмотрим заимствованный указатель let p = &v и заимствование типа let q = &*p . Было бы хорошо, если бы, когда q был мертв, вы могли бы снова использовать p - и если бы оба p и q были мертвыми, вы могли бы использовать v снова (при условии, что ни p ни q сбегают). Сложность здесь в том, что если q ускользает, p следует считать экранированным до истечения срока жизни q . Но я думаю, что это в некоторой степени естественным образом вытекает из того, как мы с этим справляемся сегодня: то есть компилятор отмечает, что q заимствовал p на (изначально) время жизни "q" (то есть, самой переменной), и если q должно исчезнуть, это будет продвинуто до полного лексического времени жизни. Я предполагаю, что сложная часть находится в потоке данных, зная, куда вставить убийства - мы не можем сразу вставить убийство для p когда p перестанет быть заимствованным. Хорошо, я не буду тратить на это больше времени, это кажется выполнимым, и в худшем случае есть более простые решения, которые были бы адекватны для обычных ситуаций (например, считайте, что p сбежал на все время жизни. q , независимо от того, ускользнет ли ссуда q ).

В любом случае, есть основания подумать, но я начинаю понимать, как это может сработать. Я по-прежнему не хочу начинать подобные расширения, пока не будут исправлены # 2202 и # 8624, это две известные проблемы с заимствованием. Я также хотел бы добиться большего прогресса в доказательстве надежности, прежде чем мы приступим к расширению системы. Другое расширение, которое есть на временной шкале, - # 6268.

Я считаю, что столкнулся с этой ошибкой. Мой вариант использования и попытки обхода:

https://gist.github.com/toffaletti/6770126

Вот еще один пример этой ошибки (я думаю):

use std::util;

enum List<T> {
    Cons(T, ~List<T>),
    Nil
}

fn find_mut<'a,T>(prev: &'a mut ~List<T>, pred: |&T| -> bool) -> Option<&'a mut ~List<T>> {
    match prev {
        &~Cons(ref x, _) if pred(x) => {}, // NB: can't return Some(prev) here
        &~Cons(_, ref mut rs) => return find_mut(rs, pred),
        &~Nil => return None
    };
    return Some(prev)
}

Я хочу написать:

fn find_mut<'a,T>(prev: &'a mut ~List<T>, pred: |&T| -> bool) -> Option<&'a mut ~List<T>> {
    match prev {
        &~Cons(ref x, _) if pred(x) => return Some(prev),
        &~Cons(_, ref mut rs) => return find_mut(rs, pred),
        &~Nil => return None
    }
}

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

У меня было больше мыслей о том, как это закодировать. Мой основной план состоит в том, что для каждой ссуды будет два бита: экранированная версия и неэкранированная версия. Сначала мы добавляем неэкранированную версию. Когда ссылка ускользает, мы добавляем экранированные биты. Когда переменная (или временная и т. Д.) Становится мертвой, мы уничтожаем неэкранированные биты, но оставляем экранированные биты (если они установлены) нетронутыми. Я считаю, что это охватывает все основные примеры.

cc @ flaper87

Это касается этого вопроса?

use std::io::{MemReader, EndOfFile, IoResult};

fn read_block<'a>(r: &mut Reader, buf: &'a mut [u8]) -> IoResult<&'a [u8]> {
    match r.read(buf) {
        Ok(len) => Ok(buf.slice_to(len)),
        Err(err) => {
            if err.kind == EndOfFile {
                Ok(buf.slice_to(0))
            } else {
                Err(err)
            }
        }
    }
}

fn main() {
    let mut buf = [0u8, ..2];
    let mut reader = MemReader::new(~[67u8, ..10]);
    let mut block = read_block(&mut reader, buf);
    loop {
        //process block
        block = read_block(&mut reader, buf); //error here
}

cc me

Хорошие примеры в # 9113

cc me

Я могу ошибаться, но, похоже, следующий код тоже обнаруживает эту ошибку:

struct MyThing<'r> {
  int_ref: &'r int,
  val: int
}

impl<'r> MyThing<'r> {
  fn new(int_ref: &'r int, val: int) -> MyThing<'r> {
    MyThing {
      int_ref: int_ref,
      val: val
    }
  }

  fn set_val(&'r mut self, val: int) {
    self.val = val;
  }
}


fn main() {
  let to_ref = 10;
  let mut thing = MyThing::new(&to_ref, 30);
  thing.set_val(50);

  println!("{}", thing.val);
}

В идеале изменяемое заимствование, вызванное вызовом set_val, должно заканчиваться, как только функция вернется. Обратите внимание, что удаление поля int_ref из структуры (и связанного кода) устраняет проблему. Непоследовательное поведение.

@SergioBenitez Я не думаю, что это та же проблема. Вы явно запрашиваете, чтобы время жизни ссылки &mut self было таким же, как время жизни структуры.

Но этого делать не нужно. Вам вообще не нужна жизнь в set_val() .

fn set_val(&mut self, val: int) {
    self.val = val;
}

Я нашел еще один случай, который довольно сложно исправить:

/// A buffer which breaks chunks only after the specified boundary
/// sequence, or at the end of a file, but nowhere else.
pub struct ChunkBuffer<'a, T: Buffer+'a> {
    input:  &'a mut T,
    boundary: Vec<u8>,
    buffer: Vec<u8>
}

impl<'a, T: Buffer+'a> ChunkBuffer<'a,T> {
    // Called internally to make `buffer` valid.  This is where all our
    // evil magic lives.
    fn top_up<'b>(&'b mut self) -> IoResult<&'b [u8]> {
        // ...
    }
}

impl<'a,T: Buffer+'a> Buffer for ChunkBuffer<'a,T> {
    fn fill_buf<'a>(&'a mut self) -> IoResult<&'a [u8]> {
        if self.buffer.as_slice().contains_slice(self.boundary.as_slice()) {
            // Exit 1: Valid data in our local buffer.
            Ok(self.buffer.as_slice())
        } else if self.buffer.len() > 0 {
            // Exit 2: Add some more data to our local buffer so that it's
            // valid (see invariants for top_up).
            self.top_up()
        } else {
            {
                // Exit 3: Exit on error.
                let read = try!(self.input.fill_buf());
                if read.contains_slice(self.boundary.as_slice()) {
                    // Exit 4: Valid input from self.input. Yay!
                    return Ok(read)
                }
            }
            // Exit 5: Accumulate sufficient data in our local buffer (see
            // invariants for top_up).
            self.top_up()
        }
    }

…который дает:

/path/to/mylib/src/buffer.rs:168:13: 168:17 error: cannot borrow `*self` as mutable more than once at a time
/path/to/mylib/src/buffer.rs:168             self.top_up()
                                                        ^~~~
/path/to/mylib/src/buffer.rs:160:33: 160:43 note: previous borrow of `*self.input` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `*self.input` until the borrow ends
/path/to/mylib/src/buffer.rs:160                 let read = try!(self.input.fill_buf());
                                                                            ^~~~~~~~~~
<std macros>:1:1: 3:2 note: in expansion of try!
/path/to/mylib/src/buffer.rs:160:28: 160:56 note: expansion site
/path/to/mylib/src/buffer.rs:170:6: 170:6 note: previous borrow ends here
/path/to/mylib/src/buffer.rs:149     fn fill_buf<'a>(&'a mut self) -> IoResult<&'a [u8]> {
...
/path/to/mylib/src/buffer.rs:170     }

Это в основном эквивалент # 12147. Переменная read скрыта во внутренней области, но return связывает время жизни read со временем жизни всей функции. Большинство очевидных обходных путей терпят неудачу:

  1. Я не могу дважды вызвать input.fill_buf , потому что интерфейс Buffer не гарантирует, что он вернет данные, которые я только что проверил во второй раз. Если я _do_ попробую это сделать, код технически неверен, но программа проверки типов успешно его передаст.
  2. Я ничего не могу поделать с top_up , потому что это злой фрагмент кода, который должен все сложным образом видоизменять.
  3. Я не могу переместить вызывающую ошибку bind + test + return в другую функцию, потому что у нового API все равно будут те же проблемы (если только if let позволит мне протестировать _then_ bind?).

Кажется, будто ограничение 'a идеале не должно распространяться на весь путь до read . но я здесь над головой. Я собираюсь попробовать if let дальше.

Что ж, if let не попало в сборку прошлой ночью, но, поскольку это предположительно просто переписывание AST, я предполагаю, что он, вероятно, не работает так же, как match (что я тоже пробовал здесь).

Я не уверен, что делать дальше, если не использовать unsafe .

Мой текущий хак здесь выглядит так:

impl<'a,T: Buffer+'a> Buffer for ChunkBuffer<'a,T> {
    fn fill_buf<'a>(&'a mut self) -> IoResult<&'a [u8]> {
        // ...

            { // Block A.
                let read_or_err = self.input.fill_buf();
                match read_or_err {
                    Err(err) => { return Err(err); }
                    Ok(read) => {
                        if read.contains_slice(self.boundary.as_slice()) {
                               return Ok(unsafe { transmute(read) });
                        }
                    }
                }
            }
            self.top_up()

Теория заключается в том, что я отбрасываю время жизни от read (которое было привязано к self.input ) и сразу же применяю новое время жизни, основанное на self , которому принадлежит self.input . В идеале я хочу, чтобы у read было лексическое время жизни, равное блоку A, и я не хочу, чтобы он поднимался до уровня блока _lexical_ только потому, что я передал его return . Очевидно, что средство проверки времени жизни все еще должно доказать, что результат имеет время жизни, совместимое с 'a , но я не понимаю, почему это означает, что LIFETIME ( read ) необходимо объединить с LIFETIME ( 'a ).

Вполне возможно, что я сильно запутался или мой код ужасно небезопасен. :-) Но мне кажется, что это должно сработать, хотя бы потому, что я могу вызвать return self.input.fill_buf() без каких-либо проблем. Есть ли способ формализовать эту интуицию?

@emk, значит, это «жесткий код», который регионы SEME (то есть нелексические регионы) не исправляют, по крайней мере, сами по себе. У меня есть несколько идей, как исправить это в компиляторе, но это нетривиальное расширение для регионов SEME. Обычно есть способ обойти это путем реструктуризации кода. Дайте мне посмотреть, смогу ли я поэкспериментировать с этим и привести хороший пример.

Я хотел бы знать, пересматривается ли это для версии 1.0. В последнее время это происходит очень много, и я боюсь, что это превратится из вырезки из бумаги в рану, как только 1.0 привлечет внимание. Как наиболее заметная и обсуждаемая функция Rust, для заимствования очень важно, чтобы его можно было отшлифовать и использовать.

Есть ли временные рамки для этого в RFC?

@nikomatsakis У меня есть реальный простой пример, который выпал из работы над Entry API, если это помогает:

use std::collections::SmallIntMap;

enum Foo<'a>{ A(&'a mut SmallIntMap<uint>), B(&'a mut uint) }

fn main() {
    let mut map = SmallIntMap::<uint>::new();
    do_stuff(&mut map);
}

fn do_stuff(map: &mut SmallIntMap<uint>) -> Foo {
    match map.find_mut(&1) {
        None => {},  // Definitely can't return A here because of lexical scopes
        Some(val) => return B(val),
    }
    return A(map); // ERROR: borrowed at find_mut???
}

детский манеж

@bstrie И @pcwalton, и @zwarich потратили некоторое время, пытаясь фактически реализовать эту работу (с возможным RFC, идущим рука об руку). Они столкнулись с неожиданной сложностью, а это означает, что потребуется гораздо больше работы, чем ожидалось. Я думаю, что все согласны с вами в том, что эти ограничения важны и могут повлиять на первое впечатление о языке, но трудно сбалансировать это с уже запланированными изменениями, несовместимыми с предыдущими версиями.

Я считаю, что если эта проблема не решена в версии 1.0, то это приведет к тому, что люди будут обвинять подход проверки заимствований в целом, когда эта проблема не является неразрешимой по своей сути проблемой с проверкой заимствований AFAIK.

@blaenk Трудно не обвинить программу проверки заимствований, я сталкивался с этим и подобным (например, @Gankro) ежедневно. Расстраивает, когда обычное решение - это колебания (например, обходной путь) / или комментарии для реструктуризации вашего кода, чтобы он стал более «неизменяемым», функциональным и т. Д.

@mtanski Да, но вина _действительно_ лежит в программе проверки заимствований AFAIK, винить ее не неправильно. Я имею в виду то, что это может заставить новичков поверить в то, что это неотъемлемая, фундаментальная, неразрешимая проблема с _approach_ проверки заимствования, что является довольно коварным и, на самом деле, неверным убеждением.

Для случая: «пусть p = & ...; use-pa-bit-but-never-again; ожидайте-ссуду-срок-истек-здесь-здесь;» Я бы нашел приемлемым на данный момент инструкцию kill (p), чтобы вручную объявить конец области действия для этого заимствования. Более поздние версии могут просто игнорировать эту инструкцию, если она не нужна, или пометить ее как ошибку, если после нее будет обнаружено повторное использование p.

/* (wanted) */
/*
fn main() {

    let mut x = 10;

    let y = &mut x;

    println!("x={}, y={}", x, *y);

    *y = 11;

    println!("x={}, y={}", x, *y);
}
*/

/* had to */
fn main() {

    let mut x = 10;
    {
        let y = &x;

        println!("x={}, y={}", x, *y);
    }

    {
        let y = &mut x;

        *y = 11;
    }

    let y = &x;

    println!("x={}, y={}", x, *y);
}

В прелюдии есть метод drop (), который это делает. Но не похоже
чтобы помочь с изменяемыми заимствованиями.

В воскресенье, 5 апреля 2015 г., 13:41 axeoth [email protected] написал:

/ * (требуется) _ // _ fn main () {let mut x = 10; пусть y = & mut x; println! ("x = {}, y = {}", x, _y); * y = 11; println! ("x = {}, y = {}", x, * y);} _ /
/ * пришлось * / fn main () {

let mut x = 10;
{
    let y = &x;

    println!("x={}, y={}", x, *y);
}

{
    let y = &mut x;

    *y = 11;
}

let y = &x;

println!("x={}, y={}", x, *y);

}

-
Ответьте на это письмо напрямую или просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/6393#issuecomment -89848449.

Закрытие в пользу https://github.com/rust-lang/rfcs/issues/811

@metajack ссылка на исходный код - 404. Можете ли вы включить ее в строку для людей, читающих эту ошибку?

После некоторого покопания я считаю, что это эквивалентно исходному коду:
https://github.com/servo/servo/blob/5e406fab7ee60d9d8077d52d296f52500d72a2f6/src/servo/layout/box_builder.rs#L374

Или, скорее, это обходной путь, который я использовал, когда зарегистрировал эту ошибку. Исходный код до этого изменения выглядит следующим образом:
https://github.com/servo/servo/blob/7267f806a7817e48b0ac0c9c4aa23a8a0d288b03/src/servo/layout/box_builder.rs#L387 -L399

Я не уверен, насколько актуальны эти конкретные примеры сейчас, поскольку они были до Rust 1.0.

@metajack, было бы здорово, если бы в начале этого выпуска был очень простой (пост 1.0) пример. Эта проблема теперь является частью https://github.com/rust-lang/rfcs/issues/811

fn main() {
    let mut nums=vec![10i,11,12,13];
    *nums.get_mut(nums.len()-2)=2;
}

Думаю, я жаловался примерно на это:
https://is.gd/yfxUfw

Этот конкретный случай сейчас работает.

@vitiral Пример сегодняшнего Rust, который, как мне кажется, применим:

fn main() {
    let mut vec = vec!();

    match vec.first() {
        None => vec.push(5),
        Some(v) => unreachable!(),
    }
}

Рука None не заимствует.

Любопытно, что если вы не попытаетесь захватить int в руке Some (т. Е. Использовать Some(_) ), он компилируется.

@wyverland о да, я ударил это только вчера, довольно раздражает.

@metajack, можете ли вы отредактировать первое сообщение, включив в него этот пример?

Это еще не популярно в ночное время, но я просто хочу сказать, что теперь это компилируется:

#![feature(nll)]

fn main() {
    let mut vec = vec!();

    match vec.first() {
        None => vec.push(5),
        Some(v) => unreachable!(),
    }
}
Была ли эта страница полезной?
0 / 5 - 0 рейтинги