Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 785
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
200 Глава 9 • Управляющие абстракции
В качестве конкретного примера представим, будто нужно реализовать кон
струкцию утверждения под названием myAssert
1
. Функция myAssert будет получать в качестве ввода функциональное значение и обращаться к флагу, чтобы решить, что делать. Если флаг установлен, то myAssert вызовет пере
данную функцию и проверит, что она возвращает true
. Если сброшен, то myAssert будет молча бездействовать.
Не прибегая к использованию параметров, передаваемых по имени, кон
струкцию myAssert можно создать следующим образом:
var assertionsEnabled = true def myAssert(predicate: () => Boolean) =
if assertionsEnabled && !predicate() then throw new AssertionError
С определением все в порядке, но пользоваться им неудобно:
myAssert(() => 5 > 3)
Конечно, лучше было бы обойтись в функциональном литерале без пустого списка параметров и обозначения
=>
и создать следующий код:
myAssert(5 > 3) // Не будет работать из-за отсутствия () =>
Именно для воплощения задуманного и существуют параметры, передавае
мые по имени. Чтобы создать такой параметр, задавать тип параметра нужно с обозначения
=>
, а не с
()
=>
. Например, можно заменить в myAssert пара
метр predicate параметром, передаваемым по имени, изменив его тип
()
=>
Boolean на
=>
Boolean
. Как это должно выглядеть, показано в листинге 9.5.
Листинг 9.5. Использование параметра, передаваемого по имени def byNameAssert(predicate: => Boolean) =
if assertionsEnabled && !predicate then throw new AssertionError
Теперь в свойстве, по поводу которого нужно высказать утверждение, мож
но избавиться от пустого параметра. В результате этого использование byNameAssert выглядит абсолютно так же, как встроенная управляющая конструкция:
byNameAssert(5 > 3)
1
Здесь используется название myAssert
, а не assert
, поскольку имя assert предостав
ляется самим языком Scala. Соответствующее описание будет дано в разделе 25.1.
9 .5 . Передача параметров по имени 201
Тип «по имени» (byname), в котором отбрасывается пустой список параме
тров
()
, допустимо использовать только в отношении параметров. Никаких bynameпеременных или bynameполей не существует.
Можно, конечно, удивиться, почему нельзя просто написать функцию myAs- sert
, воспользовавшись для ее параметров старым добрым типом
Boolean и создав следующий код:
def boolAssert(predicate: Boolean) =
if assertionsEnabled && !predicate then throw new AssertionError
Разумеется, такая формулировка тоже будет работать и код, использующий эту версию boolAssert
, будет выглядеть точно так же, как и прежде:
boolAs sert(5 > 3)
И все же эти два подхода различаются весьма значительным образом.
Для параметра boolAssert используется тип
Boolean
, и потому выраже
ние внутри круглых скобок в boolAssert(5
>
3)
вычисляется до вызова boolAssert
. Выражение
5
>
3
выдает значение true
, которое передается в boolAssert
. В отличие от этого, поскольку типом параметра predicate функции byNameAssert является
=>
Boolean
, выражение внутри круглых скобок в byNameAssert(5
>
3)
до вызова byNameAssert не вычисляется. Вме
сто этого будет создано функциональное значение, чей метод apply станет вычислять
5
>
3
, и это функциональное значение будет передано функции byNameAssert
Таким образом, разница между двумя подходами состоит в том, что при отключении утверждений вам будут видны любые побочные эффекты, которые могут быть в выражении внутри круглых скобок в boolAssert
, но byNameAssert это не касается. Например, если утверждения отключены, то попытки утверждать, что x
/
0
==
0
, в случае использования boolAssert при
ведут к генерации исключения:
val x = 5
assertionsEnabled = false boolAssert(x / 0 == 0)
java.lang.ArithmeticException: / by zero
... 27 elided
Но попытки утверждать это на основе того же самого кода в случае исполь
зования byNameAssert
не приведут к генерации исключения:
byNameAssert(x / 0 == 0) // Возвращается нормально
202 Глава 9 • Управляющие абстракции
Резюме
В этой главе мы показали, как с помощью богатого инструментария функций
Scala строить управляющие абстракции. Функции внутри вашего кода мож
но применять для избавления от распространенных шаблонов управления, а чтобы повторно задействовать шаблоны управления, часто встречающиеся в вашем программном коде, можно прибегнуть к функциям высшего порядка из библиотеки Scala. Кроме того, мы обсудили приемы использования кар
ринга и параметров, передаваемых по имени, которые позволяют применять в весьма лаконичном синтаксисе собственные функции высшего порядка.
В двух последних главах мы рассмотрели довольно много всего, что относит
ся к функциям. В следующих нескольких главах вернемся к рассмотрению дополнительных объектноориентированных средств языка.
10
Композиция и наследование
В главе 6 мы представили часть основных объектноориентированных аспектов Scala. В текущей продолжим рассматривать эту тему с того места, где остановились в главе 6, и дадим углубленное и гораздо более полное представление об имеющейся в Scala поддержке объектноориентированного программирования.
Нам предстоит сравнить два основных вида взаимоотношений между класса
ми: композицию и наследование. Композиция означает, что один класс содер
жит ссылку на другой и использует класс, на который ссылается, в качестве вспомогательного средства для выполнения своей миссии. Наследование — это отношения «суперкласс/подкласс» («родительский/дочерний класс»).
Помимо этих тем, мы рассмотрим абстрактные классы, методы без параметров, расширение классов, переопределение методов и полей, параметрические поля, вызов конструкторов суперкласса, полиморфизм и динамическое свя
зывание, финальные члены и классы, а также фабричные объекты и методы.
10 .1 . Библиотека двумерной разметки
В качестве рабочего примера в этой главе мы создадим библиотеку для по
строения и вывода на экран двумерных элементов разметки. Каждый элемент будет представлен прямоугольником, заполненным текстом. Для удобства библиотека будет предоставлять фабричные методы по имени elem
, которые создают новые элементы из переданных данных. Например, вы сможете соз
дать элемент разметки, содержащий строку, используя фабричный метод со следующей сигнатурой:
elem(s: String): Element
1 ... 18 19 20 21 22 23 24 25 ... 64
204 Глава 10 • Композиция и наследование
Как видите, элементы будут моделироваться с помощью типа данных по имени
Element
. Чтобы получить новый элемент, объединяющий два элемен
та, вы можете вызвать в отношении элемента операторы above или beside
, передавая им второй элемент. Например, выражение, показанное ниже, создаст более крупный элемент, содержащий два столбца, каждый высотой два элемента:
val column1 = elem("hello") above elem("***")
val column2 = elem("***") above elem("world")
column1 beside column2
Вывод результата этого выражения даст следующий результат:
hello ***
*** world
Элементы разметки — хороший пример системы, в которой объекты могут создаваться из простых частей с помощью операторов композиции. В данной главе будут определены классы, позволяющие создавать объекты элементов из векторов, рядов и прямоугольников. Эти объекты базовых элементов будут простыми деталями. Вдобавок будут определены операторы компо
зиции above и beside
. Такие операторы зачастую называют комбинаторами, поскольку они комбинируют элементы некой области в новые элементы.
Подходить к проектированию библиотеки лучше всего в понятиях комби
наторов: они позволяют осмыслить основные способы конструирования объектов в прикладной области. Что представляют собой простые объекты?
Какими способами из простых объектов могут создаваться более интересные объекты? Как комбинаторы должны сочетаться друг с другом? Что должны представлять собой наиболее общие комбинации? Удовлетворяют ли они всем выдвигаемым правилам? Если у вас есть верные ответы на все эти во
просы, то вы на правильном пути.
10 .2 . Абстрактные классы
Нашей первой задачей будет определить тип
Element
, представляющий эле
менты разметки. Элементы — двумерные прямоугольники из символов, по
этому имеет смысл включить в класс метод по имени contents
, ссылающийся на содержимое элемента разметки. Данный метод может быть представлен в виде векторов строк, где каждая строка обозначает ряд. Отсюда типом воз
вращаемого contents значения должен быть
Vector[String]
. Как он будет выглядеть, показано в листинге 10.1.
10 .3 . Определяем методы без параметров 205
Листинг 10.1. Определение абстрактного метода и класса abstract class Element:
def contents: Vector[String]
В этом классе contents объявляется в качестве метода, у которого нет реа
лизации. Иными словами, метод является абстрактным членом класса
Element
. Класс с абстрактными членами сам по себе должен быть объявлен абстрактным; это можно сделать, указав модификатор abstract перед клю
чевым словом class
:
abstract class Element ...
Модификатор abstract указывает на то, что у класса могут быть абстрактные члены, не имеющие реализации. Поэтому создать экземпляр абстрактного класса невозможно. При попытке сделать это будет получена ошибка ком
пиляции:
scala> new Element
1 |new Element
| ˆˆˆˆˆˆˆ
| Element is abstract; it cannot be instantiated
Чуть позже в этой главе будет показан способ создания подклассов класса
Element
, экземпляры которых можно будет создавать, поскольку они запол
няют отсутствующее определение метода contents
Обратите внимание на то, что у метода contents класса
Element нет моди
фикатора abstract
. Метод абстрактный, если у него нет реализации (то есть знака равенства и тела). В отличие от Java здесь при объявлении методов не нужны (не разрешены) модификаторы abstract
. Методы, имеющие реали
зацию, называются конкретными.
И еще одно различие в терминологии между объявлениями и определениями.
Класс
Element
объявляет абстрактный метод contents
, но пока не определяет никаких конкретных методов. Но в следующем разделе класс
Element будет усилен определением нескольких конкретных методов.
10 .3 . Определяем методы без параметров
В качестве следующего шага к
Element будут добавлены методы, пока
зывающие его ширину (
width
) и высоту (
height
) (листинг 10.2). Метод height возвращает количество рядов в содержимом. Метод width возвра
щает длину первого ряда или, при отсутствии рядов в элементе, ноль. Это
206 Глава 10 • Композиция и наследование значит, что нельзя определить элемент с нулевой высотой и ненулевой шириной.
Листинг 10.2. Определение не имеющих параметров методов width и height abstract class Element:
def contents: Vector[String]
def height: Int = contents.length def width: Int = if height == 0 then 0 else contents(0).length
Обратите внимание: ни в одном из трех методов класса
Element нет списка параметров, даже пустого. Например, вместо def width(): Int метод определен без круглых скобок:
def width: Int
Такие методы без параметров встречаются в Scala довольно часто. В отли
чие от них методы, определенные с пустыми круглыми скобками, например def height():
Int
, называются методами с пустыми скобками. Согласно имеющимся рекомендациям методы без параметров следует использо
вать, когда параметры отсутствуют и метод обращается к изменяемому состоянию только для чтения полей содержащего его объекта (притом он не модифицирует изменяемое состояние). По этому соглашению поддер
живается принцип единообразного доступа [Mey00], который гласит, что на клиентский код не должен влиять способ реализации атрибута в виде поля или метода.
Например, можно реализовать width и height в виде полей, а не методов, просто заменив def в каждом определении на val
:
abstract class Element:
def contents: Vector[String]
val height = contents.length val width = if height == 0 then 0 else contents(0).length
С точки зрения клиента две пары определений абсолютно эквивалентны.
Единственное различие заключается в том, что доступ к полю может осу
ществляться немного быстрее вызова метода, поскольку значения полей предварительно вычислены при инициализации класса, а не вычисляются при каждом вызове метода. В то же время полям в каждом объекте
Element требуется дополнительное пространство памяти. Поэтому вопрос о том, как лучше представить атрибут, в виде поля или в виде метода, зависит от способа использования класса клиентами, который со временем может изме
10 .3 . Определяем методы без параметров 207
ниться. Главное, чтобы на клиенты класса
Element никак не воздействовали изменения, вносимые во внутреннюю реализацию класса.
В частности, клиент класса
Element не должен испытывать необходимости в перезаписи кода, если поле данного класса было переделано в функцию доступа, при условии, что это чистая функция доступа (то есть не имеет никаких побочных эффектов и не зависит от изменяемого состояния). Как бы то ни было, клиент не должен решать какиелибо проблемы.
Пока у нас все получается. Но есть небольшое осложнение, связанное с мето
дами работы Java и Scala 2. Дело в том, что в данных языках нет полноценной реализации принципа единообразного доступа. Например, string.length()
в Java — не то же самое, что string.length
, и даже array.length
— не то же самое, что array.length()
. Это может привести к путанице.
Преодолеть это препятствие языку Scala 3 помогает то, что он весьма мягко относится к смешиванию методов, определенных в Java или Scala 2. В част
ности, можно заменить метод без параметров методом с пустыми круглыми скобками и наоборот, если родительский класс был написан на Java или
Scala 2. Можно также не ставить пустые круглые скобки при вызове любой функции, не получающей аргументов. Например, в Scala 3 одинаково допу
стимо применение двух следующих строк кода:
Array(1, 2, 3).toString
"abc".length
В принципе, в вызовах функций, определенных в Java или Scala 2, можно вообще не ставить пустые круглые скобки. Но их все же рекомендуется использовать, когда вызываемый метод представляет нечто большее, чем свойство своего объектаполучателя. Например, пустые круглые скобки уместны, если метод выполняет вводвывод, записывает переназначаемые переменные (
var
переменные) или считывает var
переменные, не явля
ющиеся полями объектаполучателя, как непосредственно, так и косвенно, используя изменяемые объекты. Таким образом, список параметров служит визуальным признаком того, что вызов инициирует некие примечательные вычисления, например:
"hello".length // нет (), поскольку побочные эффекты отсутствуют println() // лучше () не отбрасывать
Подводя итоги, следует отметить, что в Scala приветствуется определение методов, не получающих параметров и не имеющих побочных эффектов, в виде методов без параметров, то есть без пустых круглых скобок. В то же время никогда не нужно определять метод, имеющий побочные эффекты, без
208 Глава 10 • Композиция и наследование круглых скобок, поскольку вызов этого метода будет выглядеть как выбор поля. Следовательно, ваши клиенты могут быть удивлены, столкнувшись с побочными эффектами.
По аналогии с этим при вызове функции, имеющей побочные эффекты, не забудьте при написании вызова поставить пустые круглые скобки. Иначе говоря, если вызываемая функция выполняет операцию, то используйте круглые скобки, даже если компилятор этого не требует. Но если она про
сто предоставляет доступ к свойству, то круглые скобки следует отбросить.
10 .4 . Расширяем классы
Нам попрежнему нужна возможность создавать объектыэлементы. Вы уже видели, что новый класс
Element не приспособлен для этого в силу своей абстрактности. Поэтому для получения экземпляра элемента необходимо создать подкласс, расширяющий класс
Element и реализующий абстрактный метод contents
. Как это делается, показано в листинге 10.3.
Листинг 10.3. Определение класса ArrayElement в качестве подкласса класса
Element class VectorElement(conts: Vector[String]) extends Element:
def contents: Vector[String] = conts
Класс
ArrayElement определен в целях расширения класса
Element
. Как и в Java, для выражения данного обстоятельства после имени класса указы
вается уточнение extends
:
... extends Element ...
Использование уточнения extends заставляет класс
VectorElement
унасле-
довать у класса
Element все его неприватные элементы и превращает тип
VectorElement в подтип типа
Element
. Поскольку
VectorElement расширяет
Element
, то класс
VectorElement называется подклассом класса
Element
В свою очередь,
Element
— суперкласс
VectorElement
. Если не указать уточ
нение extends
, то компилятор Scala, безусловно, предположит, что ваш класс является расширением класса scala.AnyRef
, который на платформе Java будет соответствовать классу java.lang.Object
. Получается, класс
Element неявно расширяет класс
AnyRef
. Эти отношения наследования показаны на рис. 10.1.
Наследование означает, что все элементы суперкласса являются также эле
ментами подкласса, но с двумя исключениями. Первое: приватные элементы
10 .4 . Расширяем классы 209
Рис. 10.1. Схема классов для VectorElement суперкласса не наследуются подклассом. Второе: элемент суперкласса не на
следуется, если элемент с такими же именем и параметрами уже реализован в подклассе. В таком случае говорится, что элемент подкласса переопреде-
ляет элемент суперкласса. Если элемент в подклассе является конкретным, а в суперклассе абстрактным, также говорится, что конкретный элемент — это реализация абстрактного элемента.
Например, метод contents в
VectorElement переопределяет (или, в ином толковании, реализует) абстрактный метод contents класса
Element
1
. В от
личие от этого класс
VectorElement наследует у класса
Element методы width и height
. Например, располагая
VectorElement
объектом ae
, можно запро
сить его ширину, используя выражение ae.width
, как будто метод width был определен в классе
VectorElement
2
:
val ve = VectorElement(Vector("hello", "world"))
ve.width // 5
Создание подтипов означает, что значение подкласса может быть использо
вано там, где требуется значение суперкласса. Например:
val e: Element = VectorElement(Vector("hello"))
Переменная e
определена как принадлежащая типу
Element
, следовательно, значение, используемое для ее инициализации, также должно быть типа
Element
. А фактически типом этого значения является класс
VectorElement
1
Один из недостатков данной конструкции заключается в том, что мы пока не га
рантируем одинаковой длины массива contents каждого
String
элемента. Задачу можно решить, проверяя соблюдение предварительного условия в первичном конструкторе и генерируя исключения при его нарушении.
2
Как упоминалось в разделе 6.2, при создании экземпляров классов, принимающих параметры, таких как
VectorElement
, вы можете не использовать ключевое слово new