Язык программирования Rust
Скачать 7.02 Mb.
|
Листинг 15-18: Определение List , использующее Rc Нам нужно добавить оператор use , чтобы подключить тип Rc в область видимости, потому что он не входит в список автоматического импорта прелюдии. В main , мы создаём список владеющий 5 и 10, сохраняем его в новом Rc переменной a Затем при создании b и c , мы называем функцию Rc::clone и передаём ей ссылку на Rc как аргумент a Мы могли бы вызвать a.clone() , а не Rc::clone(&a) , но в Rust принято использовать Rc::clone в таком случае. Внутренняя реализация Rc::clone не делает глубокого копирования всех данных, как это происходит в типах большинства реализаций clone Вызов Rc::clone только увеличивает счётчик ссылок, что не занимает много времени. Глубокое копирование данных может занимать много времени. Используя Rc::clone для подсчёта ссылок, можно визуально различать виды клонирования с глубоким копированием и клонирования, которые увеличивают количество ссылок. При поиске в коде проблем с производительностью нужно рассмотреть только клонирование с глубоким копированием и игнорировать вызовы Rc::clone Клонирование Rc Давайте изменим рабочий пример в листинге 15-18, чтобы увидеть как изменяется число ссылок при создании и удалении ссылок на Rc внутри переменной a В листинге 15-19 мы изменим main так, чтобы она имела внутреннюю область видимости вокруг списка c ; тогда мы сможем увидеть, как меняется счётчик ссылок при выходе c из внутренней области видимости. Файл: src/main.rs enum List { Cons( i32 , Rc
Nil, } use crate::List::{Cons, Nil}; use std::rc::Rc; fn main () { let a = Rc::new(Cons( 5 , Rc::new(Cons( 10 , Rc::new(Nil))))); let b = Cons( 3 , Rc::clone(&a)); let c = Cons( 4 , Rc::clone(&a)); } Листинг 15-19: Печать количества ссылок В каждой части программы, где количество ссылок меняется, мы выводим количество ссылок, которое получаем, вызывая функцию Rc::strong_count . Эта функция названа strong_count , а не count , потому что тип Rc также имеет weak_count ; мы увидим, для чего используется weak_count в разделе "Предотвращение циклических ссылок: Превращение Rc в Weak " Код выводит в консоль: Можно увидеть, что Rc в переменной a имеет начальный счётчик ссылок равный 1; затем каждый раз при вызове clone счётчик увеличивается на 1. Когда c выходит из области видимости, счётчик уменьшается на 1. Нам не нужно вызывать функцию уменьшения счётчика ссылок, как при вызове Rc::clone для увеличения счётчика ссылок: реализация Drop автоматически уменьшает счётчик ссылок, когда значение Rc выходит из области видимости. В этом примере мы не наблюдаем того, что когда b , а затем a выходят из области видимости в конце main , счётчик становится равным 0, и Rc полностью очищается. Использование Rc позволяет одному значению иметь несколько владельцев, а счётчик гарантирует, что значение остаётся действительным до тех пор, пока любой из владельцев ещё существует. С помощью неизменяемых ссылок, тип Rc позволяет обмениваться данными между несколькими частями вашей программы только для чтения данных. Если тип Rc позволял бы иметь несколько изменяемых ссылок, вы могли бы нарушить одно из правил заимствования, описанных в главе 4: множественные изменяемые заимствования в одном и том же месте могут вызвать гонки данных (data races) и fn main () { let a = Rc::new(Cons( 5 , Rc::new(Cons( 10 , Rc::new(Nil))))); println! ( "count after creating a = {}" , Rc::strong_count(&a)); let b = Cons( 3 , Rc::clone(&a)); println! ( "count after creating b = {}" , Rc::strong_count(&a)); { let c = Cons( 4 , Rc::clone(&a)); println! ( "count after creating c = {}" , Rc::strong_count(&a)); } println! ( "count after c goes out of scope = {}" , Rc::strong_count(&a)); } $ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) Finished dev [unoptimized + debuginfo] target(s) in 0.45s Running `target/debug/cons-list` count after creating a = 1 count after creating b = 2 count after creating c = 3 count after c goes out of scope = 2 несогласованность данных. Но возможность изменять данные очень полезна! В следующем разделе мы обсудим шаблон внутренней изменчивости и тип RefCell , который можно использовать вместе с Rc для работы с этим ограничением. RefCell и шаблон внутренней изменяемости Внутренняя изменяемость - это паттерн проектирования Rust, который позволяет вам изменять данные даже при наличии неизменяемых ссылок на эти данные; обычно такое действие запрещено правилами заимствования. Для изменения данных паттерн использует unsafe код внутри структуры данных, чтобы обойти обычные правила Rust, регулирующие изменяемость и заимствование. Небезопасный (unsafe) код даёт понять компилятору, что мы самостоятельно следим за соблюдением этих правил, а не полагаемся на то, что компилятор будет делать это для нас; подробнее о небезопасном коде мы поговорим в главе 19. Мы можем использовать типы, в которых применяется паттерн внутренней изменяемости, только если мы можем гарантировать, что правила заимствования будут соблюдаться во время выполнения, несмотря на то, что компилятор не сможет этого гарантировать. В этом случае небезопасный код оборачивается безопасным API, и внешне тип остаётся неизменяемым. Давайте изучим данную концепцию с помощью типа данных RefCell , который реализует этот шаблон. Применение правил заимствования во время выполнения с помощью RefCell В отличие от Rc тип RefCell предоставляет единоличное владение данными, которые он содержит. В чем же отличие типа RefCell от Box ? Давайте вспомним правила заимствования из Главы 4: В любой момент времени вы можете иметь либо одну изменяемую ссылку либо сколько угодно неизменяемых ссылок (но не оба типа ссылок одновременно). Ссылки всегда должны быть действительными. С помощью ссылок и типа Box инварианты правил заимствования применяются на этапе компиляции. С помощью RefCell они применяются во время работы программы. Если вы нарушите эти правила, работая с ссылками, то будет ошибка компиляции. Если вы работаете с RefCell и нарушите эти правила, то программа вызовет панику и завершится. Преимущества проверки правил заимствования во время компиляции состоят в том, что ошибки будут обнаруживаться быстрее в процессе разработки и это не влияет на производительность во время выполнения программы, поскольку весь анализ выполняется заранее. По этим причинам проверка правил заимствования во время компиляции является лучшим выбором в большинстве случаев, именно поэтому она используется в Rust по умолчанию. Преимущество проверки правил заимствования во время выполнения заключается в том, что определённые сценарии, безопасные для памяти, разрешаются там, где они были бы запрещены проверкой во время компиляции. Статический анализ, как и компилятор Rust, по своей сути консервативен. Некоторые свойства кода невозможно обнаружить, анализируя код: самый известный пример - проблема остановки, которая выходит за рамки этой книги, но является интересной темой для исследования. Поскольку некоторый анализ невозможен, то если компилятор Rust не может быть уверен, что код соответствует правилам владения, он может отклонить корректную программу; таким образом он является консервативным. Если Rust принял некорректную программу, то пользователи не смогут доверять гарантиям, которые даёт Rust. Однако, если Rust отклонит корректную программу, то программист будет испытывать неудобства, но ничего катастрофического не произойдёт. Тип RefCell полезен, когда вы уверены, что ваш код соответствует правилам заимствования, но компилятор не может понять и гарантировать этого. Подобно типу Rc , тип RefCell предназначен только для использования в одно поточных сценариях и выдаст ошибку времени компиляции, если вы попытаетесь использовать его в много поточном контексте. Мы поговорим о том, как получить функциональность RefCell во много поточной программе в главе 16. Вот список причин выбора типов Box , Rc или RefCell : Тип Rc разрешает множественное владение одними и теми же данными; типы Box и RefCell разрешают иметь единственных владельцев. Тип Box разрешает неизменяемые или изменяемые владения, проверенные при компиляции; тип Rc разрешает только неизменяемые владения, проверенные при компиляции; тип RefCell разрешает неизменяемые или изменяемые владения, проверенные во время выполнения. Поскольку RefCell разрешает изменяемые заимствования, проверенные во время выполнения, можно изменять значение внутри RefCell даже если RefCell является неизменным. Изменение значения внутри неизменного значения является шаблоном внутренней изменяемости (interior mutability). Давайте посмотрим на ситуацию, в которой внутренняя изменяемость полезна и рассмотрим, как это возможно. Внутренняя изменяемость: изменяемое заимствование неизменяемого значения Следствием правил заимствования является то, что когда у вас есть неизменяемое значение, вы не можете заимствовать его с изменением. Например, этот код не будет компилироваться: Если вы попытаетесь скомпилировать этот код, вы получите следующую ошибку: Однако бывают ситуации, в которых было бы полезно, чтобы объект мог изменять себя при помощи своих методов, но казался неизменным для прочего кода. Код вне методов этого объекта не должен иметь возможности изменять его содержимое. Использование RefCell - один из способов получить возможность внутренней изменяемости, но при этом RefCell не позволяет полностью обойти правила заимствования: средство проверки правил заимствования в компиляторе позволяет эту внутреннюю изменяемость, однако правила заимствования проверяются во время выполнения. Если вы нарушите правила, то вместо ошибки компиляции вы получите panic! Давайте разберём практический пример, в котором мы можем использовать RefCell для изменения неизменяемого значения и посмотрим, почему это полезно. Вариант использования внутренней изменяемости: мок объекты Иногда во время тестирования программист использует один тип вместо другого для того, чтобы проверить определённое поведение и убедиться, что оно реализовано правильно. Такой тип-заместитель называется тестовым дублёром. Воспринимайте его как "каскадёра" в кинематографе, когда дублёр заменяет актёра для выполнения определённой сложной сцены. Тестовые дублёры заменяют другие типы при выполнении тестов. {Инсценировочные (Mock) объекты - это особый тип тестовых дублёров, которые сохраняют данные происходящих во время теста действий тем самым позволяя вам убедиться впоследствии, что все действия были выполнены правильно. В Rust нет объектов в том же смысле, в каком они есть в других языках и в Rust нет функциональности мок объектов, встроенных в стандартную библиотеку, как в некоторых других языках. Однако вы определённо можете создать структуру, которая будет служить тем же целям, что и мок объект. fn main () { let x = 5 ; let y = & mut x; } $ cargo run Compiling borrowing v0.1.0 (file:///projects/borrowing) error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable --> src/main.rs:3:13 | 2 | let x = 5; | - help: consider changing this to be mutable: `mut x` 3 | let y = &mut x; | ^^^^^^ cannot borrow as mutable For more information about this error, try `rustc --explain E0596`. error: could not compile `borrowing` due to previous error Вот сценарий, который мы будем тестировать: мы создадим библиотеку, которая отслеживает значение по отношению к заранее определённому максимальному значению и отправляет сообщения в зависимости от того, насколько текущее значение находится близко к такому максимальному значению. Эта библиотека может использоваться, например, для отслеживания квоты количества вызовов API пользователя, которые ему разрешено делать. Наша библиотека будет предоставлять только функции отслеживания того, насколько близко к максимальному значению находится значение и какие сообщения должны быть внутри в этот момент. Ожидается, что приложения, использующие нашу библиотеку, предоставят механизм для отправки сообщений: приложение может поместить сообщение в приложение, отправить электронное письмо, отправить текстовое сообщение или что-то ещё. Библиотеке не нужно знать эту деталь. Все что ему нужно - это что-то, что реализует типаж, который мы предоставим с названием Messenger Листинг 15-20 показывает код библиотеки: Файл: src/lib.rs Листинг 15-20: Библиотека для отслеживания степени приближения того или иного значения к максимально допустимой величине и предупреждения, в случае если значение достигает определённого уровня Одна важная часть этого кода состоит в том, что типаж Messenger имеет один метод send , принимающий аргументами неизменяемую ссылку на self и текст сообщения. Он является интерфейсом, который должен иметь наш мок объект. Другой важной частью является то, что мы хотим проверить поведение метода set_value у типа LimitTracker Мы можем изменить значение, которое передаём параметром value , но set_value ничего не возвращает и нет основания, чтобы мы могли бы проверить утверждения о выполнении метода. Мы хотим иметь возможность сказать, что если мы создаём LimitTracker с чем-то, что реализует типаж Messenger и с определённым значением для max , то когда мы передаём разные числа в переменной value экземпляр self.messenger отправляет соответствующие сообщения. pub trait Messenger { fn send (& self , msg: & str ); } pub struct LimitTracker < 'a , T: Messenger> { messenger: & 'a T, value: usize , max: usize , } impl < 'a , T> LimitTracker< 'a , T> where T: Messenger, { pub fn new (messenger: & 'a T, max: usize ) -> LimitTracker< 'a , T> { LimitTracker { messenger, value: 0 , max, } } pub fn set_value (& mut self , value: usize ) { self .value = value; let percentage_of_max = self .value as f64 / self .max as f64 ; if percentage_of_max >= 1.0 { self .messenger.send( "Error: You are over your quota!" ); } else if percentage_of_max >= 0.9 { self .messenger .send( "Urgent warning: You've used up over 90% of your quota!" ); } else if percentage_of_max >= 0.75 { self .messenger .send( "Warning: You've used up over 75% of your quota!" ); } } } Нам нужен мок объект, который вместо отправки электронного письма или текстового сообщения будет отслеживать сообщения, которые были ему поручены для отправки через send . Мы можем создать новый экземпляр мок объекта, создать LimitTracker с использованием мок объект для него, вызвать метод set_value у экземпляра LimitTracker , а затем проверить, что мок объект имеет ожидаемое сообщение. В листинге 15-21 показана попытка реализовать мок объект, чтобы сделать именно то что хотим, но анализатор заимствований не разрешит такой код: Файл: src/lib.rs Листинг 15-21: Попытка реализовать MockMessenger , которая не была принята механизмом проверки заимствований Этот тестовый код определяет структуру MockMessenger , в которой есть поле sent_messages со значениями типа Vec из String для отслеживания сообщений, которые поручены структуре для отправки. Мы также определяем ассоциированную функцию new , чтобы было удобно создавать новые экземпляры MockMessenger , которые создаются с пустым списком сообщений. Затем мы реализуем типаж Messenger для типа MockMessenger , чтобы передать MockMessenger в LimitTracker . В сигнатуре метода send #[cfg(test)] mod tests { use super::*; struct MockMessenger { sent_messages: Vec < String >, } impl MockMessenger { fn new () -> MockMessenger { MockMessenger { sent_messages: vec! [], } } } impl Messenger for MockMessenger { fn send (& self , message: & str ) { self .sent_messages.push( String ::from(message)); } } #[test] fn it_sends_an_over_75_percent_warning_message () { let mock_messenger = MockMessenger::new(); let mut limit_tracker = LimitTracker::new(&mock_messenger, 100 ); limit_tracker.set_value( 80 ); assert_eq! (mock_messenger.sent_messages.len(), 1 ); } } мы принимаем сообщение для передачи в качестве параметра и сохраняем его в MockMessenger внутри списка sent_messages В этом тесте мы проверяем, что происходит, когда LimitTracker сказано установить value в значение, превышающее 75 процентов от значения max . Сначала мы создаём новый MockMessenger , который будет иметь пустой список сообщений. Затем мы создаём новый LimitTracker и передаём ему ссылку на новый MockMessenger и max значение равное 100. Мы вызываем метод set_value у LimitTracker со значением 80, что составляет более 75 процентов от 100. Затем мы с помощью утверждения проверяем, что MockMessenger должен содержать одно сообщение из списка внутренних сообщений. Однако с этим тестом есть одна проблема, показанная ниже: Мы не можем изменять MockMessenger для отслеживания сообщений, потому что метод send принимает неизменяемую ссылку на self . Мы также не можем принять предложение из текста ошибки, чтобы использовать &mut self , потому что тогда сигнатура send не будет соответствовать сигнатуре в определении типажа Messenger (не стесняйтесь попробовать и посмотреть, какое сообщение об ошибке получите вы). Это ситуация, в которой внутренняя изменяемость может помочь! Мы сохраним sent_messages внутри типа RefCell , а затем в методе send сообщение сможет изменить список sent_messages для хранения сообщений, которые мы видели. Листинг 15-22 показывает, как это выглядит: Файл: src/lib.rs $ cargo test Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker) error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference --> src/lib.rs:58:13 | 2 | fn send(&self, msg: &str); | ----- help: consider changing that to be a mutable reference: `&mut self` 58 | self.sent_messages.push(String::from(message)); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable For more information about this error, try `rustc --explain E0596`. error: could not compile `limit-tracker` due to previous error warning: build failed, waiting for other jobs to finish... |