Язык программирования Rust
Скачать 7.02 Mb.
|
Листинг 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 заключается в его сочетании с типом 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(Rc >>, 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 { 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 и не будет некорректного указателя. В качестве примера, вместо того чтобы использовать список чей элемент знает только о следующем элементе, мы создадим дерево, чьи элементы знают о своих дочерних элементах и о своих родительских элементах. |