Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 783
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
236 Глава 11 • Трейты возвращает целые числа и удаляет их из очереди. Основная реализация
IntQueue
, которая использует
ArrayBuffer
, показана в листинге 11.6.
Листинг 11.5. Абстрактный класс IntQueue abstract class IntQueue:
def get(): Int def put(x: Int): Unit
Листинг 11.6. Реализация класса BasicIntQueue с использованием ArrayBuffer import scala.collection.mutable.ArrayBuffer class BasicIntQueue extends IntQueue:
private val buf = ArrayBuffer.empty[Int]
def get() = buf.remove(0)
def put(x: Int) = buf += x
В классе
BasicIntQueue имеется приватное поле, содержащее буфер в виде массива. Метод get удаляет запись с одного конца буфера, а метод put до
бавляет элементы к другому его концу. Пример использования данной реа
лизации выглядит следующим образом:
val queue = new BasicIntQueue queue.put(10)
queue.put(20)
queue.get() // 10
queue.get() // 20
Пока все вроде бы в порядке. Теперь посмотрим на использование трейтов для модификации данного поведения. В листинге 11.7 показан трейт, кото
рый удваивает целые числа по мере их помещения в очередь. Трейт
Doubling имеет две интересные особенности. Первая заключается в том, что в нем объявляется суперкласс
IntQueue
. Это объявление означает, что трейт может примешиваться только в класс, который также расширяет
IntQueue
. То есть
Doubling можно примешивать в
BasicIntQueue
, но не в
Rational
Листинг 11.7. Трейт наращиваемых модификаций Doubling trait Doubling extends IntQueue:
abstract override def put(x: Int) = super.put(2 * x)
Вторая интересная особенность заключается в том, что у трейта имеется вызов super в отношении метода, объявленного абстрактным. Для обычных классов такие вызовы применять запрещено, поскольку во время выполне
ния они гарантированно дадут сбой. Но для трейта такой вызов может дей
ствительно пройти успешно. В трейте вызовы super динамически связаны, поэтому вызов super в трейте
Doubling будет работать при условии, что трейт
11 .3 . Трейты как наращиваемые модификации 237
примешан после другого трейта или класса, в котором дается конкретное определение метода.
Трейтам, которые реализуют наращиваемые модификации, зачастую нужен именно такой порядок. Чтобы сообщить компилятору, что это делается на
меренно, подобные методы следует помечать модификаторами abstract override
. Это сочетание модификаторов позволительно только для членов трейтов, но не классов и означает, что трейт должен быть примешан в некий класс, имеющий конкретное определение рассматриваемого метода.
Применение трейта выглядит следующим образом:
class MyQueue extends BasicIntQueue, Doubling val queue = new MyQueue queue.put(10)
queue.get() // 20
В первой строке этого примера определяется класс
MyQueue
, котоый рас
ширяет класс
BasicIntQueueand
, примешивая в него трейт
Doubling
. Затем мы создаем новый
MyQueue и помещаем в него число
10
, но в результате при
мешивания трейта
Doubling оно удваивается. При извлечении целого числа из очереди оно уже имеет значение
20
Обратите внимание: в
MyQueue не определяется никакой новый код — про
сто объявляется класс и примешивается трейт. В подобной ситуации вместо определения именованного класса код
BasicIntQueue with
Doubling может быть предоставлен непосредственно с ключевым словом new
. Работа такого кода показана в листинге 11.8 1
Листинг 11.8. Примешивание трейта при создании экземпляра с помощью ключевого слова new val queue = new BasicIntQueue with Doubling queue.put(10)
queue.get() // 20
Чтобы посмотреть, как нарастить модификации, нужно определить еще два модифицирующих трейта:
Incrementing и
Filtering
. Реализация этих трейтов показана в листинге 11.9.
Листинг 11.9. Трейты наращиваемых модификаций Incrementing и Filtering trait Incrementing extends IntQueue:
abstract override def put(x: Int) = super.put(x + 1)
1
Вы должны использовать with
, а не запятые, примешивая трейты в анонимный класс.
238 Глава 11 • Трейты trait Filtering extends IntQueue:
abstract override def put(x: Int) =
if x >= 0 then super.put(x)
Теперь, располагая модифицирующими трейтами, можно выбрать, какой из них вам понадобится для той или иной очереди. Например, ниже показана очередь, в которой не только отфильтровываются отрицательные числа, но и ко всем сохраняемым числам прибавляется единица:
val queue = new BasicIntQueue with Incrementing with Filtering queue.put(-1)
queue.put(0)
queue.put(1)
queue.get() // 1
queue.get() // 2
Порядок примешивания играет существенную роль
1
. Конкретные правила даны в следующем разделе, но, грубо говоря, трейт, находящийся правее, вступает в силу первым. Когда метод вызывается в отношении экземпляра класса с примешанными трейтами, первым вызывается тот метод, который определен в самом правом трейте. Если этот метод выполняет вызов super
, то вызывается метод, который определен в следующем трейте левее данного трейта, и т. д. В предыдущем примере сначала вызывается метод put трейта
Filtering
, следовательно, все начинается с того, что он удаляет отрица
тельные целые числа. Вторым вызывается метод put трейта
Incrementing
, следовательно, к оставшимся целым числам прибавляется единица.
Если расположить трейты в обратном порядке, то сначала к целым числам будет прибавляться единица и только потом те целые числа, которые все же останутся отрицательными, будут удалены:
val queue = new BasicIntQueue with
Filtering with Incrementing queue.put(-1)
queue.put(0)
queue.put(1)
queue.get() // 0
queue.get() // 1
queue.get() // 2
В общем, код, создаваемый в данном стиле, открывает перед вами широкие возможности для проявления гибкости. Примешивая эти три трейта в раз
ных сочетаниях и разном порядке следования, можно определить 16 раз
личных классов. Весьма впечатляющая гибкость для столь незначительного
1
После того как трейт примешан к классу, вы также можете назвать его миксином.
11 .4 . Почему не используется множественное наследование 239
объема кода, поэтому постарайтесь не проглядеть возможности организации кода в целях получения наращиваемых модификаций.
11 .4 . Почему не используется множественное наследование
Трейты позволяют наследовать из множества похожих на классы конструк
ций, но имеют весьма важные отличия от множественного наследования, имеющегося во многих языках программирования.
Одно из отличий, интерпретация super
, играет особенно важную роль. При использовании множественного наследования метод, вызванный с помо
щью вызова super
, может быть определен прямо там, где появляется этот вызов. При использовании трейтов вызываемый метод определяется путем
линеаризации, то есть выстраивания в ряд классов и трейтов, примешанных в класс. Это то самое рассмотренное в предыдущем разделе отличие, которое позволяет выполнять наращивание модификаций.
Прежде чем рассмотреть линеаризацию, немного отвлечемся на то, как наращиваемые модификации выполняются в языке с традиционным мно
жественным наследованием. Представим следующий код, однако на этот раз интерпретируемый не как примешивание трейтов, а как множественное наследование:
// Мысленный эксперимент с множественным наследованием val q = new BasicIntQueue with Incrementing with Doubling q.put(42) // Который из методов put будет вызван?
Сразу возникает вопрос: который из методов put будет задействован в этом вызове? Возможно, вступят в силу правила, согласно которым победу одер
жит самый последний суперкласс. В таком случае будет вызван метод из
Doubling
. В данном методе будет удвоен его аргумент, сделан вызов super.put
, и на этом все. Не произойдет никакого увеличения на единицу! Кроме того, если бы действовало правило, при котором побеждал бы первый суперкласс, то в получающейся очереди целые числа увеличивались бы на единицу, но не удваивались. То есть не срабатывало бы никакое упорядочение.
Можно подумать и о том, как предоставить программистам возможность точно указывать при использовании вызова super
, из какого именно супер
класса им нужен метод. На самом деле вы можете сделать это в Scala, указав суперкласс в квадратных скобках после super
. Вот пример, в котором реали
зации put явно вызываются и для
Incrementing
, и для
Doubling
:
240 Глава 11 • Трейты
// Мысленный эксперимент с множественным наследованием trait MyQueue extends BasicIntQueue,
Incrementing, Doubling:
def put(x: Int) =
super[Incrementing].put(x) // (используется редко,
super[Doubling].put(x) // но допускается в Scala)
Если бы это был единственный подход Scala, то он породил бы новые пробле
мы (самой малой из которых будет многословие). В таком случае получается, что метод put базового класса вызывается дважды: один раз со значением, увеличенным на единицу, и один раз с удвоенным значением, но никогда с увеличенным и удвоенным значением.
Сравнение с методами Java по умолчанию
Начиная с Java 8, вы можете включать методы по умолчанию в интер
фейсы. Хотя они и напоминают конкретные методы в трейтах Scala, но сильно отличаются, потому что Java не выполняет линеаризацию.
Поскольку интерфейс не может указывать сегменты или расширять суперкласс, отличающийся от
Object
, метод по умолчанию может получить доступ к состоянию объекта только путем вызова методов интерфейса, реализованных подклассом. Напротив, конкретные методы в трейте Scala могут получить доступ к состоянию объекта через сегменты, объявленные в трейте, или путем вызова методов с super
, которые получают доступ к сегментам супертрейтов или суперклассов. Кроме того, если вы прописываете класс Java, который наследует методы по умолчанию с одинаковыми подписями из двух разных суперинтерфейсов, Java потребует, чтобы вы самостоятельно реализовали этот метод в классе. Ваше внедрение может вызывать один или оба метода по умолчанию, указывая имя интерфейса перед super
, например "Doubling.super.put(x)"
. Для сравнения: Scala гарантирует, что ваш класс наследует ближайшую реализацию в ли
неаризации.
В Java методы по умолчанию направлены на то, чтобы разработчики библиотек могли добавлять методы к существующим интерфейсам.
До Java 8 это было нецелесообразно, поскольку нарушало бинарную
(двоичную) совместимость любого класса, реализующего интерфейс.
Теперь же Java может использовать реализацию по умолчанию, если класс не предоставляет ее и даже если класс не был перекомпилирован с момента добавления нового метода в интерфейс.
11 .4 . Почему не используется множественное наследование 241
При использовании множественного наследования данная задача просто не имеет правильного решения. Придется опять возвращаться к проектирова
нию и реорганизовывать код. В отличие от этого с решением на основе при
менения трейтов в Scala все предельно понятно. Вы просто примешиваете трейты
Incrementing и
Doubling
, и имеющееся в Scala особое правило, кото
рое касается применения super в трейтах, позволяет добиться всего, чего вы хотели. Нечто здесь очевидно отличается от традиционного множественного наследования, но что именно?
Согласно уже данной подсказке ответом будет линеаризация. Когда с помо
щью ключевого слова new создается экземпляр класса, Scala берет класс со всеми его унаследованными классами и трейтами и располагает их в едином
линейном порядке. Затем при любом вызове super внутри одного из таких классов вызывается тот метод, который идет следующим по порядку. Если во всех методах, кроме последнего, присутствует вызов super
, то получается наращивание.
Описание конкретного порядка линеаризации дается в спецификации языка.
Он сложноват, но вам нужно знать лишь главное: при любой линеаризации класс всегда следует впереди всех своих суперклассов и примешанных трейтов. Таким образом, при написании метода, содержащего вызов super
, этот метод изменяет поведение суперкласса и примешанных трейтов, а не наоборот.
1 ... 22 23 24 25 26 27 28 29 ... 64
ПРИМЕЧАНИЕ
Далее в разделе описываются подробности линеаризации . Вы можете про- пустить этот материал, если сейчас он вам неинтересен .
Главные свойства выполняющейся в Scala линеаризации показаны в следу
ющем примере. Предположим, у вас есть класс
Cat
(Кот) — наследник класса
Animal
(Животное) — и два супертрейта:
Furry
(Пушистый) и
FourLegged
(Четырехлапый). Трейт
FourLegged расширяет еще один трейт —
HasLegs
(С лапами):
class Animal trait Furry extends Animal trait HasLegs extends Animal trait FourLegged extends HasLegs class Cat extends Animal, Furry, FourLegged
Иерархия наследования и линеаризация класса
Cat показаны на рис. 11.1.
Наследование изображено с помощью традиционной нотации UML [Rum04]: стрелки, на концах которых белые треугольники, обозначают наследование,
242 Глава 11 • Трейты
Рис. 11.1. Иерархия наследования и линеаризации класса Cat указывая на супертип. Стрелки с затемненными нетреугольными концами показывают линеаризацию. Они указывают направление, в котором будут разрешаться вызовы super
Линеаризация
Cat вычисляется от конца к началу следующим образом. По
следняя часть линеаризации
Cat
— линеаризация его суперкласса
Animal
. Эта линеаризация копируется без какихлибо изменений. (Линеаризация каждого из этих типов показана в табл. 11.1.) Поскольку класс
Animal не расширяет явным образом какойлибо суперкласс и в него не примешаны никакие су
пертрейты, то по умолчанию он расширяет класс
AnyRef
, который расширяет класс
Any
. Поэтому линеаризация класса
Animal имеет следующий вид:
Animal
→ AnyRef → Any
Предпоследней является линеаризация первой примеси, трейта
Furry
, но все классы, которые уже присутствуют в линеаризации класса
Animal
, теперь не учитываются, поэтому каждый класс в линеаризации
Cat появляется только раз. Результат выглядит следующим образом:
Furry
→ Animal → AnyRef → Any
Всему этому предшествует линеаризация
FourLegged
, в которой также не учитываются любые классы, которые уже были скопированы в линеариза
циях суперкласса или первого примешанного трейта:
FourLegged
→ HasLegs → Furry → Animal → AnyRef → Any
И наконец, первым в линеаризации класса
Cat фигурирует сам этот класс:
Cat
→ FourLegged → HasLegs → Furry → Animal → AnyRef → Any
11 .5 . Параметры трейтов 243
Когда любой из этих классов и трейтов вызывает метод через вызов super
, вызываться будет первая реализация, которая в линеаризации расположена справа от него.
Таблица 11.1. Линеаризация типов в иерархии класса Cat
Тип
Линеаризация
Animal
Animal
,
AnyRef
,
Any
Furry
Furry
,
Animal
,
AnyRef
,
Any
FourLegged
FourLegged
,
HasLegs
,
Animal
,
AnyRef
,
Any
HasLegs
HasLegs
,
Animal
,
AnyRef
,
Any
Cat
Cat
,
FourLegged
,
HasLegs
,
Furry
,
Animal
,
AnyRef
,
Any
11 .5 . Параметры трейтов
Начиная со Scala 3, трейты могут принимать параметры значений. Вы определяете их так же, как и классы: помещая их список через запятую рядом с именем трейта. Например, вы можете передать философское высказывание в качестве параметра трейту
Philosophical
, показанному в листинге 11.10.
Листинг 11.10. Определение параметра трейта trait Philosophical(message: String):
def philosophize = message
Теперь, когда трейт
Philosophical принимает параметр, каждый подкласс должен передать свое собственное философское сообщение в качестве па
раметра для трейта, например, таким образом:
class Frog extends Animal,
Philosophical("Я квакаю, значит, я существую!")
class Duck extends Animal,
Philosophical("Я крякаю, значит, я существую!")
Если сказать кратко, вы должны указать значение параметра трейта при определении класса, который примешивается к трейту. Философия каждого класса
Philosophical теперь будет определяться переданным параметром message
:
244 Глава 11 • Трейты val frog = new Frog frog.philosophize // Я квакаю, значит, я существую!
val duck = new Duck duck.philosophize // Я крякаю, значит, я существую!
Параметры трейта оцениваются непосредственно перед его инициализацией
1
и по умолчанию
2
доступны только его телу. Поэтому, чтобы использовать параметр сообщения в классе, реализующем трейт, необходимо захватить параметр, сделав его доступным из поля. Это поле гарантированно будет инициализировано и доступно для реализующего класса во время инициа
лизации класса.
При использовании параметризованных трейтов вы можете заметить, что правила для параметров трейтов и классов немного отличаются. В обоих случаях вы можете выполнить инициализацию только один раз. Однако, хотя каждый класс может быть расширен только одним подклассом в иерархии, трейт может быть смешан несколькими подклассами. В этом случае вы долж
ны его инициализировать при определении класса, который смешивается с трейтом, находящимся на самом высоком уровне в иерархии. Для примера рассмотрим суперкласс для любого «сознательного» животного, показанный в листинге 11.11.
Листинг 11.11. Указание параметра трейта class ProfoundAnimal extends Animal,
Philosophical("В начале было дело.")
Если суперкласс класса сам по себе не расширяет трейт, вы должны указать параметр трейта при определении класса. Суперклассом
ProfoundAnimal является
Animal
, а
Animal не расширяет
Philosophical
. Поэтому вы должны указать параметр трейта при определении
ProfoundAnimal
С другой стороны, если суперкласс класса также расширяет трейт, то вам больше не нужно указывать параметр трейта при определении класса. Это показано в листинге 11.12.
Листинг 11.12. Без указания параметра трейта class Frog extends ProfoundAnimal, Philosophical
1
Параметры трейта в Scala 3 заменяют ранние инициализаторы в Scala 2.
2
Как и в случае с параметрами класса, вы можете использовать параметрическое поле для определения общедоступного поля, инициализированного переданным параметром трейта. Это будет продемонстрировано в разделе 20.5.