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

  • Определение типажа для общего поведения

  • Листинг 17-3: Определение типажа

  • Листинг 17-4: Определение структуры Screen с полем components, которое является вектором типаж- объектов, которые реализуют типаж

  • Листинг 17-5: Реализация метода run у структуры Screen, который вызывает метод draw каждого компонента из вектора

  • Листинг 17-6: Альтернативная реализация структуры Screen и метода run, используя обобщённый тип и ограничения типажа

  • Листинг 17-7: Структура Button реализует типаж

  • Листинг 17-8: Другой крейт, использующий gui и реализующий типаж Draw у структуры

  • Листинг 17-9: Использование типаж-объектов для хранения значений разных типов, реализующих один и тот же типаж

  • Листинг 17-10: Попытка использования типа, который не реализует типаж для типаж-объекта

  • Типаж-объекты выполняют динамическую диспетчеризацию (связывание)

  • Реализация объектно-ориентированного шаблона проектирования

  • Листинг 17-11: Код, демонстрирующий желаемое поведение, которое мы хотим получить в крейте

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


    Скачать 7.02 Mb.
    НазваниеЯзык программирования Rust
    Дата12.04.2023
    Размер7.02 Mb.
    Формат файлаpdf
    Имя файлаThe Rust Programming Language_ru.pdf
    ТипУчебник
    #1056301
    страница47 из 62
    1   ...   43   44   45   46   47   48   49   50   ...   62
    Использование типаж-объектов, допускающих
    значения разных типов
    В главе 8 мы упоминали, что одним из ограничений векторов является то, что они могут хранить элементы только одного типа. Мы создали обходное решение в листинге 8-9, где мы определили перечисление
    SpreadsheetCell в котором были варианты для хранения целых чисел, чисел с плавающей точкой и текста. Это означало, что мы могли хранить разные типы данных в каждой ячейке и при этом иметь вектор, представляющий строку из ячеек. Это очень хорошее решение, когда наши взаимозаменяемые элементы вектора являются типами с фиксированным набором, известным при компиляции кода.
    Однако иногда мы хотим, чтобы пользователь нашей библиотеки мог расширить набор типов, которые допустимы в конкретной ситуации. Чтобы показать как этого добиться,
    мы создадим пример инструмента с графическим интерфейсом пользователя (GUI),
    который просматривает список элементов, вызывает метод draw для каждого из них,
    чтобы нарисовать его на экране - это обычная техника для инструментов GUI. Мы создадим библиотечный крейт с именем gui
    , содержащий структуру библиотеки GUI.
    Этот крейт мог бы включать некоторые готовые типы для использования, такие как
    Button или
    TextField
    . Кроме того, пользователи такого крейта gui захотят создавать свои собственные типы, которые могут быть нарисованы: например, кто-то мог бы добавить тип
    Image
    , а кто-то другой добавить тип
    SelectBox
    Мы не будем реализовывать полноценную библиотеку GUI для этого примера, но покажем, как её части будут подходить друг к другу. На момент написания библиотеки мы не можем знать и определить все типы, которые могут захотеть создать другие программисты. Но мы знаем, что gui должен отслеживать множество значений различных типов и ему нужно вызывать метод draw для каждого из этих значений различного типа. Ему не нужно точно знать, что произойдёт, когда вызывается метод draw
    , просто у значения будет доступен такой метод для вызова.
    Чтобы сделать это на языке с наследованием, можно определить класс с именем
    Component у которого есть метод с названием draw
    . Другие классы, такие как
    Button
    ,
    Image и
    SelectBox наследуются от
    Component и следовательно, наследуют метод draw
    Каждый из них может переопределить реализацию метода draw
    , чтобы определить своё
    пользовательское поведение, но платформа может обрабатывать все типы, как если бы они были экземплярами
    Component и вызывать draw у них. Но поскольку в Rust нет наследования, нам нужен другой способ структурировать gui библиотеку, чтобы позволить пользователям расширять её новыми типами.
    Определение типажа для общего поведения
    Чтобы реализовать поведение, которое мы хотим иметь в gui
    , определим типаж с именем
    Draw
    , который будет содержать один метод с названием draw
    . Затем мы можем
    определить вектор, который принимает типаж-объект. Типаж-объект указывает как на экземпляр типа, реализующего указанный типаж, так и на внутреннюю таблицу,
    используемую для поиска методов типажа указанного типа во время выполнения. Мы создаём типаж-объект в таком порядке: используем какой-нибудь вид указателя,
    например ссылку
    &
    или умный указатель
    Box
    , затем ключевое слово dyn
    , а затем указываем соответствующий типаж. (Мы будем говорить о причине того, что типаж- объекты должны использовать указатель в разделе "Типы динамического размера и типаж
    Sized
    "
    главы 19). Мы можем использовать типаж-объекты вместо универсального или конкретного типа. Везде, где мы используем типаж-объект, система типов Rust проверит во время компиляции, что любое значение, используемое в этом контексте, будет реализовывать нужный типаж у типаж-объекта. Следовательно, нам не нужно знать все возможные типы во время компиляции.
    Мы упоминали, что в Rust мы воздерживаемся называть структуры и перечисления
    «объектами», чтобы отличать их от объектов в других языках. В структуре или перечислении данные в полях структуры и поведение в блоках impl разделены, тогда как в других языках данные и поведение объединены в одну концепцию, часто обозначающуюся как объект. Тем не менее, типаж-объекты являются более похожими на объекты на других языках, в том смысле, что они сочетают в себе данные и поведение. Но типаж-объекты отличаются от традиционных объектов тем, что не позволяют добавлять данные к типаж-объекту. Типаж-объекты обычно не настолько полезны, как объекты в других языках: их конкретная цель - обеспечить абстракцию через общее поведение.
    В листинге 17.3 показано, как определить типаж с именем
    Draw с помощью одного метода с именем draw
    :
    Файл: src/lib.rs
    Листинг 17-3: Определение типажа
    Draw
    Этот синтаксис должен выглядеть знакомым из наших дискуссий о том, как определять типажи в главе 10. Далее следует новый синтаксис: в листинге 17.4 определена структура с именем
    Screen
    , которая содержит вектор с именем components
    . Этот вектор имеет тип
    Box
    , который и является типаж-объектом; это замена для любого типа внутри
    Box который реализует типаж
    Draw
    Файл: src/lib.rs pub trait
    Draw
    { fn draw
    (&
    self
    );
    } pub struct
    Screen
    { pub components:
    Vec
    <
    Box
    <
    dyn
    Draw>>,
    }

    Листинг 17-4: Определение структуры
    Screen
    с полем
    components
    , которое является вектором типаж-
    объектов, которые реализуют типаж
    Draw
    В структуре
    Screen
    , мы определим метод run
    , который будет вызывать метод draw каждого элемента вектора components
    , как показано в листинге 17-5:
    Файл: src/lib.rs
    Листинг 17-5: Реализация метода
    run
    у структуры
    Screen
    , который вызывает метод
    draw
    каждого
    компонента из вектора
    Это работает иначе, чем определение структуры, которая использует параметр общего типа с ограничениями типажа. Обобщённый параметр типа может быть заменён только одним конкретным типом, тогда как типаж-объекты позволяют нескольким конкретным типам замещать типаж-объект во время выполнения. Например, мы могли бы определить структуру
    Screen используя общий тип и ограничение типажа, как показано в листинге 17-6:
    Файл: src/lib.rs
    Листинг 17-6: Альтернативная реализация структуры
    Screen
    и метода
    run
    , используя обобщённый тип и
    ограничения типажа
    Это вариант ограничивает нас экземпляром
    Screen
    , который имеет список компонентов всех типов
    Button или всех типов
    TextField
    . Если у вас когда-либо будут только однородные коллекции, использование обобщений и ограничений типажа является impl
    Screen { pub fn run
    (&
    self
    ) { for component in self
    .components.iter() { component.draw();
    }
    }
    } pub struct
    Screen
    { pub components:
    Vec
    ,
    } impl
    Screen where
    T: Draw,
    { pub fn run
    (&
    self
    ) { for component in self
    .components.iter() { component.draw();
    }
    }
    }
    предпочтительным, поскольку определения будут мономорфизированы во время компиляции для использования с конкретными типами.
    С другой стороны, с помощью метода, использующего типаж-объекты, один экземпляр
    Screen может содержать
    Vec
    который содержит
    Box

    Листинг 17-8: Другой крейт, использующий
    gui
    и реализующий типаж
    Draw
    у структуры
    SelectBox
    Пользователь нашей библиотеки теперь может написать свою функцию main для создания экземпляра
    Screen
    . К экземпляру
    Screen он может добавить
    SelectBox и
    Button
    , поместив каждый из них в
    Box
    , чтобы он стал типаж-объектом. Затем он может вызвать метод run у экземпляра
    Screen
    , который вызовет draw для каждого из компонентов. Листинг 17-9 показывает эту реализацию:
    Файл: src/main.rs
    Листинг 17-9: Использование типаж-объектов для хранения значений разных типов, реализующих один и
    тот же типаж
    use gui::Draw; struct
    SelectBox
    { width: u32
    , height: u32
    , options:
    Vec
    <
    String
    >,
    } impl
    Draw for
    SelectBox { fn draw
    (&
    self
    ) {
    // code to actually draw a select box
    }
    } use gui::{Button, Screen}; fn main
    () { let screen = Screen { components: vec!
    [
    Box
    ::new(SelectBox { width:
    75
    , height:
    10
    , options: vec!
    [
    String
    ::from(
    "Yes"
    ),
    String
    ::from(
    "Maybe"
    ),
    String
    ::from(
    "No"
    ),
    ],
    }),
    Box
    ::new(Button { width:
    50
    , height:
    10
    , label:
    String
    ::from(
    "OK"
    ),
    }),
    ],
    }; screen.run();
    }

    Когда мы писали библиотеку, мы не знали, что кто-то может добавить тип
    SelectBox
    , но наша реализация
    Screen могла работать с новым типом и рисовать его, потому что
    SelectBox реализует типаж
    Draw
    , что означает, что он реализует метод draw
    Эта концепция, касающаяся только сообщений на которые значение отвечает, в отличии от конкретного тип у значения, аналогична концепции duck typing в динамически типизированных языках: если что-то ходит как утка и крякает как утка, то она должна быть утка! В реализации метода run у
    Screen в листинге 17-5, run не нужно знать каким будет конкретный тип каждого компонента. Он не проверяет, является ли компонент экземпляром
    Button или
    SelectBox
    , он просто вызывает метод draw компонента.
    Указав
    Box
    в качестве типа значений в векторе components
    , мы определили
    Screen для значений у которых мы можем вызвать метод draw
    Преимущество использования типаж-объектов и системы типов Rust для написания кода,
    похожего на код с использованием концепции duck typing состоит в том, что нам не нужно во время выполнения проверять реализует ли значение в векторе конкретный метод или беспокоиться о получении ошибок, если значение не реализует метод, мы все равно вызываем метод. Rust не скомпилирует наш код, если значения не реализуют типаж, который нужен типаж-объектам.
    Например, листинг 17-10 демонстрирует, что случится если мы попытаемся добавить
    Screen{/code0 с String в качестве компонента вектора:
    Файл: src/main.rs
    Листинг 17-10: Попытка использования типа, который не реализует типаж для типаж-объекта
    Мы получим ошибку, потому что
    String не реализует типаж
    Draw
    :
    use gui::Screen; fn main
    () { let screen = Screen { components: vec!
    [
    Box
    ::new(
    String
    ::from(
    "Hi"
    ))],
    }; screen.run();
    }

    Эта ошибка даёт понять, что либо мы передаём в компонент
    Screen что-то, что мы не собирались передавать и мы тогда должны передать другой тип, либо мы должны реализовать типаж
    Draw у типа
    String
    , чтобы
    Screen мог вызывать draw у него.
    Типаж-объекты выполняют динамическую диспетчеризацию
    (связывание)
    Напомним, в разделе
    «Производительность кода с использованием обобщений»
    главы
    10 обсуждается процесс мономорфизации выполняемый компилятором, когда мы используем ограничения типажей для обобщённых типов: компилятор генерирует конкретные реализации функций и методов для каждого конкретного типа, который мы используем вместо параметра обобщённого типа. Код, полученный в результате мономорфизации, выполняет статическую диспетчеризацию, когда компилятор знает какой метод вы вызываете во время компиляции. Это противоположно подходу
    динамической диспетчеризации, когда компилятор не может сказать во время компиляции, какой метод вы вызываете. В случаях динамической диспетчеризации компилятор генерирует код, который во время выполнения определяет, какой метод необходимо вызывать.
    Когда мы используем типаж-объекты, Rust должен использовать динамическую диспетчеризацию. Компилятор не знает всех типов, которые могут быть использованы с кодом, использующим типаж-объекты, поэтому он не знает, какой метод реализован для какого типа при вызове. Вместо этого, во время выполнения, Rust использует указатели внутри типаж-объекта, чтобы узнать какой метод вызвать. Такой поиск вызывает дополнительные затраты во время исполнения, которые не требуются при статической диспетчеризации. Динамическая диспетчеризация также не позволяет компилятору выбрать встраивание кода метода, что в свою очередь делает невозможными некоторые оптимизации. Однако мы получили дополнительную гибкость в коде, который мы написали в листинге 17-5, и которую смогли поддержать в листинге 17-9, поэтому все "за"
    и "против" нужно рассматривать в комплексе.
    $
    cargo run
    Compiling gui v0.1.0 (file:///projects/gui) error[E0277]: the trait bound `String: Draw` is not satisfied
    -->
    src/main.rs:5:26
    |
    5 | components: vec![Box::new(String::from("Hi"))],
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String`
    |
    = help: the trait `Draw` is implemented for `Button`
    = note: required for the cast to the object type `dyn Draw`
    For more information about this error, try `rustc --explain E0277`. error: could not compile `gui` due to previous error

    Реализация объектно-ориентированного шаблона
    проектирования
    Шаблон "Состояние" — это объектно-ориентированный шаблон проектирования. Суть шаблона заключается в том, что мы определяем набор состояний, которые может иметь внутреннее значение. Состояния представлены набором объектов состояния, а поведение элемента изменяется в зависимости от его состояния. Мы рассмотрим пример структуры записи в блоге, в которой есть поле для хранения состояния, которое будет объектом состояния из набора «черновик», «обзор» или «опубликовано».
    Объекты состояния имеют общую функциональность: конечно в Rust мы используем структуры и типажи, а не объекты и наследование. Каждый объект состояния отвечает за своё поведение и сам определяет, когда он должен перейти в другое состояние. Элемент,
    который содержит объект состояния, ничего не знает о различиях в поведении состояний или о том, когда одно состояние должно перейти в другое.
    Преимуществом шаблона "Состояние" является то, что при изменении требований заказчика программы не требуется изменять код элемента, содержащего состояние, или код, использующий такой элемент. Нам нужно только обновить код внутри одного из объектов состояния, чтобы изменить его порядок действий, либо, возможно, добавить больше объектов состояния.
    Для начала реализуем шаблон "Состояние" более традиционным объектно- ориентированным способом, а затем воспользуемся подходом, более естественным для
    Rust. Давайте шаг за шагом реализуем поток действий для записи в блоге, использующий шаблон "Состояние".
    Окончательный функционал будет выглядеть так:
    1. Запись в блоге создаётся как пустой черновик.
    2. Когда черновик готов, запрашивается его проверка.
    3. После проверки происходит публикация записи.
    4. Только опубликованные записи блога возвращают содержимое записи на печать,
    поэтому сообщения, не прошедшие проверку, не могут быть опубликованы случайно.
    Любые другие изменения, сделанные в записи, не должны иметь никакого эффекта.
    Например, если мы попытаемся подтвердить черновик записи в блоге до того, как запросим проверку, запись должна остаться неопубликованным черновиком.
    В листинге 17-11 показан этот поток действий в виде кода: это пример использования
    API, который мы собираемся реализовать в библиотеке (крейте) с именем blog
    . Он пока не компилируется, потому что крейт blog ещё не создан.
    Файл: src/main.rs

    Листинг 17-11: Код, демонстрирующий желаемое поведение, которое мы хотим получить в крейте
    blog
    Мы хотим, чтобы пользователь мог создать новый черновик записи в блоге с помощью
    Post::new
    . Затем мы хотим разрешить добавление текста в запись блога. Если мы попытаемся получить содержимое записи сразу, до её проверки, мы не должны получить никакого текста на выходе, потому что запись все ещё является черновиком. Мы добавили утверждение (
    assert_eq!
    ) в коде для демонстрационных целей. Утверждение
    (assertion), что черновик записи блога должен возвращать пустую строку из метода content было бы отличным модульным тестом, но мы не собираемся писать тесты для этого примера.
    Далее мы хотим разрешить сделать запрос на проверку записи и хотим, чтобы content возвращал пустую строку, пока проверки не завершена. Когда запись пройдёт проверку,
    она должна быть опубликована, то есть при вызове content будет возвращён текст записи.
    Обратите внимание, что единственный тип из крейта, с которым мы взаимодействуем - это тип
    Post
    . Этот тип будет использовать шаблон "Состояние" и будет содержать значение, которое будет являться одним из трёх объектов состояний, представляющих различные состояния, в которых может находиться запись: "черновик", "ожидание проверки" или "опубликовано". Управление переходом из одного состояния в другое будет осуществляться внутренней логикой типа
    Post
    . Состояния будут переключаться в результате реакции на вызов методов экземпляра
    Post пользователями нашей библиотеки, но пользователи не должны управлять изменениями состояния напрямую.
    Кроме того, пользователи не должны иметь возможность ошибиться с состояниями,
    например, опубликовать сообщение до его проверки.
    1   ...   43   44   45   46   47   48   49   50   ...   62


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