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

  • Клонирование

  • Листинг 15-19: Печать количества ссылок

  • и шаблон внутренней изменяемости

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

  • Внутренняя изменяемость: изменяемое заимствование неизменяемого значения

  • Вариант использования внутренней изменяемости: мок объекты

  • Листинг 15-20: Библиотека для отслеживания степени приближения того или иного значения к максимально допустимой величине и предупреждения, в случае если значение достигает определённого

  • Листинг 15-21: Попытка реализовать MockMessenger, которая не была принята механизмом проверки заимствований

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


    Скачать 7.02 Mb.
    НазваниеЯзык программирования Rust
    Дата12.04.2023
    Размер7.02 Mb.
    Формат файлаpdf
    Имя файлаThe Rust Programming Language_ru.pdf
    ТипУчебник
    #1056301
    страница41 из 62
    1   ...   37   38   39   40   41   42   43   44   ...   62
    Листинг 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...

    1   ...   37   38   39   40   41   42   43   44   ...   62


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