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
Чтобы метод расширения работал, он должен находиться в области види
мости. Если вы поместите определение метода расширения внутри класса
Резюме
143Rational
, он не попадет в область действия 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.