Главная страница
Навигация по странице:

  • Отслеживание заимствований во время выполнения с помощью

  • Листинг 15-23: Создание двух изменяемых ссылок в одной области видимости, чтобы убедиться, что RefCell вызовет панику

  • Наличие нескольких владельцев изменяемых данных путём объединения типов

  • Листинг 15-24: Использование Rc > для создания List, который мы можем изменять

  • Ссылочные зацикливания могут приводить к утечке памяти

  • Создание ссылочного зацикливания

  • Листинг 15-25: Объявление cons list, который содержит RefCell , чтобы мы могли изменять то, на что ссылается экземпляр

  • Листинг 15-26: Создание ссылочного цикла из двух значений List, указывающих друг на друга

  • Рисунок 15-4: Ссылочный цикл списков a и b, указывающих друг на друга

  • Язык программирования Rust


    Скачать 7.02 Mb.
    НазваниеЯзык программирования Rust
    Дата12.04.2023
    Размер7.02 Mb.
    Формат файлаpdf
    Имя файлаThe Rust Programming Language_ru.pdf
    ТипУчебник
    #1056301
    страница42 из 62
    1   ...   38   39   40   41   42   43   44   45   ...   62
    Листинг 15-22: Использование
    RefCell
    для изменения внутреннего значения, в то время как внешнее
    значение считается неизменяемым
    Поле sent_messages теперь имеет тип
    RefCell>
    вместо
    Vec
    . В
    функции new мы создаём новый экземпляр
    RefCell>
    для пустого вектора.
    Для реализации метода send первый параметр по-прежнему является неизменяемым для заимствования self
    , которое соответствует определению типажа. Мы вызываем borrow_mut для
    RefCell>
    в self.sent_messages
    , чтобы получить изменяемую ссылку на значение внутри
    RefCell>
    , которое является вектором. Затем мы можем вызвать push у изменяемой ссылки на вектор, чтобы отслеживать сообщения, отправленные во время теста.
    Последнее изменение, которое мы должны сделать, заключается в утверждении для проверки: чтобы увидеть, сколько элементов находится во внутреннем векторе, мы вызываем метод borrow у
    RefCell>
    , чтобы получить неизменяемую ссылку на внутренний вектор сообщений.
    Теперь, когда вы увидели как использовать
    RefCell
    , давайте изучим как он работает!
    Отслеживание заимствований во время выполнения с помощью RefCell
    #[cfg(test)]
    mod tests { use super::*; use std::cell::RefCell; struct
    MockMessenger
    { sent_messages: RefCell<
    Vec
    <
    String
    >>,
    } impl
    MockMessenger { fn new
    () -> MockMessenger {
    MockMessenger { sent_messages: RefCell::new(
    vec!
    []),
    }
    }
    } impl
    Messenger for
    MockMessenger { fn send
    (&
    self
    , message: &
    str
    ) { self
    .sent_messages.borrow_mut().push(
    String
    ::from(message));
    }
    }
    #[test]
    fn it_sends_an_over_75_percent_warning_message
    () {
    // --snip-- assert_eq!
    (mock_messenger.sent_messages.borrow().len(),
    1
    );
    }
    }

    При создании неизменных и изменяемых ссылок мы используем синтаксис
    &
    и
    &mut соответственно. У типа
    RefCell
    , мы используем методы borrow и borrow_mut
    ,
    которые являются частью безопасного API, который принадлежит
    RefCell
    . Метод borrow возвращает тип умного указателя
    Ref
    , метод borrow_mut возвращает тип умного указателя
    RefMut
    . Оба типа реализуют типаж
    Deref
    , поэтому мы можем рассматривать их как обычные ссылки.
    Тип
    RefCell
    отслеживает сколько умных указателей
    Ref
    и
    RefMut
    активны в данное время. Каждый раз, когда мы вызываем borrow
    , тип
    RefCell
    увеличивает количество активных заимствований. Когда значение
    Ref
    выходит из области видимости, то количество неизменяемых заимствований уменьшается на единицу. Как и с правилами заимствования во время компиляции,
    RefCell
    позволяет иметь много неизменяемых заимствований или одно изменяемое заимствование в любой момент времени.
    Если попытаться нарушить эти правила, то вместо получения ошибки компилятора, как это было бы со ссылками, реализация
    RefCell
    будет вызывать панику во время выполнения. В листинге 15-23 показана модификация реализации send из листинга 15-
    22. Мы намеренно пытаемся создать два изменяемых заимствования активных для одной и той же области видимости, чтобы показать как
    RefCell
    не позволяет нам делать так во время выполнения.
    Файл: src/lib.rs
    Листинг 15-23: Создание двух изменяемых ссылок в одной области видимости, чтобы убедиться, что
    RefCell
    вызовет панику
    Мы создаём переменную one_borrow для умного указателя
    RefMut
    возвращаемого из метода borrow_mut
    . Затем мы создаём другое изменяемое заимствование таким же образом в переменной two_borrow
    . Это создаёт две изменяемые ссылки в одной области видимости, что недопустимо. Когда мы запускаем тесты для нашей библиотеки, код в листинге 15-23 компилируется без ошибок, но тест завершится неудачно:
    impl
    Messenger for
    MockMessenger { fn send
    (&
    self
    , message: &
    str
    ) { let mut one_borrow = self
    .sent_messages.borrow_mut(); let mut two_borrow = self
    .sent_messages.borrow_mut(); one_borrow.push(
    String
    ::from(message)); two_borrow.push(
    String
    ::from(message));
    }
    }

    Обратите внимание, что код вызвал панику с сообщением already borrowed:
    BorrowMutError
    . Вот так тип
    RefCell
    обрабатывает нарушения правил заимствования во время выполнения.
    Решение отлавливать ошибки заимствования во время выполнения, а не во время компиляции, как мы сделали здесь, означает, что вы потенциально будете находить ошибки в своём коде на более поздних этапах разработки: возможно, не раньше, чем ваш код будет развернут в рабочем окружении. Кроме того, ваш код будет иметь небольшие потери производительности в процессе работы, поскольку заимствования будут отслеживаться во время выполнения, а не во время компиляции. Однако использование
    RefCell
    позволяет написать объект-имитатор, который способен изменять себя, чтобы сохранять сведения о тех значениях, которые он получал, пока вы использовали его в контексте, где разрешены только неизменяемые значения. Вы можете использовать
    RefCell
    , несмотря на его недостатки, чтобы получить больше функциональности, чем дают обычные ссылки.
    Наличие нескольких владельцев изменяемых данных путём
    объединения типов Rc и RefCell
    Обычный способ использования
    RefCell
    заключается в его сочетании с типом
    Rc
    . Напомним, что тип
    Rc
    позволяет иметь нескольких владельцев некоторых данных, но даёт только неизменяемый доступ к этим данным. Если у вас есть
    Rc
    ,
    который внутри содержит тип
    RefCell
    , вы можете получить значение, которое может иметь несколько владельцев и которое можно изменять!
    $
    cargo test
    Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
    Finished test [unoptimized + debuginfo] target(s) in 0.91s
    Running unittests src/lib.rs (target/debug/deps/limit_tracker- e599811fa246dbde) running 1 test test tests::it_sends_an_over_75_percent_warning_message ... FAILED failures:
    ---- tests::it_sends_an_over_75_percent_warning_message stdout ---- thread 'main' panicked at 'already borrowed: BorrowMutError', src/lib.rs:60:53 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::it_sends_an_over_75_percent_warning_message test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass '--lib'

    Например, вспомните пример cons списка листинга 15-18, где мы использовали
    Rc
    ,
    чтобы несколько списков могли совместно владеть другим списком. Поскольку
    Rc
    содержит только неизменяемые значения, мы не можем изменить ни одно из значений в списке после того, как мы их создали. Давайте добавим тип
    RefCell
    , чтобы получить возможность изменять значения в списках. В листинге 15-24 показано использование
    RefCell
    в определении
    Cons так, что мы можем изменить значение хранящееся во всех списках:
    Файл: src/main.rs
    Листинг 15-24: Использование
    Rc>
    для создания
    List
    , который мы можем изменять
    Мы создаём значение, которое является экземпляром
    Rc>
    и сохраняем его в переменной с именем value
    , чтобы получить к ней прямой доступ позже. Затем мы создаём
    List в переменной a
    с вариантом
    Cons
    , который содержит value
    . Нам нужно вызвать клонирование value
    , так как обе переменные a
    и value владеют внутренним значением
    5
    , а не передают владение из value в переменную a
    или не выполняют заимствование с помощью a
    переменной value
    Мы оборачиваем список у переменной a
    в тип
    Rc
    , поэтому при создании списков в переменные b
    и c
    они оба могут ссылаться на a
    , что мы и сделали в листинге 15-18.
    После создания списков a
    , b
    и c
    мы хотим добавить 10 к значению в value
    . Для этого вызовем borrow_mut у value
    , который использует функцию автоматического разыменования, о которой мы говорили в главе 5 (см. раздел "Где находится оператор
    -
    >
    ?"
    ) во внутреннее значение
    RefCell
    . Метод borrow_mut возвращает умный
    #[derive(Debug)]
    enum
    List
    {
    Cons(Rci32
    >>, Rc),
    Nil,
    } use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; fn main
    () { let value = Rc::new(RefCell::new(
    5
    )); let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil))); let b = Cons(Rc::new(RefCell::new(
    3
    )), Rc::clone(&a)); let c = Cons(Rc::new(RefCell::new(
    4
    )), Rc::clone(&a));
    *value.borrow_mut() +=
    10
    ; println!
    (
    "a after = {:?}"
    , a); println!
    (
    "b after = {:?}"
    , b); println!
    (
    "c after = {:?}"
    , c);
    }
    указатель
    RefMut
    , и мы используя оператор разыменования, изменяем внутреннее значение.
    Когда мы печатаем a
    , b
    и c
    то видим, что все они имеют изменённое значение равное
    15, а не 5:
    Эта техника довольно изящна! Используя
    RefCell
    , мы получаем внешне неизменяемое значение
    List
    . Но мы можем использовать методы
    RefCell
    , которые предоставляют доступ к его внутренностям, чтобы мы могли изменять наши данные,
    когда это необходимо. Проверка правил заимствования во время выполнения защищает нас от гонок данных, и иногда стоит немного пожертвовать производительностью ради такой гибкости наших структур данных. Обратите внимание, что
    RefCell
    не работает для многопоточного кода!
    Mutex
    - это thread-safe версия
    RefCell
    , а
    Mutex
    мы обсудим в главе 16.
    $
    cargo run
    Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished dev [unoptimized + debuginfo] target(s) in 0.63s
    Running `target/debug/cons-list` a after = Cons(RefCell { value: 15 }, Nil) b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil)) c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))

    Ссылочные зацикливания могут приводить к утечке
    памяти
    Гарантии безопасности памяти в Rust затрудняют, но не делают невозможным случайное выделение памяти, которое никогда не очищается (известное как утечка памяти ).
    Полное предотвращение утечек памяти не является одной из гарантий Rust, а это означает, что утечки памяти безопасны в Rust. Мы видим, что Rust допускает утечку памяти с помощью
    Rc
    и
    RefCell
    : можно создавать ссылки, в которых элементы ссылаются друг на друга в цикле. Это создаёт утечки памяти, потому что счётчик ссылок каждого элемента в цикле никогда не достигнет 0, а значения никогда не будут удалены.
    Создание ссылочного зацикливания
    Давайте посмотрим, как может произойти ситуация ссылочного зацикливания и как её
    предотвратить, начиная с определения перечисления
    List и метода tail в листинге
    15-25:
    Файл : src/main.rs
    Листинг 15-25: Объявление cons list, который содержит
    RefCell
    , чтобы мы могли изменять то, на что
    ссылается экземпляр
    Cons
    Мы используем другую вариацию определения
    List из листинга 15-5. Второй элемент в варианте
    Cons теперь
    RefCell>
    , что означает, что вместо возможности менять значение i32
    , как мы делали в листинге 15-24, мы хотим менять значение
    List
    , на которое указывает вариант
    Cons
    . Мы также добавляем метод tail
    , чтобы нам было удобно обращаться ко второму элементу, если у нас есть вариант
    Cons use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc;
    #[derive(Debug)]
    enum
    List
    {
    Cons(
    i32
    , RefCell>),
    Nil,
    } impl
    List { fn tail
    (&
    self
    ) ->
    Option
    <&RefCell>> { match self
    {
    Cons(_, item) =>
    Some
    (item),
    Nil =>
    None
    ,
    }
    }
    } fn main
    () {}

    В листинге 15-26 мы добавляем main функцию, которая использует определения листинга 15-25. Этот код создаёт список в переменной a
    и список b
    , который указывает на список a
    . Затем он изменяет список внутри a
    так, чтобы он указывал на b
    , создавая ссылочное зацикливание. В коде есть инструкции println!
    , чтобы показать значения счётчиков ссылок в различных точках этого процесса.
    Файл : src/main.rs
    Листинг 15-26: Создание ссылочного цикла из двух значений
    List
    , указывающих друг на друга
    Мы создаём экземпляр
    Rc
    содержащий значение
    List в переменной a
    с начальным списком
    5, Nil
    . Затем мы создаём экземпляр
    Rc
    содержащий другое значение
    List в переменной b
    , которое содержит значение 10 и указывает на список в a
    Мы меняем a
    так, чтобы он указывал на b
    вместо
    Nil
    , создавая зацикленность. Мы делаем это с помощью метода tail
    , чтобы получить ссылку на
    RefCell>
    из переменной a
    , которую мы помещаем в переменную link
    . Затем мы используем метод borrow_mut из типа
    RefCell>
    , чтобы изменить внутреннее значение типа
    Rc
    , содержащего начальное значение
    Nil на значение типа
    Rc
    взятое из переменной b
    Когда мы запускаем этот код, оставив последний println!
    закомментированным в данный момент, мы получим вывод:
    fn main
    () { let a = Rc::new(Cons(
    5
    , RefCell::new(Rc::new(Nil)))); println!
    (
    "a initial rc count = {}"
    , Rc::strong_count(&a)); println!
    (
    "a next item = {:?}"
    , a.tail()); let b = Rc::new(Cons(
    10
    , RefCell::new(Rc::clone(&a)))); println!
    (
    "a rc count after b creation = {}"
    , Rc::strong_count(&a)); println!
    (
    "b initial rc count = {}"
    , Rc::strong_count(&b)); println!
    (
    "b next item = {:?}"
    , b.tail()); if let
    Some
    (link) = a.tail() {
    *link.borrow_mut() = Rc::clone(&b);
    } println!
    (
    "b rc count after changing a = {}"
    , Rc::strong_count(&b)); println!
    (
    "a rc count after changing a = {}"
    , Rc::strong_count(&a));
    // Uncomment the next line to see that we have a cycle;
    // it will overflow the stack
    // println!("a next item = {:?}", a.tail());
    }

    Количество ссылок на экземпляры
    Rc
    как в a
    , так и в b
    равно 2 после того, как мы заменили список в a
    на ссылку на b
    . В конце main
    Rust уничтожает переменную b
    ,
    что уменьшает количество ссылок на
    Rc
    из b
    с 2 до 1. Память, которую
    Rc
    занимает в куче, не будет освобождена в этот момент, потому что количество ссылок на неё равно 1, а не 0. Затем Rust удаляет a
    , что уменьшает количество ссылок экземпляра
    Rc
    в a
    с 2 до 1. Память этого экземпляра также не может быть освобождена,
    поскольку другой экземпляр
    Rc
    по-прежнему ссылается на него. Таким образом,
    память, выделенная для списка не будет освобождена никогда. Чтобы наглядно представить этот цикл ссылок, мы создали диаграмму на рисунке 15-4.
    5 10
    a b
    Рисунок 15-4: Ссылочный цикл списков
    a
    и
    b
    , указывающих друг на друга
    Если вы удалите последний комментарий с println!
    и запустите программу, Rust будет пытаться печатать зацикленность в a
    , указывающей на b
    , указывающей на a
    и так далее, пока не переполниться стек.
    По сравнению с реальной программой, последствия создания цикла ссылок в этом примере не так страшны: сразу после создания цикла ссылок программа завершается.
    $
    cargo run
    Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished dev [unoptimized + debuginfo] target(s) in 0.53s
    Running `target/debug/cons-list` a initial rc count = 1 a next item = Some(RefCell { value: Nil }) a rc count after b creation = 2 b initial rc count = 1 b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) }) b rc count after changing a = 2 a rc count after changing a = 2

    Однако если более сложная программа выделит много памяти в цикле и будет удерживать её в течение длительного времени, программа будет потреблять больше памяти, чем ей нужно, и может перенапрячь систему, что приведёт к исчерпанию доступной памяти.
    Вызвать образование ссылочной зацикленности не просто, но и не невозможно. Если у вас есть значения
    RefCell
    которые содержат значения
    Rc
    или аналогичные вложенные комбинации типов с внутренней изменчивостью и подсчётом ссылок, вы должны убедиться, что вы не создаёте зацикленность; Вы не можете полагаться на то,
    что Rust их обнаружит. Создание ссылочной зацикленности являлось бы логической ошибкой в программе, для которой вы должны использовать автоматические тесты,
    проверку кода и другие практики разработки программного обеспечения для её
    минимизации.
    Другое решение для избежания ссылочной зацикленности - это реорганизация ваших структур данных, чтобы некоторые ссылки выражали владение, а другие - отсутствие владения. В результате можно иметь циклы, построенные на некоторых отношениях владения и некоторые не основанные на отношениях владения, тогда только отношения владения влияют на то, можно ли удалить значение. В листинге 15-25 мы всегда хотим,
    чтобы варианты
    Cons владели своим списком, поэтому реорганизация структуры данных невозможна. Давайте рассмотрим пример с использованием графов, состоящих из родительских и дочерних узлов, чтобы увидеть, когда отношения владения не являются подходящим способом предотвращения ссылочной зацикленности.
    Предотвращение ссылочной зацикленности: замена умного указателя
    Rc
    на Weak
    До сих пор мы демонстрировали, что вызов
    Rc::clone увеличивает strong_count экземпляра
    Rc
    , а экземпляр
    Rc
    удаляется, только если его strong_count равен 0.
    Вы также можете создать слабую ссылку на значение внутри экземпляра
    Rc
    , вызвав
    Rc::downgrade и передав ссылку на
    Rc
    . Сильные ссылки - это то с помощью чего вы можете поделиться владением экземпляра
    Rc
    . Слабые ссылки не отражают связи владения, и их подсчёт не влияет на то, когда экземпляр
    Rc
    будет очищен. Они не приведут к ссылочному циклу, потому что любой цикл, включающий несколько слабых ссылок, будет разорван, как только количество сильных ссылок для задействованных значений станет равным 0.
    Когда вы вызываете
    Rc::downgrade
    , вы получаете умный указатель типа
    Weak
    . Вместо того чтобы увеличить strong_count в экземпляре
    Rc
    на 1, вызов
    Rc::downgrade увеличивает weak_count на 1. Тип
    Rc
    использует weak_count для отслеживания количества существующих ссылок
    Weak
    , аналогично strong_count
    . Разница в том,
    что weak_count не должен быть равен 0, чтобы экземпляр
    Rc
    мог быть удалён.

    Поскольку значение, на которое ссылается
    Weak
    могло быть удалено, то необходимо убедиться, что это значение все ещё существует, чтобы сделать что-либо со значением на которое указывает
    Weak
    . Делайте это вызывая метод upgrade у экземпляра типа
    Weak
    , который вернёт
    Option>
    . Вы получите результат
    Some
    , если значение
    Rc
    ещё не было удалено и результат
    None
    , если значение
    Rc
    было удалено.
    Поскольку upgrade возвращает тип
    Option
    , Rust обеспечит обработку обоих случаев
    Some и
    None и не будет некорректного указателя.
    В качестве примера, вместо того чтобы использовать список чей элемент знает только о следующем элементе, мы создадим дерево, чьи элементы знают о своих дочерних элементах и о своих родительских элементах.
    1   ...   38   39   40   41   42   43   44   45   ...   62


    написать администратору сайта