Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 740
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
264 Глава 12 • Пакеты, импорты и экспорты case class PosInt(value: Int):
require(value > 0)
export value.*
С помощью этой конструкции вы можете вызывать любые методы в
PosInt
, которые объявлены непосредственно на
Int
:
val x = PosInt(99)
x + 1 // 100
x — 1 // 98
x / 3 // 33
Директива экспорта создает окончательные методы, называемые экспорт-
ными псевдонимами, для каждой перегруженной формы каждого имени экс
портируемого метода. Например, метод
+
, который принимет значение
Int
, будет иметь такую подпись в
PosInt
:
final def +(x: Int): Int = value + x
Вам доступны все различные формы синтаксиса для импорта с экспортом.
Например, вы можете не использовать операторы символьного сдвига
<<
,
>>
и
>>>
в
PosInt
:
val x = PosInt(24)
x << 1 // 48 (shift left)
x >> 1 // 12 (shift right)
x >>> 1 // 12 (unsigned shift right)
У вас есть возможность переименовывать эти операторы при экспорте так же, как и идентификаторы при импорте, — с помощью as
. Давайте рассмо
трим пример:
case class PosInt(value: Int):
require(value > 0)
export value.{<< as shl, >> as shr, >>> as ushr, *}
С учетом этой директивы экспорта операторы сдвига в
PosInt больше не будут иметь символьных имен:
val x = PosInt(24)
x shl 1 // 48
x shr 1 // 12
x ushr 1 // 12
Вы также можете исключить методы из экспорта подстановочных знаков с помощью as
_
. Например, сдвиг вправо (
>>
) и беззнаковый сдвиг вправо
(
>>>
) всегда дают одинаковый результат для целого положительного числа,
Резюме 265
поэтому возможно использование только одного оператора сдвига впра
во — shr
. Этого можно добиться, опустив оператор
>>>
с помощью
>>>
as
_
, например, так:
case class PosInt(value: Int):
require(value > 0)
export value.{<< as shl, >> as shr, >>> as _, *}
Теперь для метода
>>>
не создается никакого псевдонима:
scala> val x = PosInt(39)
val x: PosInt = PosInt(39)
scala> x shr 1
val res0: Int = 19
scala> x >>> 1 1 |x >>> 1
|ˆˆˆˆˆ
|value >>> is not a member of PosInt
Резюме
В данной главе мы показали основные конструкции, предназначенные для разбиения программы на пакеты. Благодаря этому вы имеете простую и по
лезную разновидность модульности и можете работать с весьма большими объемами кода, не допуская взаимного влияния различных его частей.
Существующая в Scala система по духу аналогична пакетированию, ис
пользуемому в Java, но с некоторыми отличиями: в Scala проявляется более последовательный или же более универсальный подход. Вы также видели новую функцию, exports
, которая призвана сделать композицию такой же удобной, как и наследование, для повторного использования кода.
В следующей главе мы переключимся на сопоставление шаблонов.
13
Сопоставление с образцом
Данная глава описывает понятия case-классов и сопоставления с образцом
(pattern matching) — конструкций, способствующих созданию обычных, неинкапсулированных структур данных. Особенно полезны эти две кон
струкции при работе с древовидными рекурсивными данными.
Если вам уже приходилось программировать на функциональном языке, то, возможно, сопоставление с образцом вам уже знакомо. А вот понятие case
классов будет для вас новым. case
классы в Scala позволяют применять сопоставление с образцом к объектам, добавляя к ним лишь ключевое слово case
, не требуя при этом большого объема шаблонного кода.
Эту главу мы начнем с примера case
классов и сопоставления с образцом.
Затем разберем все виды поддерживаемых шаблонов, рассмотрим роль за-
печатанных классов (sealed classes), обсудим перечисления,
Options и пока
жем некоторые неочевидные места в языке, где используется сопоставление с образцом, а также более объемный и приближенный к реальному пример его использования.
13 .1 . Простой пример
Прежде чем вникать во все правила и нюансы сопоставления с образцом, есть смысл рассмотреть простой пример, дающий общее представление. До
пустим, нужно написать библиотеку, которая работает с арифметическими выражениями и, возможно, является частью разрабатываемого предметно
ориентированного языка.
Первым шагом к решению этой задачи будет определение входных данных.
Чтобы ничего не усложнять, сконцентрируемся на арифметических выра
13 .1 . Простой пример 267
жениях, состоящих из переменных, чисел и унарных и бинарных операций.
Все это выражается иерархией классов Scala, показанной в листинге 13.1.
Листинг 13.1. Определение case-классов trait Expr case class Var(name: String) extends Expr case class Num(number: Double) extends Expr case class UnOp(operator: String, arg: Expr) extends Expr case class BinOp(operator: String,
left: Expr, right: Expr) extends Expr
Эта иерархия включает трейт
Expr с четырьмя подклассами, по одному для каждого вида рассматриваемых выражений. Тела всех пяти классов пусты. case-классы
Еще одна особенность объявлений в листинге 13.1 (см. выше), которая за
служивает внимания, — наличие у каждого подкласса модификатора case
Классы с таким модификатором называются case-классами. Как упоминалось в разделе 4.4, использование модификатора case заставляет компилятор
Scala добавлять к вашему классу некоторые синтаксические удобства. Первое заключается в том, что к классу добавляется фабричный метод с именем дан
ного класса. Это означает, к примеру, что для создания var
объекта можно применить код
Var("x")
:
val v = Var("x")
Особенно полезны фабричные методы благодаря их вложенности. Теперь код не загроможден ключевыми словами new
, и структуру выражения можно воспринять с одного взгляда:
val op = BinOp("+", Num(1), v)
Второе синтаксическое удобство заключается в том, что все аргументы в спи
ске параметров case
класса автоматически получают префикс val
, то есть сохраняются в качестве полей:
v.name // x op.left // Num(1.0)
Третье удобство состоит в том, что компилятор добавляет к вашему классу
«естественную» реализацию методов toString
, hashCode и equals
. Они будут заниматься подготовкой данных к выводу, их хешированием и сравнением всего дерева, состоящего из класса, и (рекурсивно) всех его аргументов.
268 Глава 13 • Сопоставление с образцом
Поскольку метод
==
в Scala всегда передает полномочия методу equals
, это означает, что элементы case
классов всегда сравниваются структурно:
op.toString // BinOp(+,Num(1.0),Var(x))
op.right == Var("x") // true
И наконец, чтобы создать измененные копии, компилятор добавляет к ва
шему классу метод copy
. Он пригодится для создания нового экземпляра класса, аналогичного другому экземпляру, за исключением того, что будет отличаться одним или двумя атрибутами. Метод работает за счет использо
вания именованных параметров и параметров по умолчанию (см. раздел 8.8).
Применение именованных параметров позволяет указать требуемые измене
ния. А для любого неуказанного параметра используется значение из старо
го объекта. Посмотрим в качестве примера, как можно создать операцию, похожую на op во всем, кроме того, что будет изменен параметр operator
:
op.copy(operator = "-") // BinOp(-,Num(1.0),Var(x))
Все эти соглашения в качестве небольшого бонуса придают вашей работе массу удобств. Нужно просто указать модификатор case
, и ваши классы и объекты приобретут гораздо более оснащенный вид. Они станут больше за счет создания дополнительных методов и неявного добавления поля для каждого параметра конструктора. Но самым большим преимуществом case
классов является то, что они поддерживают сопоставления с образцом
1
Сопоставление с образцом
Предположим, нужно упростить арифметические выражения только что представленных видов. Существует множество возможных правил упроще
ния. В качестве иллюстрации подойдут три следующих правила:
UnOp("-", UnOp("-", е)) => е // двойное отрицание
BinOp("+", е, Num(0)) => е // прибавление нуля
BinOp("*", е, Num(1)) => е // умножение на единицу
Как показано в листинге 13.2, чтобы в Scala сформировать ядро функ
ции упрощения с помощью сопоставления с образцом, эти правила можно взять практически в неизменном виде. Показанную в листинге функцию simplifyTop можно использовать следующим образом:
simplifyTop(UnOp("-", UnOp("-", Var("x")))) // Var(x)
1 case
классы поддерживают сопоставление шаблонов путем создания метода из
влечения unapply в объектекомпаньоне.
13 .1 . Простой пример 269
Листинг 13.2. Функция simplifyTop, выполняющая сопоставление с образцом def simplifyTop(expr: Expr): Expr =
expr match case UnOp("-", UnOp("-", e)) => e // двойное отрицание case BinOp("+", e, Num(0)) => e // прибавление нуля case BinOp("*", e, Num(1)) => e // умножение на единицу case _ => expr
Правая часть simplifyTop состоит из выражения match
, которое соответству
ет switch в Java, но записывается после выражения выбора. Иными словами, оно выглядит как
выбор match { альтернативы }
вместо switch (выбор) { альтернативы }
Сопоставление с образцом включает последовательность альтернатив, каждая из которых начинается с ключевого слова case
. Каждая альтернатива состоит из паттерна и одного или нескольких выражений, которые будут вычислены при соответствии паттерну. Обозначение стрелки
=>
отделяет паттерн от выражений.
Выражение match вычисляется проверкой соответствия каждого из пат
тернов в порядке их написания. Выбирается первый же соответствующий паттерн, а также выбирается и выполняется та часть, которая следует за обозначением стрелки.
Паттерн-константа вида
+
или
1
соответствует значениям, равным констан
те в случае применения метода
==
. Паттерн-переменная вида e
соответствует любому значению. Затем переменная ссылается на это же значение в правой части условия case
. Обратите внимание: в данном примере первые три аль
тернативы вычисляются в e
, то есть в переменную, связанную внутри соот
ветствующего паттерна. Подстановочный паттерн (
_
) также соответствует любому значению, но без представления имени переменной для ссылки на это значение. Стоит отметить, что в листинге 13.2 выражение match закан
чивается условием case
, которое применяется при отсутствии соответству
ющих паттернов и не предполагает никаких действий с выражением. Вместо этого получается просто выражение expr
, в отношении которого и выполня
ется сопоставление с образцом.
Паттерн-конструктор выглядит как
UnOp("-",
e)
. Он соответствует всем значениям типа
UnOp
, первый аргумент которых соответствует "-"
, а второй — e
. Обратите внимание: аргументы конструктора сами являются
270 Глава 13 • Сопоставление с образцом паттернами. Это позволяет составлять многоуровневые паттерны, исполь
зуя краткую форму записи.
Примером может послужить следующий паттерн:
UnOp("-", UnOp("-", e))
Представьте попытку реализовать такую же функциональную возможность с помощью шаблона проектирования visitor
1
. Практически так же трудно представить реализацию такой же функциональной возможности в виде длинной последовательности инструкций, проверок соответствия типам и явного приведения типов.
Сравнение match со switch
Выражения match могут быть представлены в качестве общих случаев switch
выражений в стиле Java. В Java switch
выражение может быть вполне есте
ственно представлено в виде match
выражения, где каждый паттерн является константой, а последний паттерн может быть подстановочным (который представлен в switch
выражении вариантом, используемым при отсутствии других соответствий).
И тем не менее следует учитывать три различия. Вопервых, match является
выражением языка Scala, то есть всегда вычисляется в значение. Вовторых, применяемые в Scala выражения альтернатив никогда не «выпадают» в сле
дующий вариант. Втретьих, если не найдено соответствие ни одному из паттернов, то выдается исключение
MatchError
. Следовательно, вам придется всегда обеспечивать охват всех возможных вариантов, даже если это будет означать вариант по умолчанию, в котором не делается ничего.
Листинг 13.3. Сопоставление с образцом с пустым вариантом по умолчанию expr match case BinOp(op, left, right) =>
println(s"$expr является бинарной операцией")
case _ =>
Пример показан в листинге 13.3. Второй необходим, поскольку без него вы
ражение match выдаст исключение
MatchError для любого expr
аргумента,
1
Гамма Э., Хелм Р., Джонсон Р., Влиссидес Дж. Паттерны объектноориентированного проектирования. — СПб.: Питер, 2020.
13 .2 . Разновидности паттернов 271
не являющегося
BinOp
. В данном примере для этого второго варианта не указан никакой код, поэтому при его срабатывании ничего не произойдет.
Результатом в любом случае будет unit
значение
()
, которое также будет результатом вычисления всего выражения match
13 .2 . Разновидности паттернов
В предыдущем примере мы кратко описали некоторые разновидности пат
тернов. А теперь потратим немного времени на более подробное изучение каждого из них.
Синтаксис паттернов довольно прост, поэтому не стоит особо переживать из
за него. Все паттерны выглядят точно так же, как и соответствующие им вы
ражения. Например, если взять иерархию из листинга 13.1, то паттерн
Var(x)
соответствует любому выражению, содержащему переменную, с привязкой x
к имени переменной. Будучи использованным в качестве выражения,
Var(x)
с точно таким же синтаксисом воссоздает эквивалентный объект, предпола
гая, что идентификатор x
уже привязан к имени переменной. В синтаксисе паттернов все прозрачно, поэтому главное, на что следует обратить внима
ние, — это какого вида паттерны можно применять.
Подстановочные паттерны
Подстановочный паттерн (
_
) соответствует абсолютно любому объекту. Вы уже видели, как он используется в качестве общего паттерна, выявляющего все оставшиеся альтернативы:
expr match case BinOp(op, left, right) =>
s"$expr является бинарной операцией"
case _ => // handle the default case s"Это что-то другое"
Кроме того, подстановочные паттерны могут использоваться для игнори
рования тех частей объекта, которые не представляют для вас интереса.
Например, в предыдущем примере нас не интересует, что представляют собой элементы бинарной операции, в нем лишь проверяется, является ли она бинарной. Поэтому, как показано в листинге 13.4, для элементов
BinOp в коде также могут использоваться подстановочные паттерны.
1 ... 25 26 27 28 29 30 31 32 ... 64