Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 780
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
422 Глава 20 • Абстрактные члены
Конкретная реализация трейта
Abstract нуждается в заполнении определе
ний для каждого из его абстрактных членов. Пример реализации, предостав
ляющий эти определения, выглядит так:
class Concrete extends Abstract:
type T = String def transform(x: String) = x + x val initial = "hi"
var current = initial
Реализация придает конкретное значение типу
T
, определяя его в качестве псевдонима типа
String
. Операция transform конкатенирует предоставлен
ную ей строку с нею же самой, а для initial
, как и для current
, устанавли
вается значение "hi"
Указанные примеры дают вам первое приблизительное представление о разновидностях абстрактных членов, существующих в Scala. Далее мы рассмотрим подробности, касающиеся этих членов, и объясним, для чего могут пригодиться эти новые формы абстрактных членов, а также члены
типы в целом.
20 .2 . Члены-типы
В примере, приведенном в предыдущем разделе, было показано, что по
нятие «абстракный тип» в Scala означает объявление типа (с ключевым словом type
) в качестве члена класса или трейта, без указания определе
ния. Абстрактными могут быть и сами классы, а трейты по определению абстрактные, однако ни один из них не является в Scala тем, что называют
абстрактным типом. Абстрактный тип в Scala всегда выступает членом какоголибо класса или трейта, как тип
T
в трейте
Abstract
Неабстрактный (или конкретный) члентип, такой как тип
T
в классе
Concrete
, можно представить себе в качестве способа определения нового имени, или псевдонима, для типа. К примеру, в классе
Concrete типу
String дается псевдоним
T
. В результате везде, где в определении класса
Concrete появляется
T
, подразумевается
String
. Сюда включаются преобразования типов параметров и результирующих типов, как исходных, так и текущих, в которых при их объявлении в супертрейте
Abstract упоминается
T
. Сле
довательно, когда в классе
Concrete реализуются эти методы, такие обозна
чения
T
интерпретируются как
String
Один из поводов использовать члентип — определение краткого описатель
ного псевдонима для типа, чье имя длиннее или значение менее понятно,
20 .3 . Абстрактные val-переменные 423
чем у псевдонима. Такие членытипы могут сделать понятнее код класса или трейта. Другое основное применение членовтипов — объявление абстракт
ного типа, который должен быть определен в подклассе. Более подробно этот вариант использования, продемонстрированный в предыдущем разделе, мы рассмотрим чуть позже в данной главе.
20 .3 . Абстрактные val-переменные
Объявление абстрактной val
переменной выглядит следующим образом:
val initial: String
Val
переменной даются имя и тип, но не указывается значение. Оно должно быть предоставлено конкретным определением val
переменной в подклассе.
Например, в классе
Concrete для реализации val
переменной используется такой код:
val initial = "hi"
Объявление в классе абстрактной val
переменной применяется, когда в этом классе еще неведомо нужное ей значение, но известно, что переменная в каж
дом экземпляре класса получит неизменяемое значение.
Объявление абстрактной val
переменной напоминает объявление абстракт
ного метода без параметров:
def initial: String
Клиентский код будет ссылаться как на val
переменную, так и на метод абсолютно одинаково (то есть obj.initial
). Но если initial является аб
страктной val
переменной, то клиенту гарантируется, что obj.initial будет при каждом обращении выдавать одно и то же значение. Если initial
— абстрактный метод, то данная гарантия соблюдаться не будет, поскольку в таком случае метод initial можно реализовать с помощью конкретного метода, возвращающего при каждом своем вызове разные значения.
Иными словами, val
переменная ограничивает свою допустимую реализа
цию: любая реализация должна быть определением val
переменной — она не может быть var
или def
определением. А вот объявления абстрактных методов можно реализовать как конкретными определениями методов, так и конкретными определениями var
переменных. Если взять абстрактный класс
Fruit
, показанный в листинге 20.1, то класс
Apple будет допустимой реализацией подкласса, а класс
BadApple
— нет.
424 Глава 20 • Абстрактные члены
Листинг 20.1. Переопределение абстрактных val-переменных и методов без параметров abstract class Fruit:
val v: String // `v' — значение (value)
def m: String // `m' — метод (method)
abstract class Apple extends Fruit:
val v: String val m: String // нормально воспринимаемое переопределение 'def'
// в 'val'
abstract class BadApple extends Fruit:
def v: String // ОШИБКА: переопределять 'val' в 'def' нельзя def m: String
20 .4 . Абстрактные var-переменные
Как и для абстрактной val
переменной, для абстрактной var
переменной объявляются только имя и тип, но не начальное значение. Например, в ли
стинге 20.2 показан трейт
AbstractTime
, в котором объявляются две абстракт
ные переменные с именами hour и minute
Листинг 20.2. Объявление абстрактных var-переменных trait AbstractTime:
var hour: Int var minute: Int
Что означают такие абстрактные var
переменные, как hour и minute
? В раз
деле 16.2 было показано, что var
переменные, объявленные в качестве членов класса, оснащаются геттером и сеттером. Это справедливо и для абстрактных var
переменных. Если, к примеру, объявляется абстрактная var
переменная по имени hour
, то подразумевается, что для нее объявляется абстрактный геттер hour и абстрактный сеттер hour_=
. Тем самым не определяется никакое переназначаемое поле, а конкретная реализация абстрактной var
переменной будет выполнена в подклассах. Например, определение
AbstractTime
, по
казанное выше, в листинге 20.2, абсолютно эквивалентно определению, показанному в листинге 20.3.
Листинг 20.3. Расширение абстрактных var-переменных в геттеры и сеттеры trait AbstractTime:
def hour: Int // get-метод для 'hour'
def hour_=(x: Int): Unit // set-метод для 'hour'
def minute: Int // get-метод для 'minute'
def minute_=(x: Int) : Unit // set-метод для 'minute'
20 .5 . Инициализация абстрактных val-переменных 425
20 .5 . Инициализация абстрактных val-переменных
Иногда абстрактные val
переменные играют роль, аналогичную роли пара
метров суперкласса: они позволяют предоставить в подклассе подробности, не указанные в суперклассе. Рассмотрим в качестве примера переформу
лировку класса
Rational из главы 6, который был показан в листинге 6.5, в трейт:
trait RationalTrait:
val numerArg: Int val denomArg: Int
У класса
Rational из главы 6 были два параметра: n
для числителя рацио
нального числа и d
для его знаменателя. Представленный здесь трейт
Ra- tio nalTrait определяет вместо них две абстрактные val
переменные: numerArg и denomArg
. Чтобы создать конкретный экземпляр этого трейта, нужно реализовать определения абстрактных val
переменных, например:
new RationalTrait:
val numerArg = 1
val denomArg = 2
Здесь появляется ключевое слово new перед
RationalTrait
, после которого стоит двоеточие и отступ от тела класса. Это выражение выдает экземпляр
анонимного класса, примешивающего трейт и определяемого телом. Создание экземпляра данного анонимного класса дает эффект, аналогичный созданию экземпляра с помощью кода new
Rational(1,
2)
Но аналогия здесь неполная. Есть небольшое различие, касающееся порядка, в котором инициализируются выражения. При записи следующего кода:
new Rational(expr1, expr2)
два выражения, expr1
и expr2
, вычисляются перед инициализацией класса
Rational
, следовательно, значения expr1
и expr2
доступны для инициализа
ции класса
Rational
С трейтами складывается обратная ситуация. При записи кода new RationalTrait:
val numerArg = expr1
val denomArg = expr2
выражения expr1
и expr2
вычисляются как часть инициализации аноним
ного класса, но анонимный класс инициализируется после
RationalTrait
426 Глава 20 • Абстрактные члены
Следовательно, значения numerArg и denomArg в ходе инициализации
RationalTrait недоступны (точнее говоря, выбор любого значения даст значение по умолчанию для типа
Int
, то есть ноль). Для представленного ранее определения
RationalTrait это не проблема, поскольку при инициа
лизации трейта значения numerArg или denomArg не используются. Но про
блема возникает в варианте
RationalTrait
, показанном в листинге 20.4, где определяются нормализованные числитель и знаменатель.
Листинг 20.4. Трейт, использующий абстрактные val-переменные trait RationalTrait:
val numerArg: Int val denomArg: Int require(denomArg != 0)
private val g = gcd(numerArg, denomArg)
val numer = numerArg / g val denom = denomArg / g private def gcd(a: Int, b: Int): Int =
if (b == 0) a else gcd(b, a % b)
override def toString = s"$numer/$denom"
При попытке создать экземпляр этого трейта с какимилибо выражениями для числителя и знаменателя, не являющимися простыми литералами, вы
дается исключение:
scala> val x = 2
val x: Int = 2
scala> new RationalTrait:
val numerArg = 1 * x val denomArg = 2 * x java.lang.IllegalArgumentException: requirement failed at scala.Predef$.require(Predef.scala:280)
at RationalTrait.$init$(
... 28 elided
Исключение в этом примере было сгенерировано потому, что при инициа
лизации класса
RationalTrait у denomArg сохранилось исходное нулевое значение, изза чего вызов require завершился сбоем.
В данном примере показано, что порядок инициализации для параметров класса и абстрактных полей разный. Аргумент параметра класса вычисляется
до его передачи конструктору класса (если только это не параметр, передава
емый по имени). А вот реализация определения val
переменной, которая на
ходится в подклассе, вычисляется только после инициализации суперкласса.
Теперь вы понимаете, почему поведение абстрактных val
переменных отличается от поведения параметров, и было бы неплохо узнать, что
20 .5 . Инициализация абстрактных val-переменных 427
с этим делать. Получится ли определить
RationalTrait
, который можно надежно инициализировать, не опасаясь, что возникнут ошибки изза не
инициализированных полей? В действительности в Scala предлагаются два альтернативных решения этой проблемы: параметрические поля трейтов и ленивые
val
-переменные. Эти решения рассматриваются в остальной части раздела.
Параметрические поля трейтов
Первое решение — параметрическиеполя трейтов — позволяет вычислять значения для полей трейтов до того, как сам трейт будет инициализирован.
Для этого определите поля как параметрические. Пример приведен в ли
стингах 20.5 и 20.6.
1 ... 40 41 42 43 44 45 46 47 ... 64
Листинг 20.5. Трейт, принимающий параметрические поля trait RationalTrait(val numerArg: Int, val denomArg: Int):
require(denomArg != 0)
private val g = gcd(numerArg, denomArg)
val numer = numerArg / g val denom = denomArg / g private def gcd(a: Int, b: Int): Int =
if (b == 0) a else gcd(b, a % b)
override def toString = s"$numer/$denom"
Листинг 20.6. Параметрические поля трейта в выражении анонимного класса scala> new RationalTrait(1 * x, 2 * x) {}
val res1: RationalTrait = 1/2
Сфера применения параметрических полейне ограничивается анонимными классами, они могут использоваться также в объектах или именованных под
классах. Соответствующие примеры показаны в листингах 20.7 и 20.8. Класс
RationalClass
, показанный в листинге 20.8, иллюстрирует общую схему до
ступности параметров класса для инициализации супертрейта.
Листинг 20.7. Параметрические поля трейта в определении объекта object TwoThirds extends RationalTrait(2, 3)
Листинг 20.8. Параметрические поля трейта в определении класса class RationalClass(n: Int, d: Int) extends RationalTrait(n, d):
def + (that: RationalClass) = new RationalClass(
numer * that.denom + that.numer * denom,
denom * that.denom
)
428 Глава 20 • Абстрактные члены
Ленивые val-переменные
Параметры трейта могут применяться для точной имитации поведения ини
циализации аргументов конструктора класса. Но иногда лучше позволить самой системе разобраться, как и что должно быть проинициализировано.
Добиться этого можно с помощью определения ленивой val
переменной.
Если перед определением val
переменной поставить модификатор lazy
, то выражение инициализации справа будет вычисляться только при первом использовании val
переменной.
Определим, к примеру, объект
Demo с val
переменной:
object Demo:
val x = { println("initializing x"); "done" }
Теперь сначала сошлемся на
Demo
, а затем на
Demo.x
:
scala> Demo initializing x val res0: Demo.type = Demo$@3002e397
scala> Demo.x val res1: String = done
Как видите, на момент использования объекта
Demo его поле x
становится проинициализированным. Инициализация x
составляет часть инициализа
ции
Demo
. Но ситуация изменится, если определить поле x
как lazy
:
object Demo:
lazy val x = { println("initializing x"); "done" }
scala> Demo val res2: Demo.type = Demo$@24e5389c scala> Demo.x initializing x val res3: String = done
Теперь инициализация
Demo не включает инициализацию x
. Она будет отложена до первого использования x
. Это похоже на ситуацию опреде
ления x
в качестве метода без параметров с помощью ключевого слова def
. Но в отличие от def ленивая val
переменная никогда не вычисляется более одного раза. Фактически после первого вычисления ленивой val
переменной результат вычисления сохраняется, чтобы его можно было применить повторно при последующем использовании той же самой val
переменной.
20 .5 . Инициализация абстрактных val-переменных 429
При изучении данного примера создается впечатление, что объекты, подоб
ные
Demo
, сами ведут себя как ленивые val
переменные, поскольку инициа
лизируются по необходимости при их первом использовании. Так и есть.
Действительно, определение объекта может рассматриваться как сокращен
ная запись для определения ленивой val
переменной с анонимным классом, в котором описывается содержимое объекта.
Используя ленивые val
переменные, можно переделать
RationalTrait
, как показано в листинге 20.9. В новом определении трейта все конкретные поля определены как lazy
. Есть еще одно изменение, касающееся предыдущего определения
RationalTrait
, показанного выше, в листинге 20.4. Данное из
менение заключается в том, что условие require было перемещено из тела трейта в инициализатор приватного поля g
, вычисляющий наибольший об
щий делитель для numerArg и denomArg
. После внесения этих изменений при инициализации
LazyRationalTrait делать больше ничего не нужно, посколь
ку весь код инициализации теперь является правосторонней частью ленивой val
переменной. Таким образом, теперь вполне безопасно инициализировать абстрактные поля
LazyRationalTrait после того, как класс уже определен.
Давайте рассмотрим пример:
scala> val x = 2
val x: Int = 2
Листинг 20.9. Инициализация трейта с ленивыми val-переменными trait LazyRationalTrait:
val numerArg: Int val denomArg: Int lazy val numer = numerArg / g lazy val denom = denomArg / g override def toString = s"$numer/$denom"
private lazy val g =
require(denomArg != 0)
gcd(numerArg, denomArg)
private def gcd(a: Int, b: Int): Int =
if b == 0 then a else gcd(b, a % b)
Рассмотрим пример:
scala> new LazyRationalTrait:
val numerArg = 1 * x val denomArg = 2 * x val res4: LazyRationalTrait = 1/2
430 Глава 20 • Абстрактные члены
Здесь не нужны какиелибо предварительные вычисления. Проследим по
следовательность инициализации, приводящей к тому, что в показанном ранее коде на стандартное устройство будет выведена строка
1/2 1. Создается новый экземпляр
LazyRationalTrait
, и запускается код инициа лизации
LazyRationalTrait
. Этот код пуст — ни одно из полей
LazyRa tionalTrait еще не проинициализировано.
2. С помощью вычисления выражения new определяется первичный кон
структор анонимного подкласса. Данная процедура включает в себя ини
циализацию numerArg значением
2
и инициализацию denomArg значением
4 3. Далее интерпретатор в отношении создаваемого объекта вызывает ме
тод toString
, чтобы получившееся значение можно было бы вывести на стандартное устройство.
4. Метод toString
, определенный в трейте
LazyRationalTrait
, выполняет первое обращение к полю numer
, что вызывает вычисление инициализа
тора.
5. Инициализатор поля numer обращается к приватному полю g
; таким образом, следующим вычисляется g
. При этом вычислении происходит обращение к numerArg и denomArg
, которые были определены в шаге 2.
6. Метод toString обращается к значению denom
, что вызывает вычисление denom
. При этом происходит обращение к значениям denomArg и g
. Ини
циализатор поля g
заново уже не вычисляется, поскольку был вычислен в шаге 5.
7. Создается и выводится строка результата
1/2
Обратите внимание: в классе
LazyRationalTrait определение g
появляется в тексте кода после определений в нем numer и denom
. Несмотря на это, вви
ду того что все три значения ленивые, g
инициализируется до завершения инициализации numer и denom
Тем самым демонстрируется важное свойство ленивых val
переменных: по
рядок следования их определений в тексте кода не играет никакой роли, по
скольку инициализация значений выполняется по требованию. Стало быть, ленивые val
переменные могут освободить вас как программиста от необ
ходимости обдумывать порядок расстановки определений val
переменных, чтобы гарантировать, что к моменту востребованности все будет определено.
Но данное преимущество сохраняется до тех пор, пока инициализация ле
нивых val
переменных не производит никаких побочных эффектов, а также