Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 790
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
210 Глава 10 • Композиция и наследование
Это нормально, поскольку он расширяет класс
Element и в результате тип
VectorElement совместим с типом
Element
1
На рис. 10.1 также показано отношение композиции между
VectorElement и
Vector[String]
. Оно так называется, поскольку класс
VectorElement состо
ит из
Vector[String]
, то есть компилятор Scala помещает в генерируемый им для
VectorElement двоичный класс поле, содержащее ссылку на переданный массив conts
Некоторые моменты, касающиеся композиции и наследования, будут рас
смотрены чуть позже — в разделе 10.11.
10 .5 . Переопределяем методы и поля
Принцип единообразного доступа является одним из тех аспектов, где Scala подходит к полям и методам единообразно — не так, как Java. Еще одно от
личие заключается в том, что в Scala поля и методы принадлежат одному и тому же пространству имен. Этот позволяет полю переопределить метод без параметров. Например, можно, как показано в листинге 10.4, изменить реализацию contents в классе
VectorElement из метода в поле, не модифици
руя определение абстрактного метода contents в классе
Element
Листинг 10.4. Переопределение метода без параметров в поле class VectorElement(conts: Vector[String]) extends Element:
val contents: Vector[String] = conts
Поле contents
(определенное с ключевым словом val
) в этой версии
VectorEle ment
— вполне подходящая реализация метода без параметров contents
(объявленное с ключевым словом def
) в классе
Element
. В то же время в Scala запрещено в одном и том же классе определять поле и метод с одинаковыми именами, а в Java это разрешено.
Например, этот класс в Java пройдет компиляцию вполне успешно:
// Это код Java class CompilesFine {
private int f = 0;
public int f() {
return 1;
}
}
1
Чтобы получить более четкое представление о разнице между подклассом и под
типом, обратитесь к статье глоссария о подтипах.
10 .6 . Определяем параметрические поля 211
Но соответствующий класс в Scala не скомпилируется:
class WontCompile:
private var f = 0 // Не пройдет компиляцию, поскольку поле def f = 1 // и метод имеют одинаковые имена
В принципе, в Scala вместо четырех имеющихся в Java пространств имен для определений имеются только два пространства. Четыре пространства имен в Java — это поля, методы, типы и пакеты. Напротив, двумя пространствами имен в Scala являются:
z z
значения (поля, методы, пакеты и объектыодиночки);
z z
типы (имена классов и трейтов).
Причина, по которой поля и методы в Scala помещаются в одно и то же про
странство имен, заключается в предоставлении возможности переопределить методы без параметров в val
поля, чего нельзя сделать в Java
1 10 .6 . Определяем параметрические поля
Рассмотрим еще раз определение класса
VectorElement
, показанное в преды
дущем разделе. В нем имеется параметр conts
, единственное предназначение которого — его копирование в поле contents
. Имя conts было выбрано для параметра, чтобы походило на имя поля contents
, но не вступало с ним в кон
фликт имен. Это «код с душком» — признак того, что в вашем коде может быть некая совершенно ненужная избыточность и повторяемость.
От этого кода сомнительного качества можно избавиться, скомбинировав параметр и поле в едином определении параметрического поля, что и по
казано в листинге 10.5.
Листинг 10.5. Определение contents в качестве параметрического поля
// Расширенный элемент, показанный в листинге 10.2.
class VectorElement(
val contents: Vector[String]
) extends Element
1
Причиной того, что пакеты в Scala используют общее с полями и методами про
странство имен, является стремление предоставить вам возможность получать доступ к импорту пакетов (а не только к именам типов), а также к полям и мето
дам объектоводиночек. Это тоже входит в перечень того, что невозможно сделать в Java. Подробности будут рассмотрены в разделе 12.3.
212 Глава 10 • Композиция и наследование
Обратите внимание: параметр contents имеет префикс val
. Это сокращенная форма записи, определяющая одновременно параметр и поле с одним и тем же именем. Если выразиться более конкретно, то класс
VectorElement теперь имеет поле contents
(непереназначаемое), доступ к которому может быть получен за пределами класса. Поле инициализировано значением параметра.
Похоже на то, будто бы класс был написан следующим образом:
class VectorElement(x123: Vector[String]) extends Element:
val contents: Vector[String] = x123
где x123
— произвольное имя для параметра.
Кроме того, можно поставить перед параметром класса префикс var
, и тогда соответствующее поле станет переназначаемым. И наконец, подобным пара
метризованным полям, как и любым другим членам класса, можно добавлять такие модификаторы, как private
, protected
1
или override
. Рассмотрим, к примеру, следующие определения классов:
class Cat:
val dangerous = false class Tiger(
override val dangerous: Boolean,
private var age: Int
) extends Cat
Определение класса
Tiger
— сокращенная форма для следующего альтер
нативного определения класса с переопределяемым элементом dangerous и приватным элементом age
:
class Tiger(param1: Boolean, param2: Int) extends Cat:
override val dangerous = param1
private var age = param2
Оба элемента инициализируются соответствующими параметрами. Имена для этих параметров, param1
и param2
, были выбраны произвольно. Главное, чтобы они не конфликтовали с какимилибо другими именами в пространстве имен.
10 .7 . Вызываем конструктор суперкласса
Теперь вы располагаете полноценной системой из двух классов: абстрактного класса
Element
, который расширяется конкретным классом
VectorElement
1
Модификатор protected
, предоставляющий доступ к подклассам, будет подробно рассмотрен в главе 12.
10 .8 . Используем модификатор override
1 ... 19 20 21 22 23 24 25 26 ... 64
213
Можно также наметить иные способы выражения элемента. Например, клиенту может понадобиться создать элемент разметки, содержащий один ряд, задаваемый строкой. Объектноориентированное программирование упрощает расширение системы новыми вариантами данных. Можно просто добавить подклассы. Например, в листинге 10.6 показан класс
LineElement
, расширяющий класс
VectorElement
Листинг 10.6. Вызов конструктора суперкласса
// Расширенный элемент VectorElement, показанный в листинге 10.5.
class LineElement(s: String) extends VectorElement(Vector(s)):
override def width = s.length override def height = 1
Поскольку
LineElement расширяет
VectorElement
, а конструктор
Vec torEle- ment получает параметр (
Vector[String]
), то
LineElement нужно передать аргумент первичному конструктору своего суперкласса. В целях вызова конструктора суперкласса аргумент или аргументы, которые нужно пере
дать, просто помещаются в круглые скобки, стоящие за именем суперклас
са. Например, класс
LineElement передает аргумент
Vector(s)
первичному конструктору класса
VectorElement
, поместив его в круглые скобки и указав после имени суперкласса
VectorElement
:
... extends VectorElement(Vector(s)) ...
С появлением нового подкласса иерархия наследования для элементов раз
метки приобретает вид, показанный на рис. 10.2.
Рис. 10.2. Схема классов для LineElement
10 .8 . Используем модификатор override
Обратите внимание: определения width и height в
LineElement имеют мо
дификатор override
. В разделе 6.3 он встречался в определении метода
214 Глава 10 • Композиция и наследование toString
. В Scala такой модификатор требуется для всех элементов, пере
определяющих конкретный член родительского класса. Если элемент явля
ется реализацией абстрактного элемента с тем же именем, то модификатор указывать не обязательно. Применять модификатор запрещено, если член не переопределяется или не является реализацией какоголибо другого члена базового класса. Поскольку height и width в классе
LineElement переопре
деляют конкретные определения в классе
Element
, то модификатор override указывать обязательно.
Соблюдение этого правила дает полезную информацию для компилятора, которая помогает избежать некоторых трудно отлавливаемых ошибок и сде
лать развитие системы более безопасным. Например, если вы опечатались в названии метода или случайно указали для него не тот список параметров, то компилятор тут же отреагирует, выдав сообщение об ошибке:
$ scalac LineElement.scala
-- [E037] Declaration Error: LineElement.scala:3:15 --
3 | override def hight = 1
| ˆ
| method hight overrides nothing
Соглашение о применении модификатора override приобретает еще большее значение, когда дело доходит до развития системы. Скажем, вы определи
ли библиотеку методов рисования двумерных фигур, сделали ее общедо
ступной, и она стала использоваться довольно широко. Посмотрев на эту библио теку позже, вы захотели добавить к основному классу
Shape новый метод с такой сигнатурой:
def hidden(): Boolean
Новый метод будут использовать различные средства рисования, чтобы определить необходимости отрисовки той или иной фигуры. Тем самым можно существенно ускорить работу, но сделать это, не рискуя вывести из строя клиентский код, невозможно. Кроме всего прочего, у клиента может быть определен подкласс класса
Shape с другой реализацией hidden
. Возмож
но, клиентский метод фактически заставит объектполучатель просто исчез
нуть вместо того, чтобы проверять, не является ли он невидимым. Две версии hidden переопределяют друг друга, поэтому ваши методы рисования в итоге заставят объекты исчезать, что совершенно не соответствует задуманному!
Эти «случайные переопределения» — наиболее распространенное проявле
ние проблемы так называемого хрупкого базового класса. Проблема в том, что, если вы добавите новые члены в базовые классы (которые мы обычно называем суперклассами), вы рискуете вывести из строя клиентский код.
10 .9 . Полиморфизм и динамическое связывание 215
Полностью разрешить проблему хрупкого базового класса в Scala невоз
можно, но по сравнению с Java ситуация несколько лучше
1
. Если библиотека рисования и ее клиентский код созданы в Scala, то у клиентской исходной реализации hidden не мог использоваться модификатор override
, поскольку на момент его применения не могло быть другого метода с таким же именем.
Когда вы добавите метод hidden во вторую версию вашего класса фигур, при повторной компиляции кода клиента будет выдана следующая ошибка:
-- Error: Circle.scala:3:6 ---------------------------
3 | def hidden(): Boolean =
| ˆ
| error overriding method hidden in class Shape
| of type (): Boolean; method hidden of type
| (): Boolean needs `override` modifier
То есть вместо неверного поведения ваш клиент получит ошибку в ходе компиляции, и такой исход обычно более предпочтителен.
10 .9 . Полиморфизм и динамическое связывание
В разделе 10.4 было показано, что переменная типа
Element может ссылаться на объект типа
VectorElement
. Этот феномен называется полиморфизмом, что означает «множество форм». В данном случае объекты типа
Element могут иметь множество форм
2
Ранее вам уже попадались две такие формы:
VectorElement и
LineElement
Можно создать еще больше форм
Element
, определив новые подклассы клас
са
Element
. Например, можно определить новую форму
Element с заданными шириной и высотой (
width и height
) и полностью заполненную заданным символом:
// Расширенный элемент, показанный в листинге 10.2
class UniformElement(
ch: Char,
1
В Java есть аннотация
@Override
, работающая аналогично имеющемуся в Scala модификатору override
, но в отличие от Scalaмодификатора override применять ее не обязательно.
2
Этот вид полиморфизма называется полиморфизмом подтипов. Другие виды полиморфизма в Scala обсуждаются в последующих главах, универсальный по
лиморфизм — в главе 18, а специальный полиморфизм — в главах 21 и 23.
216 Глава 10 • Композиция и наследование override val width: Int,
override val height: Int
) extends Element:
private val line = ch.toString * width def contents = Vector.fill(height)(line)
Иерархия наследования для класса
Element теперь приобретает вид, пока
занный на рис. 10.3. В результате Scala примет все следующие присваивания, поскольку тип выражения присваивания соответствует типу определяемой переменной:
val e1: Element = VectorElement(Vector("hello", "world"))
val ve: VectorElement = LineElement("hello")
val e2: Element = ve val e3: Element = UniformElement('x', 2, 3)
Рис. 10.3. Иерархия классов элементов разметки
Если изучить иерархию наследования, то окажется, что в каждом их этих четырех val
определений тип выражения справа от знака равенства при
надлежит типу val
переменной, инициализируемой слева от знака равен
ства.
Есть еще одна сторона вопроса: вызовы методов с переменными и выраже
ниями динамически связаны. Это значит, текущая реализация вызываемого метода определяется во время выполнения программы на основе класса объекта, а не типа переменной или выражения. Чтобы продемонстрировать данное поведение, мы временно уберем все существующие члены из наших классов
Element и добавим к
Element метод по имени demo
. Затем переопре
делим demo в
VectorElement и
LineElement
, но не в
UniformElement
:
abstract class Element:
def demo = "Element's implementation invoked"
class VectorElement extends Element:
override def demo = "VectorElement's implementation invoked"
10 .10 . Объявляем финальные элементы 217
class LineElement extends VectorElement:
override def demo = "LineElement's implementation invoked"
// UniformElement наследует demo из Element class UniformElement extends Element
Если ввести данный код в REPL, то можно будет определить метод, который получает объект типа
Element и вызывает в отношении него метод demo
:
def invokeDemo(e: Element) = e.demo
Если передать методу invokeDemo объект типа
VectorElement
, то будет по
казано сообщение, свидетельствующее о вызове реализации demo из класса
VectorElement
, даже если типом переменной e
, в отношении которой был вызван метод demo
, являлся
Element
:
invokeDemo(new VectorElement)
// Вызвана реализация, определенная в VectorElement
Аналогично, если передать invokeDemo объект типа
LineElement
, будет пока
зано сообщение, свидетельствующее о вызове той реализации demo
, которая была определена в классе
LineElement
:
invokeDemo(new LineElement)
// Вызвана реализация, определенная в LineElement
Поведение при передаче объекта типа
UniformElement может на первый взгляд показаться неожиданным, но оно вполне корректно:
invokeDemo(new UniformElement)
// Вызвана реализация, определенная в Element
В классе
UniformElement метод demo не переопределяется, поэтому в нем наследуется реализация demo из его суперкласса
Element
. Таким образом, реализация, определенная в классе
Element
, — это правильная реализация вызова demo
, когда классом объекта является
UniformElement
10 .10 . Объявляем финальные элементы
Иногда при проектировании иерархии наследования нужно обеспечить не
возможность переопределения элемента подклассом. В Scala, как и в Java, это делается путем добавления к элементу модификатора final
. Как показано в листинге 10.7, модификатор final можно указать для метода demo класса
VectorElement
218 Глава 10 • Композиция и наследование
Листинг 10.7. Объявление финального метода class VectorElement extends Element:
final override def demo =
"VectorElement's implementation invoked"
При наличии данной версии в классе
VectorElement попытка переопределить demo в его подклассе
LineElement не пройдет компиляцию:
-- Error: LineElement.scala:2:15 ----------------------
2 | override def demo =
| ˆ
|error overriding method demo in class VectorElement
| of type => String; method demo of type => String
| cannot override final member method demo in class
| VectorElement
Порой вы должны быть уверены, что создать подкласс для класса в целом невозможно. Для этого нужно просто сделать весь класс финальным, добавив к объявлению класса модификатор final
. Например, в листинге 10.8 показа
но, как должен быть объявлен финальный класс
VectorElement
Листинг 10.8. Объявление финального класса final class VectorElement extends Element:
override def demo = "VectorElement's implementation invoked"
При наличии данной версии класса
VectorElement любая попытка опреде
лить подкласс не пройдет компиляцию:
-- [E093] Syntax Error: LineElement.scala:1:6 ---------
1 |class LineElement extends VectorElement:
| ˆ
| class LineElement cannot extend final class
| VectorElement
Теперь мы удалим модификаторы final и методы demo и вернемся к прежней реализации семейства классов
Element
. Далее в главе мы сконцентрируемся на завершении создания работоспособной версии библиотеки разметки.
10 .11 . Используем композицию и наследование
Композиция и наследование — два способа определить новый класс в по
нятиях другого, уже существующего класса. Если вы ориентируетесь преимущественно на повторное использование кода, то, как правило,