Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 746
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
6 .5 . Добавление полей 131
для этих параметров класса. Но он не может позволить указать that.n или that.d
, поскольку они не ссылаются на объект
Rational
, в отношении кото
рого был вызван метод add
1
. Чтобы получить доступ к числителю и знамена
телю, вам нужно превратить их в поля. В листинге 6.1 показано, как можно добавить эти поля в класс
Rational
2
Листинг 6.1. Класс Rational с полями class Rational(n: Int, d: Int):
require(d != 0)
val numer: Int = n val denom: Int = d override def toString = s"$numer/$denom"
def add(that: Rational): Rational =
Rational(
numer * that.denom + that.numer * denom,
denom * that.denom
)
В версии
Rational
, показанной в листинге, добавлены два поля с именами numer и denom
, которые были проинициализированы значениями параме
тров n
и d
данного класса
3
. Вдобавок были внесены изменения в реализацию методов toString и add
, позволяющие им использовать поля, а не параметры класса. Эта версия класса
Rational проходит компиляцию. Ее можно про
тестировать путем сложения рациональных чисел:
val oneHalf = Rational(1, 2) // 1/2
val twoThirds = Rational(2, 3) // 2/3 oneHalf.add(twoThirds) // 7/6
Теперь вы уже можете сделать то, чего не могли сделать раньше, а именно получить доступ к значениям числителя и знаменателя изза пределов объекта. Для этого нужно просто обратиться к публичным полям numer и denom
:
1
Фактически объект
Rational можно сложить с самим собой, тогда ссылка будет на тот же объект, в отношении которого был вызван метод add
. Но поскольку данному методу можно передать любой объект
Rational
, то компилятор все же не позволит вам воспользоваться кодом that.n
2
В разделе 10.6 вы узнаете о параметрических полях, которые позволяют сделать тот же самый код короче.
3
Несмотря на то что n
и d
используются в теле класса и учитывая, что они применя
ются только внутри конструкторов, компилятор Scala не станет выделять под них поля. Таким образом, получив этот код, компилятор Scala создаст класс с двумя полями типа
Int
: одним для numer
, другим для denom
132 Глава 6 • Функциональные объекты val r = Rational(1, 2) // 1/2
r.numer // 1
r.denom // 2 6 .6 . Собственные ссылки
Ключевое слово this позволяет сослаться на экземпляр объекта, в отноше
нии которого был вызван выполняемый в данный момент метод, или, если оно использовалось в конструкторе, — на создаваемый экземпляр объекта.
Рассмотрим в качестве примера добавление метода lessThan
. Он проверяет, не имеет ли объект
Rational
, в отношении которого он вызван, значение меньше значения параметра:
def lessThan(that: Rational) =
this.numer * that.denom < that.numer * this.denom
Здесь выражение this.numer ссылается на числительное объекта, в отноше
нии которого вызван метод lessThan
. Можно также не указывать префикс this и написать просто numer
, обе записи будут равнозначны.
В качестве примера случаев, когда вам не обойтись без this
, рассмотрим до
бавление к классу
Rational метода max
, возвращающего наибольшее число из заданного рационального числа и переданного аргумента:
def max(that: Rational) =
if this.lessThan(that) then that else this
Здесь первое ключевое слово this избыточно. Можно его не указывать и на
писать lessThan(that)
. Но второе ключевое слово this представляет резуль
тат метода в том случае, если тест вернет false
, и если вы его не укажете, то возвращать будет просто нечего!
6 .7 . Вспомогательные конструкторы
Иногда нужно, чтобы в классе было несколько конструкторов. В Scala все конструкторы, кроме первичного, называются вспомогательными. Например, рациональное число со знаменателем 1 можно кратко записать просто в виде числителя. Вместо 5/1, например, можно просто указать 5. Поэтому было бы неплохо, чтобы вместо записи
Rational(5,
1)
программисты могли напи
сать просто
Rational(5)
. Для этого потребуется добавить к классу
Rational вспомогательный конструктор, который получает только один аргумент — числитель, а в качестве знаменателя имеет предопределенное значение
1
. Как может выглядеть соответствующий код, показано в листинге 6.2.
6 .7 . Вспомогательные конструкторы 133
Листинг 6.2. Класс Rational со вспомогательным конструктором class Rational(n: Int, d: Int):
require(d != 0)
val numer: Int = n val denom: Int = d def this(n: Int) = this(n, 1) // вспомогательный конструктор override def toString = s"$numer/$denom"
def add(that: Rational): Rational =
Rational(
numer * that.denom + that.numer * denom,
denom * that.denom
)
Определения вспомогательных конструкторов в Scala начинаются с def this(...)
. Тело вспомогательного конструктора класса
Rational просто вызывает первичный конструктор, передавая дальше свой единственный аргумент n
в качестве числителя и
1
— в качестве знаменателя. Увидеть вспо
могательный конструктор в действии можно, набрав в REPL следующий код:
val y = Rational(3) // 3/1
В Scala каждый вспомогательный конструктор в качестве первого действия должен вызывать еще один конструктор того же класса. Иными словами, первой инструкции в каждом вспомогательном конструкторе каждого класса
Scala следует иметь вид this(...)
. Вызываемым должен быть либо первич
ный конструктор (как в примере с классом
Rational
), либо другой вспо
могательный конструктор, который появляется в тексте программы перед вызывающим его конструктором. Конечный результат применения данного правила заключается в том, что каждый вызов конструктора в Scala должен в конце концов завершаться вызовом первичного конструктора класса. Пер
вичный конструктор, таким образом, — единственная точка входа в класс.
ПРИМЕЧАНИЕ
Знатоков Java может удивить то, что в Scala правила в отношении конструк- торов более строгие, чем в Java . Ведь в Java первым действием конструктора должен быть либо вызов другого конструктора того же класса, либо вызов конструктора суперкласса напрямую . В классе Scala конструктор суперклас- са может быть вызван только первичным конструктором . Более сильные ограничения в Scala фактически являются компромиссом дизайна — платой за большую лаконичность и простоту конструкторов Scala по сравнению с конструкторами Java . Суперклассы и подробности вызова конструкторов и наследования будут рассмотрены в главе 10 .
134 Глава 6 • Функциональные объекты
6 .8 . Приватные поля и методы
В предыдущей версии класса
Rational мы просто инициализировали numer значением n
, а denom
— значением d
. Изза этого числитель и знаменатель
Rational могут превышать необходимые значения. Например, дробь 66/42 можно сократить и привести к виду 11/7, но первичный конструктор класса
Rational пока этого не делает:
Rational(66, 42) // 66/42
Чтобы выполнить такое сокращение, нужно разделить числитель и знаме
натель на их наибольший общий делитель. Например, таковым для 66 и 42 будет число 6. (Иными словами, 6 — наибольшее целое число, на которое без остатка делится как 66, так и 42.) Деление и числителя, и знаменателя числа 66/42 на 6 приводит к получению сокращенной формы 11/7. Один из способов решения данной задачи показан в листинге 6.3.
В данной версии класса
Rational было добавлено приватное поле g
и из
менены инициализаторы для полей numer и denom
. (Инициализатором на
зывается код, инициализирующий переменную, например n
/
g
, который инициализирует поле numer
.) Поле g
является приватным, поэтому доступ к нему может быть выполнен изнутри, но не снаружи тела класса. Кроме того, был добавлен приватный метод по имени gcd
, вычисляющий наиболь
ший общий делитель двух переданных ему значений
Int
. Например, вызов gcd(12,
8)
дает результат
4
. Как было показано в разделе 4.1, чтобы сделать поле или метод приватным, следует просто поставить перед его определе
нием ключевое слово private
. Назначение приватного «вспомогательного метода» gcd
— обособление кода, необходимого для остальных частей класса, в данном случае для первичного конструктора. Чтобы обеспечить постоянное положительное значение поля g
, методу передаются абсолютные значения параметров n
и d
, которые вызов получает в отношении этих параметров метода abs
. Последний может вызываться в отношении любого
Int
объекта в целях получения его абсолютного значения.
Листинг 6.3. Класс Rational с приватным полем и методом class Rational(n: Int, d: Int):
require(d != 0)
private val g = gcd(n.abs, d.abs)
val numer = n / g val denom = d / g def this(n: Int) = this(n, 1)
6 .9 . Определение операторов 135
def add(that: Rational): Rational =
Rational(
numer * that.denom + that.numer * denom,
denom * that.denom
)
override def toString = s"$numer/$denom"
private def gcd(a: Int, b: Int): Int =
if b == 0 then a else gcd(b, a % b)
Компилятор Scala поместит коды инициализаторов трех полей класса
Rational в первичный конструктор в порядке их следования в исходном коде. Таким образом, инициализатор поля g
, имеющий код gcd(n.abs,
d.abs)
, будет выполнен до выполнения двух других инициализаторов, поскольку в исходном коде появляется первым. Поле g
будет инициализировано резуль
татом — наибольшим общим делителем абсолютных значений параметров n
и d
класса. Затем поле g
будет использовано в инициализаторах полей numer и denom
. Разделив n
и d
на их наибольший общий делитель g
, каждый объект
Rational можно сконструировать в нормализованной форме:
Rational(66, 42) // 11/7 6 .9 . Определение операторов
Текущая реализация класса
Rational нас вполне устраивает, но ее можно сделать гораздо более удобной в использовании. Вы можете спросить, по
чему допустима запись x + y если x
и y
— целые числа или числа с плавающей точкой. Но когда это рацио
нальные числа, приходится пользоваться записью x.add(y)
или в крайнем случае x add y
Такое положение дел ничем не оправдано. Рациональные числа совершенно неотличимы от остальных чисел. В математическом смысле они гораздо бо
лее естественны, чем, скажем, числа с плавающей точкой. Так почему бы не воспользоваться во время работы с ними естественными математическими
136 Глава 6 • Функциональные объекты операторами? И в Scala есть такая возможность. Она будет показана в остав
шейся части главы.
Сначала нужно заменить add обычным математическим символом. Сделать это нетрудно, поскольку знак
+
является в Scala вполне допустимым иден
тификатором. Можно просто определить метод с именем
+
. Если уж на то пошло, то можно определить и метод
*
, выполняющий умножение. Результат показан в листинге 6.4.
Листинг 6.4. Класс Rational с методами-операторами class Rational(n: Int, d: Int):
require(d != 0)
private val g = gcd(n.abs, d.abs)
val numer = n / g val denom = d / g def this(n: Int) = this(n, 1)
def + (that: Rational): Rational =
Rational(
numer * that.denom + that.numer * denom,
denom * that.denom
)
def * (that: Rational): Rational =
Rational(numer * that.numer, denom * that.denom)
override def toString = s"$numer/$denom"
private def gcd(a: Int, b: Int): Int =
if b == 0 then a else gcd(b, a % b)
После такого определения класса
Rational можно будет воспользоваться следующим кодом:
val x = Rational(1, 2) // 1/2
val y = Rational(2, 3) // 2/3
x + y // 7/6
Как всегда, синтаксис оператора в последней строке ввода — эквивалент вы
зова метода. Можно также использовать следующий код:
x.+(y) // 7/6
но читать его будет намного труднее.
6 .10 . Идентификаторы в Scala 137
Следует также заметить, что изза действующих в Scala правил приоритета операторов, рассмотренных в разделе 5.9, метод
*
будет привязан к объ
ектам
Rational сильнее метода
+
. Иными словами, выражения, в которых к объектам
Rational применяются операции
+
и
*
, будут вести себя вполне ожидаемым образом. Например, x
+
x
*
y будет выполняться как x
+
(x
*
y)
, а не как
(x
+
x)
*
y
:
x + x * y // 5/6
(x + x) * y // 2/3
x + (x * y) // 5/6 6 .10 . Идентификаторы в Scala
Вам уже встречались два наиболее важных способа составления идентифи
каторов в Scala — из буквенноцифровых символов и из операторов. В Scala используются весьма гибкие правила формирования идентификаторов.
Кроме двух уже встречавшихся форм, существует еще две. Все четыре формы составления идентификаторов рассматриваются в этом разделе.
Буквенно-цифровые идентификаторы начинаются с буквы или знака под
черкивания, за которыми могут следовать другие буквы, цифры или знаки подчеркивания. Символ
$
также считается буквой, но зарезервирован для идентификаторов, создаваемых компилятором Scala. Идентификаторы в пользовательских программах не должны содержать символы
$
, несмотря на возможность успешно пройти компиляцию: если это произойдет, то мо
гут возникнуть конфликты имен с теми идентификаторами, которые будут созданы компилятором Scala.
В Scala соблюдается соглашение, принятое в Java относительно применения идентификаторов в смешанном регистре
1
, таких как toString и
HashSet
. Хотя использование знаков подчеркивания в идентификаторах вполне допустимо, в программах на Scala они встречаются довольно редко — отчасти в целях со
блюдения совместимости с Java, а также изза того, что знаки подчеркивания в коде Scala активно применяются не только для идентификаторов. Поэтому лучше избегать таких идентификаторов, как, например, to_string
,
__init__
или name_
. Имена полей, параметры методов, имена локальных перемен
ных и имена функций в смешанном регистре должны начинаться с буквы в нижнем регистре, например: length
, flatMap и s
. Имена классов и трейтов
1
Этот стиль именования идентификаторов называется верблюжьим, поскольку у идентификаторов ИмеютсяГорбы, состоящие из символов в верхнем регистре.
1 ... 11 12 13 14 15 16 17 18 ... 64
138 Глава 6 • Функциональные объекты в смешанном регистре должны начинаться с буквы в верхнем регистре, на
пример:
BigInt
,
List и
UnbalancedTreeMap
1
ПРИМЕЧАНИЕ
Одним из последствий использования в идентификаторе замыкающего знака подчеркивания при попытке, к примеру, написания объявления val name_: Int = 1 может стать ошибка компиляции . Компилятор подумает, что вы пытаетесь объявить val-переменную по имени name_: . Чтобы такой идентификатор прошел компиляцию, перед двоеточием нужно поставить дополнительный пробел, как в коде val name_ : Int = 1 .
Один из примеров отступления Scala от соглашений, принятых в Java, касается имен констант. В Scala слово «константа» означает не только val
переменную. Даже притом что val
переменная остается неизменной после инициализации, она не перестает быть переменной. Например, параметры метода относятся к val
переменным, но при каждом вызове метода в этих val
переменных содержатся разные значения. Константа обладает более выраженным постоянством. Например, scala.math.Pi определяется как значение с двойной точностью, наиболее близкое к реальному значению числа
π — отношению длины окружности к ее диаметру. Это значение вряд ли когдалибо изменится, поэтому со всей очевидностью можно сказать, что
Pi
— константа. Константы можно использовать также для присваивания имен значениям, которые иначе были бы в вашем коде магическими числа-
ми — буквальными значениями без объяснений, которые в худшем случае появлялись бы в коде в нескольких местах. Вдобавок может понадобиться определить константы для использования при сопоставлении с образцом
(подобный случай будет рассматриваться в разделе 13.2). В соответствии с соглашением, принятым в Java, константам присваиваются имена, в ко
торых используются символы в верхнем регистре, где знак подчеркивания является разделителем слов, например
MAX_VALUE
или
PI
. В Scala соглашение требует, чтобы в верхнем регистре была только первая буква. Таким образом, константы, названные в стиле Java, например
X_OFFSET
, будут работать в Scala в качестве констант, но в соответствии с соглашением, принятым в Scala, для имен констант применяется смешанный регистр, например
XOffset
Идентификатор оператора состоит из одного или нескольких символов операторов. Таковыми являются выводимые на печать ASCIIсимволы, такие
1
В разделе 14.5 вы увидите, что иногда может возникнуть желание придать классу особый вид, как у case
класса, чье имя состоит только из символов оператора.
Например, в API Scala имеется класс по имени
::
, облегчающий сопоставление с образцом для объектов
List
6 .10 . Идентификаторы в Scala 139
как
+
,
:
,
?
,
или
#
1
. Ниже показаны некоторые примеры идентификаторов операторов:
+ ++ ::: > :–>
Компилятор Scala на внутреннем уровне перерабатывает идентификаторы операторов, чтобы превратить их в допустимые Javaидентификаторы со встроенными символами
$
. Например, идентификатор
:–>
будет представлен как
$colon$minus$greater
. Если вам когдалибо захочется получить доступ к этому идентификатору из кода Java, то потребуется использовать данное внутреннее представление.
Поскольку идентификаторы операторов в Scala могут принимать произ
вольную длину, то между Java и Scala есть небольшая разница в этом вопросе.
В Java введенный код x<–y будет разобран на четыре лексических символа, в результате чего станет эквивалентен x
<
–
y
. В Scala оператор
<–
будет рас
смотрен как единый идентификатор, в результате чего получится x
<–
y
. Если нужно получить первую интерпретацию, то следует отделить символы
<
и
–
друг от друга пробелом. На практике это вряд ли станет проблемой, так как немногие станут писать на Java x<–y
, не вставляя пробелы или круглые скобки между операторами.
Смешанный идентификатор состоит из буквенноцифрового идентифика
тора, за которым стоят знак подчеркивания и идентификатор оператора.
Например, unary_+
, использованный как имя метода, определяет унарный оператор
+
. А myvar_=
, использованный как имя метода, определяет оператор присваивания. Кроме того, смешанный идентификатор вида myvar_=
генери
руется компилятором Scala в целях поддержки свойств (более подробно этот вопрос рассматривается в главе 16).
Литеральный идентификатор представляет собой произвольную строку, заключенную в обратные кавычки (
`...`
). Примеры литеральных иденти
фикаторов выглядят следующим образом:
`x` `` `yield`
Замысел состоит в том, что между обратными кавычками можно поместить любую строку, которую среда выполнения станет воспринимать в качестве
1
Точнее, символ оператора принадлежит к математическим символам (Sm) или прочим символам (So) стандарта Unicode либо к семибитным ASCIIсимволам, не являющимся буквами, цифрами, круглыми, квадратными и фигурными скобками, одинарными или двойными кавычками или знаками подчеркивания, точки, точки с запятой, запятой или обратных кавычек.
140 Глава 6 • Функциональные объекты идентификатора. В результате всегда будет получаться идентификатор
Scala. Это сработает даже в том случае, если имя, заключенное в обратные кавычки, является в Scala зарезервированным словом. Обычно такие иден
тификаторы используются при обращении к статическому методу yield в Javaклассе
Thread
. Вы не можете прибегнуть к коду
Thread.yield()
, по
скольку в Scala yield является зарезервированным словом. Но имя метода все же можно применить, если заключить его в обратные кавычки, например
Thread.`yield`()
6 .11 . Перегрузка методов
Вернемся к классу
Rational
. После внесения последних изменений появи
лась возможность применять операции сложения и умножения рациональ
ных чисел в их естественном виде. Но мы все же упустили из виду смешан
ную арифметику. Например, вы не можете умножить рациональное число на целое, поскольку операнды у оператора
*
всегда должны быть объектами
Rational
. Следовательно, для рационального числа r
вы не можете написать код r
*
2
. Вам нужно написать r
*
Rational(2)
, а это имеет неприглядный вид.
Чтобы сделать класс
Rational еще более удобным в использовании, добавим к нему новые методы, выполняющие смешанное сложение и умножение ра
циональных и целых чисел. А заодно добавим методы вычитания и деления.
Результат показан в листинге 6.5.
Теперь здесь две версии каждого арифметического метода: одна в качестве аргумента получает рациональное число, вторая — целое. Иными словами, все эти методы называются перегруженными, поскольку каждое имя теперь используется несколькими методами. Например, имя
+
применяется и ме
тодом, получающим объект
Rational
, и методом, получающим объект
Int
При вызове метода компилятор выбирает версию перегруженного метода, которая в точности соответствует типу аргументов. Например, если аргу
мент y
в вызове x.+(y)
является объектом
Rational
, то компилятор выберет метод
+
, получающий в качестве параметра объект
Rational
. Но если аргу
мент — целое число, то компилятор выберет метод
+
, получающий в качестве параметра объект
Int
. Если испытать код в действии:
Val r = Rational(2, 3) // 2/3
r * r // 4/9
r * 2 // 4/3
станет понятно, что вызываемый метод
*
определяется каждый раз по типу его правого операнда.
6 .11 . Перегрузка методов 141
ПРИМЕЧАНИЕ
В Scala процесс анализа при выборе перегруженного метода очень похож на аналогичный процесс в Java . В любом случае выбирается перегруженная версия, которая лучше подходит к статическим типам аргументов . Иногда случается, что одной такой версии нет, и тогда компилятор выдаст ошибку, связанную с неоднозначной ссылкой, — ambiguous reference .
Листинг 6.5. Класс Rational с перегруженными методами class Rational(n: Int, d: Int):
require(d != 0)
private val g = gcd(n.abs, d.abs)
val numer = n / g val denom = d / g def this(n: Int) = this(n, 1)
def + (that: Rational): Rational =
Rational(
numer * that.denom + that.numer * denom,
denom * that.denom
)
def + (i: Int): Rational =
Rational(numer + i * denom, denom)
def - (that: Rational): Rational =
Rational(
numer * that.denom - that.numer * denom,
denom * that.denom
)
def - (i: Int): Rational =
Rational(numer — i * denom, denom)
def * (that: Rational): Rational =
Rational(numer * that.numer, denom * that.denom)
def * (i: Int): Rational =
Rational(numer * i, denom)
def / (that: Rational): Rational =
Rational(numer * that.denom, denom * that.numer)
def / (i: Int): Rational =
Rational(numer, denom * i)
override def toString = s"$numer/$denom"
private def gcd(a: Int, b: Int): Int =
if b == 0 then a else gcd(b, a % b)
142 Глава 6 • Функциональные объекты
6 .12 . Методы расширения
Теперь, когда можно воспользоваться кодом r
*
2
, вы также можете захотеть поменять операнды местами, задействовав код
2
*
r
. К сожалению, пока этот код работать не будет:
scala> 2 * r
1 |2 * r
|ˆˆˆ
|None of the overloaded alternatives of method * in
| class Int with types
| (x: Double): Double
| (x: Float): Float
| (x: Long): Long
| (x: Int): Int
| (x: Char): Int
| (x: Short): Int
| (x: Byte): Int
|match arguments ((r : Rational))
Проблема в том, что эквивалент выражения
2
*
r
— выражение
2.*(r)
, то есть вызов метода в отношении числа 2, которое является целым. Но в классе
Int не содержится метода умножения, получающего в качестве аргумента объект
Rational
, его там и не может быть, поскольку он не входит в состав стандартных классов библиотеки Scala.
Но в Scala есть другой способ решения этой проблемы. Вы можете создавать методы расширения для
Int
, которые содержат рациональные числа. Попро
буйте добавить эти строки в REPL:
extension (x: Int)
def + (y: Rational) = Rational(x) + y def - (y: Rational) = Rational(x) - y def * (y: Rational) = Rational(x) * y def / (y: Rational) = Rational(x) / y
Это определяет четыре метода расширения для
Int
, каждый из которых использует
Rational
. Компилятор может использовать их автоматически в ряде ситуаций. С определенными методами расширения теперь вы можете повторить пример, который раньше не удался:
val r = Rational(2,3) // 2/3 2 * r // 4/3
Чтобы метод расширения работал, он должен находиться в области види
мости. Если вы поместите определение метода расширения внутри класса
Резюме 143
Rational
, он не попадет в область действия REPL, поэтому вам необходимо определить его непосредственно в REPL.
Как видно из этого примера, методы расширения — это очень эффективная техника, позволяющая сделать библиотеки более гибкими и удобными в использовании. Однако ее чрезмерное использование может и навредить.
В главе 22 вы узнаете больше о методах расширения, в том числе о способах включения их в область видимости там, где они необходимы.
6 .13 . Предостережение
Создание методов с именами операторов и определение методов расширения, продемонстрированные в этой главе, призваны помочь в проектировании библиотек, для которых код клиента будет лаконичным и понятным. Scala предоставляет широкие возможности для разработки таких весьма доступ
ных для использования библиотек. Но, пожалуйста, имейте в виду: реализуя возможности, не стоит забывать об ответственности.
При неумелом использовании и методыоператоры, и методы расширения могут сделать клиентский код таким, что его станет трудно читать и по
нимать. Выполнение компилятором методов расширения никак внешне не проявляется и не записывается в явном виде в исходный код. Поэтому про
граммистам на клиенте может быть невдомек, что именно оно и применяется в вашем коде. И хотя методыоператоры обычно делают клиентский код более лаконичным и читабельным, таким он становится только для наибо
лее сведущих программистовклиентов, способных запомнить и распознать значение каждого оператора.
При проектировании библиотек всегда нужно стремиться сделать клиент
ский код не просто лаконичным, но и легкочитаемым и понятным. Читабель
ность в значительной степени может быть обусловлена лаконичностью, кото
рая способна заходить очень далеко. Проектируя библиотеки, позволяющие добиваться изысканной лаконичности, и в то же время создавая понятный клиентский код, вы можете существенно повысить продуктивность работы программистов, которые используют эти библиотеки.
Резюме
В этой главе мы рассмотрели многие аспекты классов Scala. Вы увидели способы добавления к классу параметров, определили несколько конструк
144 Глава 6 • Функциональные объекты торов, операторы и методы и настроили классы таким образом, чтобы их применение приобрело более естественный вид. Важнее всего, вероятно, было показать вам, что определение и использование неизменяющихся объ
ектов — вполне естественный способ программирования на Scala.
Хотя показанная здесь финальная версия класса
Rational соответствует всем требованиям, обозначенным в начале главы, ее можно усовершенствовать.
Позже мы вернемся к этому примеру. В частности, в главе 8 будет рассмотре
но переопределение методов equals и hashcode
, которое позволяет объектам
Rational улучшить свое поведение в момент, когда их сравнивают с помощью оператора
==
или помещают в хештаблицы. В главе 22 поговорим о том, как помещать методы расширения в объектыкомпаньоны класса
Rational
, кото
рые упрощают для программистовклиентов помещение в область видимости объектов типа
Rational
7
Встроенные управляющие конструкции
В Scala имеется весьма незначительное количество встроенных управля
ющих конструкций. К ним относятся if
, while
, for
, try
, match и вызовы функций. Их в Scala немного, поскольку с момента создания данного языка в него были включены функциональные литералы. Вместо накопления в базовом синтаксисе одной за другой высокоуровневых управляющих конструкций Scala собирает их в библиотеках. Как именно это делается, мы покажем в главе 9. А здесь рассмотрим имеющиеся в Scala немногочисленные встроенные управляющие конструкции.
Следует учесть, что почти все управляющие конструкции Scala приводят к какомулибо значению. Такой подход принят в функциональных языках, где программы рассматриваются в качестве вычислителей значений, стало быть, компоненты программы тоже должны вычислять значения. Можно рассматривать данное обстоятельство как логическое завершение тенденции, уже присутствующей в императивных языках. В них вызовы функций могут возвращать значение, даже когда наряду с этим будет происходить обнов
ление вызываемой функцией выходной переменной, переданной в качестве аргумента. Кроме того, в императивных языках зачастую имеется тернарный оператор (такой как оператор
?:
в C, C++ и Java), который ведет себя полно
стью аналогично if
, но при этом возвращает значение. Scala позаимствовал эту модель тернарного оператора, но назвал ее if
. Иными словами, исполь
зуемый в Scala оператор if может выдавать значение. Затем эта тенденция в Scala получила развитие: for
, try и match тоже стали выдавать значения.
Программисты могут использовать полученное в результате значение, чтобы упростить свой код, применяя те же приемы, что и для значений, возвращаемых функциями. Не будь этой особенности, программистам пришлось бы созда
вать временные переменные, просто чтобы хранить результаты, вычисленные
146 Глава 7 • Встроенные управляющие конструкции внутри управляющей конструкции. Отказ от таких переменных немного упрощает код, а также избавляет от многих ошибок, возникающих, когда в од
ном ответвлении переменная создается, а в другом о ее создании забывают.
В целом, основные управляющие конструкции Scala в минимальном соста
ве обеспечивают все, что нужно было взять из императивных языков. При этом они позволяют сделать код более лаконичным за счет неизменного на
личия значений, получаемых в результате их применения. Чтобы показать все это в работе, далее более подробно рассмотрим основные управляющие конструкции Scala.
7 .1 . Выражения if
Выражение if в Scala работает практически так же, как во многих других языках. Оно проверяет условие, а затем выполняет одну из двух ветвей кода в зависимости от того, вычисляется ли условие в true
. Простой пример, на
писанный в императивном стиле, выглядит следующим образом:
var filename = "default.txt"
if !args.isEmpty then filename = args(0)
В этом коде объявляется переменная по имени filename
, которая инициа
лизируется значением по умолчанию. Затем в нем используется выражение if с целью проверить, предоставлены ли программе какиелибо аргументы.
Если да, то в переменную вносят изменения, чтобы в ней содержалось зна
чение, указанное в списке аргументов. Если нет, то выражение оставляет значение переменной, установленное по умолчанию.
Этот код можно сделать гораздо более выразительным, поскольку, как упоминалось в шаге 3 главы 2, выражение if в Scala возвращает значение.
В листинге 7.1 показано, как можно выполнить те же самые действия, что и в предыдущем примере, не прибегая к использованию var
переменных.
Листинг 7.1. Особый стиль Scala, применяемый для условной инициализации val filename =
if !args.isEmpty then args(0)
else "default.txt"
На этот раз у if имеются два ответвления. Если массив args непустой, то выбирается его начальный элемент args(0)
. В противном случае выбирается значение по умолчанию. Выражение if выдает результат в виде выбранного
7 .2 . Циклы while 147
значения, которым инициализируется переменная filename
. Данный код немного короче предыдущего. Но гораздо более существенно то, что в нем используется val
, а не var
переменная. Это соответствует функциональному стилю и помогает вам примерно так же, как применение финальной (
final
) переменной в Java. Она сообщает читателям кода, что переменная никогда не изменится, избавляя их от необходимости просматривать весь код в об
ласти видимости переменной, чтобы понять, изменяется ли она гденибудь.
Второе преимущество использования v a r
переменной вместо v a l
переменной заключается в том, что она лучше поддерживает выводы, кото
рые делаются с помощью эквациональных рассуждений (equational reasoning).
Введенная переменная равна вычисляющему выражению при условии, что у него нет побочных эффектов. Таким образом, всякий раз, собираясь напи
сать имя переменной, вы можете вместо него написать выражение. Вместо println(filename)
, к примеру, можно просто написать следующий код:
println(if (!args.isEmpty) args(0) else "default.txt")
Выбор за вами. Вы можете прибегнуть к любому из вариантов. Использова
ние val
переменных помогает совершенно безопасно проводить подобный рефакторинг кода по мере его развития.
Всегда ищите возможности для применения val
переменных. Они смогут упростить не только чтение вашего кода, но и его рефакторинг.
7 .2 . Циклы while
Используемые в Scala циклы while ведут себя точно так же, как и в других языках. В них имеются условие и тело, которое выполняется снова и снова, пока условие вычисляется в true
. Пример показан в листинге 7.2.
1 ... 12 13 14 15 16 17 18 19 ... 64
6 .10 . Идентификаторы в Scala 139
как
+
,
:
,
?
,
или
#
1
. Ниже показаны некоторые примеры идентификаторов операторов:
+ ++ ::: > :–>
Компилятор Scala на внутреннем уровне перерабатывает идентификаторы операторов, чтобы превратить их в допустимые Javaидентификаторы со встроенными символами
$
. Например, идентификатор
:–>
будет представлен как
$colon$minus$greater
. Если вам когдалибо захочется получить доступ к этому идентификатору из кода Java, то потребуется использовать данное внутреннее представление.
Поскольку идентификаторы операторов в Scala могут принимать произ
вольную длину, то между Java и Scala есть небольшая разница в этом вопросе.
В Java введенный код x<–y будет разобран на четыре лексических символа, в результате чего станет эквивалентен x
<
–
y
. В Scala оператор
<–
будет рас
смотрен как единый идентификатор, в результате чего получится x
<–
y
. Если нужно получить первую интерпретацию, то следует отделить символы
<
и
–
друг от друга пробелом. На практике это вряд ли станет проблемой, так как немногие станут писать на Java x<–y
, не вставляя пробелы или круглые скобки между операторами.
Смешанный идентификатор состоит из буквенноцифрового идентифика
тора, за которым стоят знак подчеркивания и идентификатор оператора.
Например, unary_+
, использованный как имя метода, определяет унарный оператор
+
. А myvar_=
, использованный как имя метода, определяет оператор присваивания. Кроме того, смешанный идентификатор вида myvar_=
генери
руется компилятором Scala в целях поддержки свойств (более подробно этот вопрос рассматривается в главе 16).
Литеральный идентификатор представляет собой произвольную строку, заключенную в обратные кавычки (
`...`
). Примеры литеральных иденти
фикаторов выглядят следующим образом:
`x` `
Замысел состоит в том, что между обратными кавычками можно поместить любую строку, которую среда выполнения станет воспринимать в качестве
1
Точнее, символ оператора принадлежит к математическим символам (Sm) или прочим символам (So) стандарта Unicode либо к семибитным ASCIIсимволам, не являющимся буквами, цифрами, круглыми, квадратными и фигурными скобками, одинарными или двойными кавычками или знаками подчеркивания, точки, точки с запятой, запятой или обратных кавычек.
140 Глава 6 • Функциональные объекты идентификатора. В результате всегда будет получаться идентификатор
Scala. Это сработает даже в том случае, если имя, заключенное в обратные кавычки, является в Scala зарезервированным словом. Обычно такие иден
тификаторы используются при обращении к статическому методу yield в Javaклассе
Thread
. Вы не можете прибегнуть к коду
Thread.yield()
, по
скольку в Scala yield является зарезервированным словом. Но имя метода все же можно применить, если заключить его в обратные кавычки, например
Thread.`yield`()
6 .11 . Перегрузка методов
Вернемся к классу
Rational
. После внесения последних изменений появи
лась возможность применять операции сложения и умножения рациональ
ных чисел в их естественном виде. Но мы все же упустили из виду смешан
ную арифметику. Например, вы не можете умножить рациональное число на целое, поскольку операнды у оператора
*
всегда должны быть объектами
Rational
. Следовательно, для рационального числа r
вы не можете написать код r
*
2
. Вам нужно написать r
*
Rational(2)
, а это имеет неприглядный вид.
Чтобы сделать класс
Rational еще более удобным в использовании, добавим к нему новые методы, выполняющие смешанное сложение и умножение ра
циональных и целых чисел. А заодно добавим методы вычитания и деления.
Результат показан в листинге 6.5.
Теперь здесь две версии каждого арифметического метода: одна в качестве аргумента получает рациональное число, вторая — целое. Иными словами, все эти методы называются перегруженными, поскольку каждое имя теперь используется несколькими методами. Например, имя
+
применяется и ме
тодом, получающим объект
Rational
, и методом, получающим объект
Int
При вызове метода компилятор выбирает версию перегруженного метода, которая в точности соответствует типу аргументов. Например, если аргу
мент y
в вызове x.+(y)
является объектом
Rational
, то компилятор выберет метод
+
, получающий в качестве параметра объект
Rational
. Но если аргу
мент — целое число, то компилятор выберет метод
+
, получающий в качестве параметра объект
Int
. Если испытать код в действии:
Val r = Rational(2, 3) // 2/3
r * r // 4/9
r * 2 // 4/3
станет понятно, что вызываемый метод
*
определяется каждый раз по типу его правого операнда.
6 .11 . Перегрузка методов 141
ПРИМЕЧАНИЕ
В Scala процесс анализа при выборе перегруженного метода очень похож на аналогичный процесс в Java . В любом случае выбирается перегруженная версия, которая лучше подходит к статическим типам аргументов . Иногда случается, что одной такой версии нет, и тогда компилятор выдаст ошибку, связанную с неоднозначной ссылкой, — ambiguous reference .
Листинг 6.5. Класс Rational с перегруженными методами class Rational(n: Int, d: Int):
require(d != 0)
private val g = gcd(n.abs, d.abs)
val numer = n / g val denom = d / g def this(n: Int) = this(n, 1)
def + (that: Rational): Rational =
Rational(
numer * that.denom + that.numer * denom,
denom * that.denom
)
def + (i: Int): Rational =
Rational(numer + i * denom, denom)
def - (that: Rational): Rational =
Rational(
numer * that.denom - that.numer * denom,
denom * that.denom
)
def - (i: Int): Rational =
Rational(numer — i * denom, denom)
def * (that: Rational): Rational =
Rational(numer * that.numer, denom * that.denom)
def * (i: Int): Rational =
Rational(numer * i, denom)
def / (that: Rational): Rational =
Rational(numer * that.denom, denom * that.numer)
def / (i: Int): Rational =
Rational(numer, denom * i)
override def toString = s"$numer/$denom"
private def gcd(a: Int, b: Int): Int =
if b == 0 then a else gcd(b, a % b)
142 Глава 6 • Функциональные объекты
6 .12 . Методы расширения
Теперь, когда можно воспользоваться кодом r
*
2
, вы также можете захотеть поменять операнды местами, задействовав код
2
*
r
. К сожалению, пока этот код работать не будет:
scala> 2 * r
1 |2 * r
|ˆˆˆ
|None of the overloaded alternatives of method * in
| class Int with types
| (x: Double): Double
| (x: Float): Float
| (x: Long): Long
| (x: Int): Int
| (x: Char): Int
| (x: Short): Int
| (x: Byte): Int
|match arguments ((r : Rational))
Проблема в том, что эквивалент выражения
2
*
r
— выражение
2.*(r)
, то есть вызов метода в отношении числа 2, которое является целым. Но в классе
Int не содержится метода умножения, получающего в качестве аргумента объект
Rational
, его там и не может быть, поскольку он не входит в состав стандартных классов библиотеки Scala.
Но в Scala есть другой способ решения этой проблемы. Вы можете создавать методы расширения для
Int
, которые содержат рациональные числа. Попро
буйте добавить эти строки в REPL:
extension (x: Int)
def + (y: Rational) = Rational(x) + y def - (y: Rational) = Rational(x) - y def * (y: Rational) = Rational(x) * y def / (y: Rational) = Rational(x) / y
Это определяет четыре метода расширения для
Int
, каждый из которых использует
Rational
. Компилятор может использовать их автоматически в ряде ситуаций. С определенными методами расширения теперь вы можете повторить пример, который раньше не удался:
val r = Rational(2,3) // 2/3 2 * r // 4/3
Чтобы метод расширения работал, он должен находиться в области види
мости. Если вы поместите определение метода расширения внутри класса
Резюме 143
Rational
, он не попадет в область действия REPL, поэтому вам необходимо определить его непосредственно в REPL.
Как видно из этого примера, методы расширения — это очень эффективная техника, позволяющая сделать библиотеки более гибкими и удобными в использовании. Однако ее чрезмерное использование может и навредить.
В главе 22 вы узнаете больше о методах расширения, в том числе о способах включения их в область видимости там, где они необходимы.
6 .13 . Предостережение
Создание методов с именами операторов и определение методов расширения, продемонстрированные в этой главе, призваны помочь в проектировании библиотек, для которых код клиента будет лаконичным и понятным. Scala предоставляет широкие возможности для разработки таких весьма доступ
ных для использования библиотек. Но, пожалуйста, имейте в виду: реализуя возможности, не стоит забывать об ответственности.
При неумелом использовании и методыоператоры, и методы расширения могут сделать клиентский код таким, что его станет трудно читать и по
нимать. Выполнение компилятором методов расширения никак внешне не проявляется и не записывается в явном виде в исходный код. Поэтому про
граммистам на клиенте может быть невдомек, что именно оно и применяется в вашем коде. И хотя методыоператоры обычно делают клиентский код более лаконичным и читабельным, таким он становится только для наибо
лее сведущих программистовклиентов, способных запомнить и распознать значение каждого оператора.
При проектировании библиотек всегда нужно стремиться сделать клиент
ский код не просто лаконичным, но и легкочитаемым и понятным. Читабель
ность в значительной степени может быть обусловлена лаконичностью, кото
рая способна заходить очень далеко. Проектируя библиотеки, позволяющие добиваться изысканной лаконичности, и в то же время создавая понятный клиентский код, вы можете существенно повысить продуктивность работы программистов, которые используют эти библиотеки.
Резюме
В этой главе мы рассмотрели многие аспекты классов Scala. Вы увидели способы добавления к классу параметров, определили несколько конструк
144 Глава 6 • Функциональные объекты торов, операторы и методы и настроили классы таким образом, чтобы их применение приобрело более естественный вид. Важнее всего, вероятно, было показать вам, что определение и использование неизменяющихся объ
ектов — вполне естественный способ программирования на Scala.
Хотя показанная здесь финальная версия класса
Rational соответствует всем требованиям, обозначенным в начале главы, ее можно усовершенствовать.
Позже мы вернемся к этому примеру. В частности, в главе 8 будет рассмотре
но переопределение методов equals и hashcode
, которое позволяет объектам
Rational улучшить свое поведение в момент, когда их сравнивают с помощью оператора
==
или помещают в хештаблицы. В главе 22 поговорим о том, как помещать методы расширения в объектыкомпаньоны класса
Rational
, кото
рые упрощают для программистовклиентов помещение в область видимости объектов типа
Rational
7
Встроенные управляющие конструкции
В Scala имеется весьма незначительное количество встроенных управля
ющих конструкций. К ним относятся if
, while
, for
, try
, match и вызовы функций. Их в Scala немного, поскольку с момента создания данного языка в него были включены функциональные литералы. Вместо накопления в базовом синтаксисе одной за другой высокоуровневых управляющих конструкций Scala собирает их в библиотеках. Как именно это делается, мы покажем в главе 9. А здесь рассмотрим имеющиеся в Scala немногочисленные встроенные управляющие конструкции.
Следует учесть, что почти все управляющие конструкции Scala приводят к какомулибо значению. Такой подход принят в функциональных языках, где программы рассматриваются в качестве вычислителей значений, стало быть, компоненты программы тоже должны вычислять значения. Можно рассматривать данное обстоятельство как логическое завершение тенденции, уже присутствующей в императивных языках. В них вызовы функций могут возвращать значение, даже когда наряду с этим будет происходить обнов
ление вызываемой функцией выходной переменной, переданной в качестве аргумента. Кроме того, в императивных языках зачастую имеется тернарный оператор (такой как оператор
?:
в C, C++ и Java), который ведет себя полно
стью аналогично if
, но при этом возвращает значение. Scala позаимствовал эту модель тернарного оператора, но назвал ее if
. Иными словами, исполь
зуемый в Scala оператор if может выдавать значение. Затем эта тенденция в Scala получила развитие: for
, try и match тоже стали выдавать значения.
Программисты могут использовать полученное в результате значение, чтобы упростить свой код, применяя те же приемы, что и для значений, возвращаемых функциями. Не будь этой особенности, программистам пришлось бы созда
вать временные переменные, просто чтобы хранить результаты, вычисленные
146 Глава 7 • Встроенные управляющие конструкции внутри управляющей конструкции. Отказ от таких переменных немного упрощает код, а также избавляет от многих ошибок, возникающих, когда в од
ном ответвлении переменная создается, а в другом о ее создании забывают.
В целом, основные управляющие конструкции Scala в минимальном соста
ве обеспечивают все, что нужно было взять из императивных языков. При этом они позволяют сделать код более лаконичным за счет неизменного на
личия значений, получаемых в результате их применения. Чтобы показать все это в работе, далее более подробно рассмотрим основные управляющие конструкции Scala.
7 .1 . Выражения if
Выражение if в Scala работает практически так же, как во многих других языках. Оно проверяет условие, а затем выполняет одну из двух ветвей кода в зависимости от того, вычисляется ли условие в true
. Простой пример, на
писанный в императивном стиле, выглядит следующим образом:
var filename = "default.txt"
if !args.isEmpty then filename = args(0)
В этом коде объявляется переменная по имени filename
, которая инициа
лизируется значением по умолчанию. Затем в нем используется выражение if с целью проверить, предоставлены ли программе какиелибо аргументы.
Если да, то в переменную вносят изменения, чтобы в ней содержалось зна
чение, указанное в списке аргументов. Если нет, то выражение оставляет значение переменной, установленное по умолчанию.
Этот код можно сделать гораздо более выразительным, поскольку, как упоминалось в шаге 3 главы 2, выражение if в Scala возвращает значение.
В листинге 7.1 показано, как можно выполнить те же самые действия, что и в предыдущем примере, не прибегая к использованию var
переменных.
Листинг 7.1. Особый стиль Scala, применяемый для условной инициализации val filename =
if !args.isEmpty then args(0)
else "default.txt"
На этот раз у if имеются два ответвления. Если массив args непустой, то выбирается его начальный элемент args(0)
. В противном случае выбирается значение по умолчанию. Выражение if выдает результат в виде выбранного
7 .2 . Циклы while 147
значения, которым инициализируется переменная filename
. Данный код немного короче предыдущего. Но гораздо более существенно то, что в нем используется val
, а не var
переменная. Это соответствует функциональному стилю и помогает вам примерно так же, как применение финальной (
final
) переменной в Java. Она сообщает читателям кода, что переменная никогда не изменится, избавляя их от необходимости просматривать весь код в об
ласти видимости переменной, чтобы понять, изменяется ли она гденибудь.
Второе преимущество использования v a r
переменной вместо v a l
переменной заключается в том, что она лучше поддерживает выводы, кото
рые делаются с помощью эквациональных рассуждений (equational reasoning).
Введенная переменная равна вычисляющему выражению при условии, что у него нет побочных эффектов. Таким образом, всякий раз, собираясь напи
сать имя переменной, вы можете вместо него написать выражение. Вместо println(filename)
, к примеру, можно просто написать следующий код:
println(if (!args.isEmpty) args(0) else "default.txt")
Выбор за вами. Вы можете прибегнуть к любому из вариантов. Использова
ние val
переменных помогает совершенно безопасно проводить подобный рефакторинг кода по мере его развития.
Всегда ищите возможности для применения val
переменных. Они смогут упростить не только чтение вашего кода, но и его рефакторинг.
7 .2 . Циклы while
Используемые в Scala циклы while ведут себя точно так же, как и в других языках. В них имеются условие и тело, которое выполняется снова и снова, пока условие вычисляется в true
. Пример показан в листинге 7.2.
1 ... 12 13 14 15 16 17 18 19 ... 64