ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1185
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
1 ... 41 42 43 44 45 46 47 48 ... 62
Листинг 16-15: Использование типа
Arc
для обёртывания
Mutex
, теперь несколько потоков могут
совместно владеть мьютексом
Код напечатает следующее:
Мы сделали это! Мы посчитали от 0 до 10, что может показаться не очень впечатляющим, но это позволило больше узнать про
Mutex
и безопасность потоков.
Вы также можете использовать структуру этой программы для выполнения более сложных операций, чем просто увеличение счётчика. Используя эту стратегию, вы можете разделить вычисления на независимые части, разделить эти части на потоки, а затем использовать
Mutex
, чтобы каждый поток обновлял конечный результат своей частью кода.
Обратите внимание, что если вы выполняете простые числовые операции, существуют более простые типы, чем
Mutex
, предоставляемые модулем std::sync::atomic стандартной библиотеки
. Эти типы обеспечивают безопасный многопоточный атомарный доступ для примитивных типов. В этом примере мы решили использовать
Mutex
с примитивным типом, чтобы сосредоточиться на том, как работает
Mutex
Сходства RefCell
Вы могли заметить, что counter сам по себе не изменяемый (immutable), но мы можем получить изменяемую ссылку на значение внутри него; это означает, что
Mutex
use std::sync::{Arc, Mutex}; use std::thread; fn main
() { let counter = Arc::new(Mutex::new(
0
)); let mut handles = vec!
[]; for
_ in
0 10
{ let counter = Arc::clone(&counter); let handle = thread::spawn(
move
|| { let mut num = counter.lock().unwrap();
*num +=
1
;
}); handles.push(handle);
} for handle in handles { handle.join().unwrap();
} println!
(
"Result: {}"
, *counter.lock().unwrap());
}
Result: 10
обеспечивает внутреннюю изменяемость, также как и семейство
Cell типов. Мы использовали
RefCell
в главе 15, чтобы получить возможность изменять содержимое внутри
Rc
, теперь аналогичным образом мы используем
Mutex
для изменения содержимого внутри
Arc
Ещё одна деталь, на которую стоит обратить внимание: Rust не может защитить вас от всевозможных логических ошибок при использовании
Mutex
. Вспомните в главе 15,
что использование
Rc
сопряжено с риском создания ссылочной зацикленности, где два значения
Rc
ссылаются друг на друга, что приводит к утечкам памяти.
Аналогичным образом,
Mutex
сопряжён с риском создания взаимных блокировок
(deadlocks). Это происходит, когда операции необходимо заблокировать два ресурса и каждый из двух потоков получил одну из блокировок, заставляя оба потока ждать друг друга вечно. Если вам интересна тема взаимных блокировок, попробуйте создать программу Rust, которая её содержит; затем исследуйте стратегии устранения взаимных блокировок для мьютексов на любом языке и попробуйте реализовать их в Rust.
Документация стандартной библиотеки для
Mutex
и
MutexGuard предлагает полезную информацию.
Мы завершим эту главу, рассказав о типажах
Send и
Sync и о том, как мы можем использовать их с пользовательскими типами.
Cell типов. Мы использовали
RefCell
в главе 15, чтобы получить возможность изменять содержимое внутри
Rc
, теперь аналогичным образом мы используем
Mutex
для изменения содержимого внутри
Arc
Ещё одна деталь, на которую стоит обратить внимание: Rust не может защитить вас от всевозможных логических ошибок при использовании
Mutex
. Вспомните в главе 15,
что использование
Rc
сопряжено с риском создания ссылочной зацикленности, где два значения
Rc
ссылаются друг на друга, что приводит к утечкам памяти.
Аналогичным образом,
Mutex
сопряжён с риском создания взаимных блокировок
(deadlocks). Это происходит, когда операции необходимо заблокировать два ресурса и каждый из двух потоков получил одну из блокировок, заставляя оба потока ждать друг друга вечно. Если вам интересна тема взаимных блокировок, попробуйте создать программу Rust, которая её содержит; затем исследуйте стратегии устранения взаимных блокировок для мьютексов на любом языке и попробуйте реализовать их в Rust.
Документация стандартной библиотеки для
Mutex
и
MutexGuard предлагает полезную информацию.
Мы завершим эту главу, рассказав о типажах
Send и
Sync и о том, как мы можем использовать их с пользовательскими типами.
Расширенная многопоточность с помощью типажей
Sync
и Send
Интересно, что сам язык Rust имеет очень мало возможностей для многопоточности.
Почти все функции многопоточности о которых мы говорили в этой главе, были частью стандартной библиотеки, а не языка. Ваши варианты работы с многопоточностью не ограничиваются языком или стандартной библиотекой; Вы можете написать свой собственный многопоточный функционал или использовать возможности написанные другими.
Тем не менее, в язык встроены две концепции многопоточности: std::marker типажи
Sync и
Send
Разрешение передачи во владение между потоками с помощью Send
Маркерный типаж
Send указывает, что владение типом реализующим
Send
, может передаваться между потоками. Почти каждый тип Rust является типом
Send
, но есть некоторые исключения, вроде
Rc
: он не может быть
Send
, потому что если вы клонировали значение
Rc
и попытались передать владение клоном в другой поток,
оба потока могут обновить счётчик ссылок одновременно. По этой причине
Rc
реализован для использования в однопоточных ситуациях, когда вы не хотите платить за снижение производительности.
Следовательно, система типов Rust и ограничений типажа гарантируют, что вы никогда не сможете случайно небезопасно отправлять значение
Rc
между потоками. Когда мы попытались сделать это в листинге 16-14, мы получили ошибку, the trait Send is not implemented for Rc
. Когда мы переключились на
Arc
, который является типом
Send
, то код скомпилировался.
Любой тип полностью состоящий из типов
Send автоматически помечается как
Send
Почти все примитивные типы являются
Send
, кроме сырых указателей, которые мы обсудим в главе 19.
Разрешение доступа из нескольких потоков с Sync
Маркерный типаж
Sync указывает, что на тип реализующий
Sync можно безопасно ссылаться из нескольких потоков. Другими словами, любой тип
T
является типом
Sync
,
если
&T
(ссылка на
T
) является типом
Send
, что означает что ссылку можно безопасно отправить в другой поток. Подобно
Send
, примитивные типы являются типом
Sync
, а типы полностью скомбинированные из типов
Sync
, также являются
Sync типом.
Умный указатель
Rc
не является
Sync типом по тем же причинам, по которым он не является
Send
. Тип
RefCell
(о котором мы говорили в главе 15) и семейство
связанных типов
Cell
не являются
Sync
. Реализация проверки заимствования,
которую делает тип
RefCell
во время выполнения программы не является поточно- безопасной. Умный указатель
Mutex
является типом
Sync и может использоваться для совместного доступа из нескольких потоков, как вы уже видели в разделе
«Совместное использование
Mutex
между несколькими потоками»
Реализация Send и Sync вручную небезопасна
Поскольку типы созданные из типажей
Send и
Sync автоматически также являются типами
Send и
Sync
, мы не должны реализовывать эти типажи вручную. Являясь маркерными типажами у них нет никаких методов для реализации. Они просто полезны для реализации инвариантов, связанных с многопоточностью.
Ручная реализация этих типажей включает в себя реализацию небезопасного кода Rust.
Мы поговорим об использовании небезопасного кода Rust в главе 19; на данный момент важная информация заключается в том, что для создания новых многопоточных типов,
не состоящих из частей
Send и
Sync необходимо тщательно продумать гарантии безопасности. В
Rustonomicon есть больше информации об этих гарантиях и о том как их соблюдать.
Итоги
Это не последний случай, когда вы увидите многопоточность в этой книге: проект в главе
20 будет использовать концепции этой главы для более реалистичного случая, чем небольшие примеры обсуждаемые здесь.
Как упоминалось ранее, поскольку в языке Rust очень мало того, с помощью чего можно управлять многопоточностью, многие решения реализованы в виде крейтов. Они развиваются быстрее, чем стандартная библиотека, поэтому обязательно поищите в
Интернете текущие современные крейты.
Стандартная библиотека Rust предоставляет каналы для передачи сообщений и типы умных указателей, такие как
Mutex
и
Arc
, которые можно безопасно использовать в многопоточных контекстах. Система типов и анализатор заимствований гарантируют, что код использующий эти решения не будет содержать гонки данных или недействительные ссылки. Получив компилирующийся код, вы можете быть уверены,
что он будет успешно работать в нескольких потоках без ошибок, которые трудно обнаружить в других языках. Многопоточное программирование больше не является концепцией, которую стоит опасаться: иди вперёд и сделай свои программы многопоточными безбоязненно!
Далее мы поговорим об идиоматичных способах моделирования проблем и структурирования решений по мере усложнения ваших программ на Rust. Кроме того,
Cell
не являются
Sync
. Реализация проверки заимствования,
которую делает тип
RefCell
во время выполнения программы не является поточно- безопасной. Умный указатель
Mutex
является типом
Sync и может использоваться для совместного доступа из нескольких потоков, как вы уже видели в разделе
«Совместное использование
Mutex
между несколькими потоками»
Реализация Send и Sync вручную небезопасна
Поскольку типы созданные из типажей
Send и
Sync автоматически также являются типами
Send и
Sync
, мы не должны реализовывать эти типажи вручную. Являясь маркерными типажами у них нет никаких методов для реализации. Они просто полезны для реализации инвариантов, связанных с многопоточностью.
Ручная реализация этих типажей включает в себя реализацию небезопасного кода Rust.
Мы поговорим об использовании небезопасного кода Rust в главе 19; на данный момент важная информация заключается в том, что для создания новых многопоточных типов,
не состоящих из частей
Send и
Sync необходимо тщательно продумать гарантии безопасности. В
Rustonomicon есть больше информации об этих гарантиях и о том как их соблюдать.
Итоги
Это не последний случай, когда вы увидите многопоточность в этой книге: проект в главе
20 будет использовать концепции этой главы для более реалистичного случая, чем небольшие примеры обсуждаемые здесь.
Как упоминалось ранее, поскольку в языке Rust очень мало того, с помощью чего можно управлять многопоточностью, многие решения реализованы в виде крейтов. Они развиваются быстрее, чем стандартная библиотека, поэтому обязательно поищите в
Интернете текущие современные крейты.
Стандартная библиотека Rust предоставляет каналы для передачи сообщений и типы умных указателей, такие как
Mutex
и
Arc
, которые можно безопасно использовать в многопоточных контекстах. Система типов и анализатор заимствований гарантируют, что код использующий эти решения не будет содержать гонки данных или недействительные ссылки. Получив компилирующийся код, вы можете быть уверены,
что он будет успешно работать в нескольких потоках без ошибок, которые трудно обнаружить в других языках. Многопоточное программирование больше не является концепцией, которую стоит опасаться: иди вперёд и сделай свои программы многопоточными безбоязненно!
Далее мы поговорим об идиоматичных способах моделирования проблем и структурирования решений по мере усложнения ваших программ на Rust. Кроме того,
мы обсудим как идиомы Rust связаны с теми, с которыми вы, возможно, знакомы по объектно-ориентированному программированию.
Возможности объектно-
ориентированного программирования в
Rust
Объектно-ориентированное программирование (ООП) — это способ построения программ. Объекты, как программная концепция, были введены в язык программирования Simula в 1960-х годах. Эти объекты повлияли на архитектуру программирования Алана Кея, в которой объекты передают сообщения друг другу.
Чтобы описать эту архитектуру, он ввёл термин объектно-ориентированное
программирование в 1967 году. Есть много конкурирующих определений ООП, и по некоторым из этих определений Rust является объектно-ориентированным, а по другим
— нет. В этой главе мы рассмотрим некоторые характеристики, которые обычно считаются объектно-ориентированными, и то, как эти характеристики транслируются в идиомы языка Rust. Затем мы покажем, как реализовать шаблон объектно- ориентированного проектирования в Rust, и обсудим компромиссы между этим вариантом и решением, использующим вместо этого некоторые сильные стороны Rust.
Характеристики объектно-ориентированных языков
В сообществе разработчиков нет согласия относительно того, какие особенности языка делают его объектно-ориентированным. На Rust повлияли многие парадигмы программирования, включая ООП; например, в главе 13 мы изучили вещи, пришедшие из функционального программирования. С некоторыми оговорками, ООП языки обладают некоторыми общими характеристики, а именно объектами, инкапсуляцией и наследованием. Давайте посмотрим, что означает каждая из этих характеристик, и поддерживает ли её Rust.
Объекты содержат данные и поведение
Книга «Приёмы объектно-ориентированного проектирования. Шаблоны проектирования» (1994), называемая также «книгой банды четырёх», является каталогом объектно-ориентированных шаблонов проектирования. Объектно-ориентированные программы определяются в ней следующим образом:
Объектно-ориентированные программы состоят из объектов. Объект объединяет данные и процедуры, которые работают с этими данными. Эти процедуры обычно называются методами или операциями.
В соответствии с этим определением, Rust является объектно-ориентированным языком:
в структурах и перечислениях содержатся данные, а в блоках impl определяются методы для них. Хотя структуры и перечисления, имеющие методы, не называются объектами,
они обеспечивают функциональность, соответствующую определению объектов в книге банды четырёх.
Инкапсуляция, скрывающая детали реализации
Другим аспектом, обычно связанным с объектно-ориентированным программированием, является идея инкапсуляции: детали реализации объекта недоступны для кода, использующего этот объект. Единственный способ взаимодействия с объектом — через его публичный интерфейс; код, использующий этот объект, не должен иметь возможности взаимодействовать с внутренними свойствами объекта и напрямую изменять его данные или поведение. Инкапсуляция позволяет изменять и реорганизовывать внутренние свойства объекта без необходимости изменять код,
который использует объект.
Как мы обсудили в главе 7, мы можем использовать ключевое слово pub чтобы решить,
какие модули, типы, функции и методы в нашем коде должны быть публичными; по умолчанию все остальное является приватным. Например, мы можем определить структуру
AveragedCollection
, которая имеет поле, содержащее вектор значений типа
i32
. Структура также может иметь поле содержащее среднее значение в векторе, так что всякий раз, когда кто-либо захочет получить среднее значение элементов вектора, нам не нужно вычислять его заново, Другими словами,
AveragedCollection будет кэшировать рассчитанное среднее значение для нас. В примере 17-1 приведено определение структуры
AveragedCollection
:
Файл: src/lib.rs
Листинг 17-1: структура
AveragedCollection
содержит список целых чисел и среднее значение элементов в
коллекции.
Обратите внимание, что структура помечена ключевым словом pub
, что позволяет другому коду её использовать, однако, поля внутри структуры остаются недоступными.
Это важно, потому что мы хотим гарантировать обновление среднего значения при добавлении или удалении элемента из списка. Мы можем получить нужное поведение,
определив в структуре методы add
, remove и average
, как показано в примере 17-2:
Файл: src/lib.rs
Листинг 17-2: Реализация публичных методов
add
,
remove
и
average
структуры
AveragedCollection pub struct
AveragedCollection
{ list:
Vec
<
i32
>, average: f64
,
} impl
AveragedCollection { pub fn add
(&
mut self
, value: i32
) { self
.list.push(value); self
.update_average();
} pub fn remove
(&
mut self
) ->
Option
<
i32
> { let result = self
.list.pop(); match result {
Some
(value) => { self
.update_average();
Some
(value)
}
None
=>
None
,
}
} pub fn average
(&
self
) -> f64
{ self
.average
} fn update_average
(&
mut self
) { let total: i32
= self
.list.iter().sum(); self
.average = total as f64
/ self
.list.len() as f64
;
}
}
. Структура также может иметь поле содержащее среднее значение в векторе, так что всякий раз, когда кто-либо захочет получить среднее значение элементов вектора, нам не нужно вычислять его заново, Другими словами,
AveragedCollection будет кэшировать рассчитанное среднее значение для нас. В примере 17-1 приведено определение структуры
AveragedCollection
:
Файл: src/lib.rs
Листинг 17-1: структура
AveragedCollection
содержит список целых чисел и среднее значение элементов в
коллекции.
Обратите внимание, что структура помечена ключевым словом pub
, что позволяет другому коду её использовать, однако, поля внутри структуры остаются недоступными.
Это важно, потому что мы хотим гарантировать обновление среднего значения при добавлении или удалении элемента из списка. Мы можем получить нужное поведение,
определив в структуре методы add
, remove и average
, как показано в примере 17-2:
Файл: src/lib.rs
Листинг 17-2: Реализация публичных методов
add
,
remove
и
average
структуры
AveragedCollection pub struct
AveragedCollection
{ list:
Vec
<
i32
>, average: f64
,
} impl
AveragedCollection { pub fn add
(&
mut self
, value: i32
) { self
.list.push(value); self
.update_average();
} pub fn remove
(&
mut self
) ->
Option
<
i32
> { let result = self
.list.pop(); match result {
Some
(value) => { self
.update_average();
Some
(value)
}
None
=>
None
,
}
} pub fn average
(&
self
) -> f64
{ self
.average
} fn update_average
(&
mut self
) { let total: i32
= self
.list.iter().sum(); self
.average = total as f64
/ self
.list.len() as f64
;
}
}
Публичные методы add
, remove и average являются единственным способом получить или изменить данные в экземпляре
AveragedCollection
. Когда элемент добавляется в list методом add
, или удаляется с помощью метода remove
, код реализации каждого из этих методов вызывает приватный метод update_average
, который позаботится об обновлении поля average
Мы оставляем поля list и average приватными, чтобы внешний код не мог добавлять или удалять элементы непосредственно в поле list
; в противном случае поле average может оказаться не синхронизировано при изменении list
. Метод average возвращает значение в поле average
, что позволяет внешнему коду читать значение average
, но не изменять его.
Поскольку мы инкапсулировали детали реализации структуры
AveragedCollection
, мы можем легко изменить такие аспекты, как структура данных, в будущем. Например, мы могли бы использовать
HashSet
вместо
Vec
для поля list
. Благодаря тому,
что сигнатуры публичных методов add
, remove и average остаются неизменными, код,
использующий
AveragedCollection
, также не будет нуждаться в изменении. У нас бы не получилось этого достичь, если бы мы сделали поле list доступным внешнему коду:
HashSet
и
Vec
имеют разные методы для добавления и удаления элементов,
поэтому внешний код, вероятно, должен измениться, если он модифицирует list напрямую.
Если инкапсуляция является обязательным аспектом для определения языка как объектно-ориентированного, то Rust соответствует этому требованию. Возможность использовать или не использовать модификатор доступа pub для различных частей кода позволяет скрыть детали реализации.
Наследование как система типов и способ совместного использования
кода
Наследование — это механизм, с помощью которого объект может быть унаследовать элементы из определения другого объекта, то есть получить данные и поведение родительского объекта без необходимости повторно их определять.
Если язык должен иметь наследование, чтобы быть объектно-ориентированным, то Rust таким не является. Здесь нет способа определить структуру, наследующую поля и реализации методов родительской структуры, без использования макроса.
Однако, если вы привыкли иметь наследование в своём наборе инструментов для программирования, вы можете использовать другие решения в Rust, в зависимости от того, по какой причине вы изначально хотите использовать наследование.
Вы могли бы выбрать наследование по двум основным причинам. Одна из них - возможность повторного использования кода: вы можете реализовать определённое поведение для одного типа, а наследование позволит вам повторно использовать эту
реализацию для другого типа. В Rust для этого есть ограниченный способ,
использующий реализацию метода типажа по умолчанию, который вы видели в листинге 10-14, когда мы добавили реализацию по умолчанию в методе summarize типажа
Summary
. Любой тип, реализующий свойство
Summary будет иметь доступный метод summarize без дополнительного кода. Это похоже на то, как родительский класс имеет реализацию метода, и класс-наследник тоже имеет реализацию метода. Мы также можем переопределить реализацию по умолчанию для метода summarize
, когда реализуем типаж
Summary
, что похоже на дочерний класс, переопределяющий реализацию метода, унаследованного от родительского класса.
Вторая причина использования наследования относится к системе типов: чтобы иметь возможность использовать дочерний тип в тех же места, что и родительский. Эта возможность также называется полиморфизм и означает возможность подменять объекты во время исполнения, если они имеют одинаковые характеристики.
Полиморфизм
Для многих людей полиморфизм является синонимом наследования. Но на самом деле это более общая концепция, которая относится к коду, который может работать с данными разных типов. Для наследования эти типы обычно являются подклассами. Вместо этого Rust использует обобщённые типы для абстрагирования от типов, и ограничения типажей (trait bounds) для указания того, какие возможности эти типы должны предоставлять. Это иногда называют ограниченным
параметрическим полиморфизмом.
Вместо этого Rust использует обобщённые типы для абстрагирования от типов, и ограничения типажей (trait bounds) для указания того, какие возможности эти типы должны предоставлять. Это иногда называют ограниченным параметрическим
полиморфизмом.
Наследование, как подход к разработке, в последнее время утратило популярность во многих языках программирования, поскольку часто существует риск, что мы будем наследовать код чаще, чем это необходимо. Подклассы не всегда должны обладать всеми характеристиками родительского класса, но при использовании наследования другого варианта нет. Это может сделать дизайн программы менее гибким. Кроме этого,
появляется возможность вызова у подклассов методов, которые не имеют смысла или вызывают ошибки, потому что эти методы неприменимы к подклассу. Кроме того, в некоторых языках разрешается только одиночное наследование (т.е. подкласс может наследоваться только от одного класса), что ещё больше ограничивает гибкость разработки программы.
По этим причинам в Rust применяется альтернативный подход, с использованием типажей-объектов вместо наследования. Давайте посмотрим как типажи-объекты
использующий реализацию метода типажа по умолчанию, который вы видели в листинге 10-14, когда мы добавили реализацию по умолчанию в методе summarize типажа
Summary
. Любой тип, реализующий свойство
Summary будет иметь доступный метод summarize без дополнительного кода. Это похоже на то, как родительский класс имеет реализацию метода, и класс-наследник тоже имеет реализацию метода. Мы также можем переопределить реализацию по умолчанию для метода summarize
, когда реализуем типаж
Summary
, что похоже на дочерний класс, переопределяющий реализацию метода, унаследованного от родительского класса.
Вторая причина использования наследования относится к системе типов: чтобы иметь возможность использовать дочерний тип в тех же места, что и родительский. Эта возможность также называется полиморфизм и означает возможность подменять объекты во время исполнения, если они имеют одинаковые характеристики.
Полиморфизм
Для многих людей полиморфизм является синонимом наследования. Но на самом деле это более общая концепция, которая относится к коду, который может работать с данными разных типов. Для наследования эти типы обычно являются подклассами. Вместо этого Rust использует обобщённые типы для абстрагирования от типов, и ограничения типажей (trait bounds) для указания того, какие возможности эти типы должны предоставлять. Это иногда называют ограниченным
параметрическим полиморфизмом.
Вместо этого Rust использует обобщённые типы для абстрагирования от типов, и ограничения типажей (trait bounds) для указания того, какие возможности эти типы должны предоставлять. Это иногда называют ограниченным параметрическим
полиморфизмом.
Наследование, как подход к разработке, в последнее время утратило популярность во многих языках программирования, поскольку часто существует риск, что мы будем наследовать код чаще, чем это необходимо. Подклассы не всегда должны обладать всеми характеристиками родительского класса, но при использовании наследования другого варианта нет. Это может сделать дизайн программы менее гибким. Кроме этого,
появляется возможность вызова у подклассов методов, которые не имеют смысла или вызывают ошибки, потому что эти методы неприменимы к подклассу. Кроме того, в некоторых языках разрешается только одиночное наследование (т.е. подкласс может наследоваться только от одного класса), что ещё больше ограничивает гибкость разработки программы.
По этим причинам в Rust применяется альтернативный подход, с использованием типажей-объектов вместо наследования. Давайте посмотрим как типажи-объекты
реализуют полиморфизм в Rust.
1 ... 42 43 44 45 46 47 48 49 ... 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>>,
}
используемую для поиска методов типажа указанного типа во время выполнения. Мы создаём типаж-объект в таком порядке: используем какой-нибудь вид указателя,
например ссылку
&
или умный указатель
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
Vec
} impl
T: Draw,
{ pub fn run
(&
self
) { for component in self
.components.iter() { component.draw();
}
}
}
предпочтительным, поскольку определения будут мономорфизированы во время компиляции для использования с конкретными типами.
С другой стороны, с помощью метода, использующего типаж-объекты, один экземпляр
Screen может содержать
Vec
который содержит
Box
С другой стороны, с помощью метода, использующего типаж-объекты, один экземпляр
Screen может содержать
Vec
который содержит
Box