Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 781
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
11
Трейты
Трейты в Scala являются фундаментальными повторно используемыми блоками кода. В трейте инкапсулируются определения тех методов и полей, которые затем могут повторно использоваться путем их примешивания в классы. В отличие от наследования классов, в котором каждый класс дол
жен быть наследником только одного суперкласса, в класс может примеши
ваться любое количество трейтов. В этой главе мы покажем, как работают трейты. Далее рассмотрим два наиболее распространенных способа их при
менения: расширение «тонких» интерфейсов и превращение их в «толстые», а также определение наращиваемых модификаций. Здесь мы покажем, как используется трейт
Ordered
, и сравним механизм трейтов с множественным наследованием, имеющимся в других языках.
11 .1 . Как работают трейты
Определение трейта похоже на определение класса, за исключением того, что в нем используется ключевое слово trait
. Пример показан в листинге 11.1.
Листинг 11.1. Определение трейта Philosophical trait Philosophical:
def philosophize = "На меня тратится память, следовательно, я существую!"
Данный трейт называется
Philosophical
. В нем не объявлен суперкласс, следовательно, как и у класса, у него есть суперкласс по умолчанию —
AnyRef
. В нем определяется один конкретный метод по имени philosophize
Это простой трейт, и его вполне достаточно, чтобы показать, как работают трейты.
230 Глава 11 • Трейты
После того как трейт определен, он может быть примешан в класс либо с помощью ключевого слова extends или with
, либо с помощью запятой.
Программисты, работающие со Scala, примешивают трейты, а не наследуют их, поскольку примешивание трейта весьма отличается от множественно
го наследования, встречающегося во многих других языках. Этот вопрос рассматривается в разделе 11.4. Например, в листинге 11.2 показан класс, в который с помощью ключевого слова extends примешивается трейт
Philosophical
Листинг 11.2. Примешивание трейта с использованием ключевого слова extends class Frog extends Philosophical:
override def toString = "зеленая"
Примешивать трейт можно с помощью ключевого слова extends
, в таком слу
чае происходит неявное наследование суперкласса трейта. Например, в ли
стинге 11.2 класс
Frog
(лягушка) становится подклассом
AnyRef
(это супер
класс для трейта
Philosophical
) и примешивает в себя трейт
Philosophical
Методы, унаследованные от трейта, могут использоваться точно так же, как и методы, унаследованные от суперкласса. Рассмотрим пример:
val frog = new Frog frog.philosophize() // На меня тратится память, следовательно, я существую!
Трейт также определяет тип. Рассмотрим пример, в котором
Philosophical используется как тип:
val phil: Philosophical = frog phil.philosophize // На меня тратится память, следовательно, я существую!
Типом phil является
Philosophical
, то есть трейт. Таким образом, пере
менная phil может быть инициализирована любым объектом, в чей класс примешан трейт
Philosophical
Если нужно примешать трейт в класс, который явно расширяет супер
класс, то ключевое слово extends используется для указания суперкласса, а для примешивания трейта — запятая (или ключевое слово with
). Пример показан в листинге 11.3. Если нужно примешать сразу несколько трейтов, то дополнительные трейты указываются с помощью ключевого слова with
Например, располагая трейтом
HasLegs
, вы, как показано в листинге 11.4, можете примешать в класс
Frog как трейт
Philosophical
, так и трейт
HasLegs
11 .1 . Как работают трейты 231
Листинг 11.3. Примешивание трейта с использованием запятой class Animal class Frog extends Animal with Philosophical {
override def toString = "зеленая"
}
Листинг 11.4. Примешивание нескольких трейтов class Animal trait HasLegs class Frog extends Animal, Philosophical, HasLegs:
override def toString = "зеленая"
В показанных ранее примерах класс
Frog наследовал реализацию метода philosophize из трейта
Philosophical
. В качестве альтернативного вариан
та метод philosophize в классе
Frog может быть переопределен. Синтаксис выглядит точно так же, как и при переопределении метода, объявленного в суперклассе. Рассмотрим пример:
class Animal class Frog extends Animal, Philosophical:
override def toString = "зеленая"
override def philosophize = s"Мне живется нелегко, потому что я $this!"
В новое определение класса
Frog попрежнему примешивается трейт
Phi- losophical
, поэтому его, как и раньше, можно использовать из перемен
ной данного типа. Но так как во
Frog переопределено определение метода philosophize
, которое было дано в трейте
Philosophical
, при вызове будет получено новое поведение:
val phrog: Philosophical = new Frog phrog.philosophize // Мне живется нелегко, потому что я зеленая!
Теперь можно прийти к философскому умозаключению, что трейты подобны
Javaинтерфейсам со стандартными методами, но фактически их возмож
ности гораздо шире. Так, в трейтах можно объявлять поля и сохранять со
стояние. Фактически в определении трейта можно делать то же самое, что и в определении класса, и синтаксис выглядит почти так же.
Ключевое отличие классов от трейтов заключается в том, что в классах вызовы super имеют статическую привязку, а в трейтах — динамическую.
Если в классе воспользоваться кодом super.toString
, то вы будете точно знать, какая именно реализация метода будет вызвана. Но когда точно такой
232 Глава 11 • Трейты же код применяется в трейте, то вызываемая с помощью super реализация метода при определении трейта еще не определена. Вызываемая реализация станет определяться заново при каждом примешивании трейта в конкретный класс. Такое своеобразное поведение super является ключевым фактором, позволяющим трейтам работать в качестве наращиваемых модификаций, и рассматривается в разделе 11.3. А правила разрешения вызовов super будут изложены в разделе 11.4.
11 .2 . Сравнение «тонких» и «толстых» интерфейсов
Чаще всего трейты используются для автоматического добавления к классу методов в дополнение к тем методам, которые в нем уже имеются. То есть трейты способны расширить «тонкий» интерфейс, превратив его в «тол-
стый».
Противопоставление «тонких» интерфейсов «толстым» представляет собой компромисс, который довольно часто встречается в объектноориентирован
ном проектировании. Это компромисс между теми, кто реализует интерфейс, и теми, кто им пользуется. В «толстых» интерфейсах имеется множество методов, обеспечивающих удобство применения для тех, кто их вызывает.
Клиенты могут выбрать метод, целиком отвечающий их функциональным запросам. В то же время «тонкий» интерфейс имеет незначительное количе
ство методов и поэтому проще обходится тем, кто их реализует. Но клиентам, обращающимся к «тонким» интерфейсам, приходится создавать больше собственного кода. При более скудном выборе доступных для вызова мето
дов им приходится выбирать то, что хотя бы в какойто мере отвечает их по
требностям, а чтобы использовать выбранный метод, им требуется создавать дополнительный код.
Добавление в трейт конкретного метода уводит компромисс «тонкий — тол
стый» в сторону более «толстых» интерфейсов — это одноразовое действие.
Вам нужно единожды реализовать конкретный метод, сделав это в самом трейте, вместо того чтобы возиться с его повторной реализацией для каждого класса, в который примешивается трейт. Таким образом, создание «толстых» интерфейсов в Scala требует меньше работы, чем в языках без трейтов.
Чтобы расширить интерфейс с помощью трейтов, просто определите трейт с небольшим количеством абстрактных методов — «тонкую» часть интерфей
са трейта — и с потенциально большим количеством конкретных методов, реализованных в терминах абстрактных методов. Затем можно будет при
11 .2 . Сравнение «тонких» и «толстых» интерфейсов
1 ... 21 22 23 24 25 26 27 28 ... 64
233
мешать расширяющий трейт в класс, реализовав «тонкую» часть интерфейса, и получить в результате класс, позволяющий обеспечить доступ ко всему доступному «толстому» интерфейсу.
Хорошим примером области, в которой «толстый» интерфейс удобен, яв
ляется сравнение.
Сравнивая два упорядочиваемых объекта, было бы рационально воспользо
ваться вызовом одного метода, чтобы выяснить результаты желаемого срав
нения. Если нужно использовать сравнение «меньше», то предпочтительнее было бы вызвать
<
, а если требуется применить сравнение «меньше или равно» — вызвать
<=
. С «тонким» интерфейсом можно располагать только методом
<
, и тогда временами приходилось бы создавать код наподобие
(x
<
y)
||
(x
==
y)
. «Толстый» интерфейс предоставит вам все привычные операторы сравнения, позволяя напрямую создавать код вроде x
<=
y
Прежде чем посмотреть на трейт
Ordered
, представим, что можно сделать без него. Предположим, вы взяли класс
Rational из главы 6 и добавили к нему операции сравнения. У вас должен получиться примерно такой код
1
:
class Rational(n: Int, d: Int):
// ...
def < (that: Rational) =
this.numer * that.denom < that.numer * this.denom def > (that: Rational) = that < this def <= (that: Rational) = (this < that) || (this == that)
def >= (that: Rational) = (this > that) || (this == that)
В данном классе определяются четыре оператора сравнения (
<
,
>
,
<=
и
>=
), и это классическая демонстрация стоимости определения «толстого» ин
терфейса. Сначала обратите внимание на то, что три оператора сравнения определены на основе первого оператора. Например, оператор
>
определен как противоположность оператора
<
, а оператор
<=
определен буквально как «меньше или равно». Затем обратите внимание на то, что все три этих метода будут такими же для любого другого класса, объекты которого могут сравниваться друг с другом. В отношении оператора
<=
для рациональных чисел не прослеживается никаких особенностей. В контексте сравнения оператор
<=
всегда используется для обозначения «меньше или равно».
В общем, в этом классе есть довольно много шаблонного кода, который будет точно таким же в любом другом классе, реализующем операции сравнения.
1
Этот пример основан на использовании класса
Rational
, показанного в листин
ге 6.5, с методами equals
, hashCode и модификациями, гарантирующими положи
тельный denom
234 Глава 11 • Трейты
Данная проблема встречается настолько часто, что в Scala предоставляет
ся трейт, помогающий справиться с ее решением. Этот трейт называется
Ordered
. Чтобы воспользоваться им, нужно заменить все отдельные методы сравнений одним методом compare
. Затем на основе одного этого метода определить в трейте
Ordered методы
<
,
>
,
<=
и
>=
. Таким образом, трейт
Ordered позволит вам расширить класс методами сравнений с помощью реализации всего одного метода по имени compare
Если определить операции сравнения в
Rational путем использования трей
та
Ordered
, то код будет иметь следующий вид:
class Rational(n: Int, d: Int) extends Ordered[Rational]:
// ...
def compare(that: Rational) =
(this.numer * that.denom) - (that.numer * this.denom)
Нужно выполнить две задачи. Начнем с того, что в
Rational примешивается трейт
Ordered
. В отличие от трейтов, которые встречались до сих пор,
Ordered требует от вас при примешивании указать параметр типа. Параметры ти
пов до главы 18 подробно рассматриваться не будут, а пока все, что нужно знать при примешивании
Ordered
, сводится к следующему: фактически следует выполнять примешивание
Ordered[C]
, где
C
обозначает класс, эле
менты которого сравниваются. В данном случае в
Rational примешивается
Ordered[Rational]
Вторая задача, требующая выполнения, заключается в определении метода compare для сравнения двух объектов. Этот метод должен сравнивать полу
чатель this с объектом, переданным методу в качестве аргумента. Возвра
щать он должен целочисленное значение, которое равно нулю, если объекты одинаковы, отрицательное число, если получатель меньше аргумента, и по
ложительное, если получатель больше аргумента.
В данном случае метод сравнения класса
Rational использует формулу, основанную на приведении чисел к общему знаменателю с последующим вычитанием получившихся числителей. Теперь при наличии этого при
мешивания и определения метода compare класс
Rational имеет все четыре метода сравнения:
val half = new Rational(1, 2)
val third = new Rational(1, 3)
half < third // false half > third // true
При каждой реализации класса с какойлибо возможностью упорядочен
ности путем сравнения нужно рассматривать вариант примешивания в него
11 .3 . Трейты как наращиваемые модификации 235
трейта
Ordered
. Если выполнить это примешивание, то пользователи класса получат богатый набор методов сравнений.
Имейте в виду, что трейт
Ordered не определяет за вас метод equals
, посколь
ку не способен на это. Дело в том, что реализация equals в терминах compare требует проверки типа переданного объекта, а изза удаления типов сам
Ordered не может ее выполнить. Поэтому определять equals вам придется самим, даже если вы примешиваете трейт
Ordered
11 .3 . Трейты как наращиваемые модификации
Основное применение трейтов — превращение «тонкого» интерфейса в «тол
стый» — вы уже видели. Перейдем теперь ко второму по значимости способу использования трейтов — предоставлению классам наращиваемых модифи
каций. Трейты дают возможность изменять методы класса, позволяя вам
наращивать их друг на друге.
Рассмотрим в качестве примера наращиваемые модификации применитель
но к очереди целых чисел. В очереди будут две операции: put
, помещающая целые числа в очередь, и get
, извлекающая их из очереди. Очереди работают по принципу «первым пришел, первым ушел», поэтому целые числа извле
каются из очереди в том же порядке, в котором были туда помещены.
Располагая классом, реализующим такую очередь, можно определить трейты для выполнения следующих модификаций:
z z
удваивания — удваиваются все целые числа, помещенные в очередь;
z z
увеличения на единицу — увеличиваются все целые числа, помещенные в очередь;
z z
фильтрации — из очереди отфильтровываются все отрицательные целые числа.
Эти три трейта представляют модификации, поскольку модифицируют по
ведение соответствующего класса очереди, а не определяют полный класс очереди. Все три трейта также являются наращиваемыми. Можно выбрать любые из трех трейтов, примешать их в класс и получить новый класс, об
ладающий всеми выбранными модификациями.
В листинге 11.5 показан абстрактный класс
IntQueue
. В
IntQueue имеются метод put
, добавляющий к очереди новые целые числа, и метод get
, который