Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 749
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
108 Глава 5 • Основные типы и операции инициализации произвольных типов. Вы можете включить эту функцию с помощью импорта этого языка:
import scala.language.experimental.genericNumberLiterals
Вот два примера из стандартной библиотеки:
val invoice: BigInt = 1_000_000_000_000_000_000_000
val pi: BigDecimal = 3.1415926535897932384626433833
Символьные литералы
Символьные литералы состоят из любого Unicodeсимвола, заключенного в одинарные кавычки:
scala> val a = 'A'
val a: Char = A
Помимо того что символ представляется в одинарных кавычках в явном виде, его можно указывать с помощью кода из таблицы символов Unicode.
Для этого нужно записать
\u
, после чего указать четыре шестнадцатеричные цифры кода:
scala> val d = '\u0041'
val d: Char = A
scala> val f = '\u0044'
val f: Char = D
Такие символы в кодировке Unicode могут появляться в любом месте про
граммы на языке Scala. Например, вы можете набрать следующий иденти
фикатор:
scala> val B\u0041\u0044 = 1
val BAD: Int = 1
Он рассматривается точно так же, как идентификатор
BAD
, являющийся результатом раскрытия символов в кодировке Unicode в показанном ранее коде. По сути, в именовании идентификаторов подобным образом нет ничего хорошего, поскольку их трудно прочесть. Иногда с помощью этого синта
ксиса исходные файлы Scala, которые содержат отсутствующие в таблице
ASCII символы из таблицы Unicode, можно представить в кодировке ASCII.
И наконец, нужно упомянуть о нескольких символьных литералах, пред
ставленных специальными управляющими последовательностями (escape sequences), показанными в табл. 5.2, например:
scala> val backslash = '\\'
val backslash: Char = \
5 .2 . Литералы 109
Таблица 5.2. Управляющие последовательности специальных символьных литералов
Литерал
Предназначение
\n
Перевод строки (
\u000A
)
\b
Возврат на одну позицию (
\u0008
)
\t
Табуляция (
\u0009
)
\f
Перевод страницы (
\u000C
)
\r
Возврат каретки (
\u000D
)
\"
Двойные кавычки (
\u0022
)
\'
Одинарная кавычка (
\u0027
)
\\
Обратный слеш (
\u005C
)
Строковые литералы
Строковый литерал состоит из символов, заключенных в двойные кавычки:
scala> val hello = "hello"
val hello: String = hello
Синтаксис символов внутри кавычек такой же, как и в символьных литера
лах, например:
scala> val escapes = "\\\"\'"
val escapes: String = \"'
Данный синтаксис неудобен для строк, в которых содержится множество управляющих последовательностей, или для строк, не умещающихся в одну строку текста, поэтому для неформатированных строк в Scala включен специальный синтаксис. Неформатированная строка начинается и закан
чивается тремя идущими подряд двойными кавычками (
"""
). Внутри нее могут содержаться любые символы, включая символы новой строки, кавычки и специальные символы, за исключением, разумеется, трех кавычек подряд.
Например, следующая программа выводит сообщение, используя неформа
тированную строку:
println("""Welcome to Ultamix 3000.
Type "HELP" for help.""")
Но при запуске этого кода получается не совсем то, что хотелось:
Welcome to Ultamix 3000.
Type "HELP" for help.
1 ... 8 9 10 11 12 13 14 15 ... 64
110 Глава 5 • Основные типы и операции
Проблема во включении в строку пробелов перед второй строкой текста!
Чтобы справиться с этой весьма часто возникающей ситуацией, вы можете вызывать в отношении строк метод stripMargin
. Чтобы им воспользоваться, поставьте символ вертикальной черты (
|
) перед каждой строкой текста, а за
тем в отношении всей строки вызовите метод stripMargin
:
println("""|Welcome to Ultamix 3000.
|Type "HELP" for help.""".stripMargin)
Вот теперь код ведет себя подобающим образом:
Welcome to Ultamix 3000.
Type "HELP" for help.
Булевы литералы
У типа
Boolean имеется два литерала, true и false
:
val bool = true // true: Boolean val fool = false // false: Boolean
Вот, собственно, и все. Теперь вы буквально (или литерально) стали боль
шим специалистом по Scala.
5 .3 . Интерполяция строк
В Scala включен довольно гибкий механизм для интерполяции строк, позволяющий вставлять выражения в строковые литералы. В самом рас
пространенном случае использования этот механизм предоставляет лако
ничную и удобочитаемую альтернативу конкатенации строк. Рассмотрим пример:
val name = "reader"
println(s"Hello, $name!")
Выражение s"Hello,
$name!"
— обрабатываемый строковый литерал. По
скольку за буквой s
стоят открывающие кавычки, то Scala для обработки литерала воспользуется интерполятором строк s
. Он станет вычислять каждое встроенное выражение, вызывая в отношении каждого результата метод toString и заменяя встроенные выражения в литерале этими резуль
татами. Таким образом, из s"Hello,
$name!"
получится "Hello,
reader!"
,
5 .3 . Интерполяция строк 111
то есть точно такой же результат, как при использовании кода "Hello,
"
+
name
+
"!"
После знака доллара (
$
) в обрабатываемом строковом литерале можно ука
зать любое выражение. Для выражений с одной переменной зачастую можно просто поместить после знака доллара имя этой переменной. Все символы, вплоть до первого символа, не относящегося к идентификатору, Scala будет интерпретировать как выражение. Если в него включены символы, не явля
ющиеся идентификаторами, то это выражение следует заключить в фигур
ные скобки, а открывающая фигурная скобка должна ставиться сразу же после знака доллара, например:
scala> s"The answer is ${6 * 7}."
val res0: String = The answer is 42.
Scala содержит еще два интерполятора строк: raw и f
. Интерполятор строк raw ведет себя практически так же, как и s
, за исключением того, что не рас
познает управляющие последовательности символьных литералов (те самые, которые показаны в табл. 5.2). Например, следующая инструкция выводит четыре, а не два обратных слеша:
println(raw"No\\\\escape!") // выводит: No\\\\escape!
Интерполятор строк f
позволяет прикреплять к встроенным выражениям инструкции форматирования в стиле функции printf
. Инструкции ставятся после выражения и начинаются со знака процента (
%
), при этом используется синтаксис, заданный классом java.util.Formatter
. Например, вот как можно было бы отформатировать число
π:
scala> f"${math.Pi}%.5f"
val res1: String = 3.14159
Если для встроенного выражения не указать никаких инструкций форма
тирования, то интерполятор строк f
по умолчанию превратится в
%s
, что означает подстановку значения, полученного в результате выполнения ме
тода toString
, точно так же, как это делает интерполятор строк s
, например:
scala> val pi = "Pi"
val pi: String = Pi scala> f"$pi is approximately ${math.Pi}%.8f."
val res2: String = Pi is approximately 3.14159265.
В Scala интерполяция строк реализуется перезаписью кода в ходе ком
пиляции. Компилятор в качестве выражения интерполятора строк будет
112 Глава 5 • Основные типы и операции рассматривать любое выражение, состоящее из идентификатора, за которым сразу же стоят открывающие двойные кавычки строкового литерала. Интер
поляторы строк s
, f
и raw реализуются с помощью этого общего механизма.
Библиотеки и пользователи могут определять другие интерполяторы строк, применяемые в иных целях.
5 .4 . Все операторы являются методами
Для основных типов Scala предоставляет весьма богатый набор операторов.
Как упоминалось в предыдущих главах, эти операторы — всего лишь прият
ный синтаксис для обычных вызовов методов. Например,
1
+
2
означает то же самое, что и
1.+(2)
. Иными словами, в классе
Int имеется метод по имени
+
, который получает
Int
значение и возвращает
Int
результат. Он вызывается при сложении двух
Int
значений:
val sum = 1 + 2 // Scala вызывает 1.+(2)
Чтобы убедиться в этом, можете набрать выражение, в точности соответ
ствующее вызову метода:
scala> val sumMore = 1.+(2)
val sumMore: Int = 3
Фактически в классе
Int содержится несколько перегруженных методов
+
, получающих различные типы параметров
1
. Например, у
Int есть еще один метод, тоже по имени
+
, который получает и возвращает значения типа
Long
При сложении
Long и
Int будет вызван именно этот альтернативный метод:
scala> val longSum = 1 + 2L // Scala вызывает 1.+(2L)
val longSum: Long = 3
Символ
+
— оператор, точнее, инфиксный оператор. Форма записи опера
торов не ограничивается методами, подобными
+
, которые в других языках выглядят как операторы. Любой метод может использоваться в нотации операторов, если он принимает только один параметр
2
. Например, в классе
String есть метод indexOf
, получающий один параметр типа
Char
. Метод indexOf ведет поиск первого появления в строке указанного символа и воз
1
Перегруженные методы имеют точно такие же имена, но используют другие типы аргументов. Более подробно перегрузка методов рассматривается в разделе 6.11.
2
В будущих версиях Scala методы с несимвольными именами будут разрешены в качестве операторов только в том случае, если они объявлены с модификато
ром infix
5 .4 . Все операторы являются методами 113
вращает его индекс или
–1
, если символ найден не будет. Метод indexOf можно использовать как оператор:
scala> val s = "Hello, world!"
val s: String = Hello, world!
scala> s indexOf 'o' // Scala вызывает s.indexOf('o')
val res0: Int = 4
Любой однопараметрический метод может быть оператором
В Scala операторы не относятся к специальному синтаксису языка.
Любой метод, который содержит один параметр, может быть операто
ром. Однопараметрический метод становится оператором в зависимо
сти от того, как вы его используете. Если вы напишете s.indexOf('o')
, то indexOf не будет являться оператором, но станет им, если запись будет иметь вид формы оператора — s
indexOf
'o'
До сих пор рассматривались только примеры инфиксной формы записи операторов, означающей, что вызываемый метод находится между объектом и параметром или параметрами, которые нужно передать методу, как в вы
ражении
7
+
2
. В Scala также имеются две другие формы записи операторов: префиксная и постфиксная. В префиксной форме записи имя метода ста
вится перед объектом, в отношении которого вызывается этот метод (напри
мер,
–
в выражении
–7
). В постфиксной форме имя метода ставится после объекта (например, toLong в выражении
7
toLong
).
В отличие от инфиксной формы записи, в которой операторы получают два операнда (один слева, другой справа), префиксные и постфиксные операторы являются унарными — получают только один операнд. В префиксной форме записи операнд размещается справа от оператора. В качестве примеров мож
но привести выражения
–2.0
,
!found и
0xFF
. Как и в случае использования инфиксных операторов, эти префиксные операторы являются сокращенной формой вызова методов. Но в данном случае перед символом оператора в име
ни метода ставится приставка unary_
. Например, Scala превратит выражение
–2.0
в вызов метода
(2.0).unary_–
. Вы можете убедиться в этом, набрав вызов метода как с использованием формы записи операторов, так и в явном виде:
scala> -2.0 // Scala вызывает (2.0).unary_- val res2: Double = -2.0
scala> (2.0).unary_- val res3: Double = -2.0
114 Глава 5 • Основные типы и операции
Идентификаторами, которые могут служить в качестве префиксных операто
ров, являются только
+
,
–
,
!
и
. Следовательно, если вы определите метод по имени unary_!
, то сможете вызвать его в отношении значения или переменной подходящего типа, прибегнув к префиксной форме записи операторов, напри
мер
!p
. Но, определив метод по имени unary_*
, вы не сможете использовать префиксную форму записи операторов, поскольку
*
не входит в число четы
рех идентификаторов, которые могут использоваться в качестве префиксных операторов. Метод можно вызвать обычным способом как p.unary_*
, но при попытке вызвать его в виде
*p
Scala воспримет код так, словно он записан в виде
*.p
, что, вероятно, совершенно не совпадает с задуманным
1
!
Постфиксные операторы, будучи вызванными без точки или круглых ско
бок, являются методами, не получающими аргументов. В Scala при вызове метода пустые круглые скобки можно не ставить. Соглашение гласит, что круглые скобки ставятся, если метод имеет побочные эффекты, как в случае с методом println()
. Но их можно не ставить, если метод не имеет побочных эффектов, как в случае с методом toLowerCase
, вызываемым в отношении значения типа
String
:
scala> val s = "Hello, world!"
val s: String = Hello, world!
scala> s.toLowerCase val res4: String = hello, world!
В последнем случае, где методу не требуются аргументы, можно при желании не ставить точку и воспользоваться постфиксной формой записи операто
ров. Однако компилятор потребует, чтобы вы импортировали scala.lan- guage.postfixOps
, прежде чем вызывать метод:
scala> import scala.language.postfixOps scala> s toLowerCase val res5: String = hello, world!
Здесь метод toLowerCase используется в качестве постфиксного оператора в отношении операнда s
Чтобы понять, какие операторы можно использовать с основными типами
Scala, нужно посмотреть на методы, объявленные в классах типов, в доку
ментации по Scala API. Но данная книга — пособие по языку Scala, поэтому в нескольких следующих разделах будет представлен краткий обзор боль
шинства этих методов.
1
Однако не обязательно все будет потеряно. Есть весьма незначительная вероят
ность того, что программа с кодом
*p может скомпилироваться как код C++.
5 .5 . Арифметические операции 115
УСКОРЕННЫЙ РЕЖИМ ЧТЕНИЯ ДЛЯ JAVA-ПРОГРАММИСТОВ
Многие аспекты Scala, рассматриваемые в оставшейся части главы, со- впадают с аналогичными Java-аспектами . Если вы хорошо разбираетесь в Java и у вас мало времени, то можете спокойно перейти к разделу 5 .8, в котором рассматриваются отличия Scala от Java в области равенства объектов .
5 .5 . Арифметические операции
Арифметические методы при работе с любыми числовыми типами можно вызвать в инфиксной форме для сложения (
+
), вычитания (
–
), умноже
ния (
*
), деления (
/
) и получения остатка от деления (
%
). Вот несколько примеров:
1.2 + 2.3 // 3.5: Double
3 — 1 // 2: Int
'b' — 'a' // 1: Int
2L * 3L // 6: Long
11 / 4 // 2: Int
11 % 4 // 3: Int
11.0f / 4.0f // 2.75: Float
11.0 % 4.0 // 3.0: Double
Когда целочисленными типами являются как правый, так и левый операнды
(
Int
,
Long
,
Byte
,
Short или
Char
), оператор
/
выведет всю числовую часть результата деления, исключая остаток. Оператор
%
показывает остаток от предполагаемого целочисленного деления.
Остаток от деления числа с плавающей точкой, полученный с помощью метода
%
, не определен в стандарте IEEE 754. Что касается операции вы
числения остатка, в этом стандарте используется деление с округлением, а не деление с отбрасыванием остатка. Поэтому данная операция сильно от
личается от операции вычисления остатка от целочисленного деления. Если всетаки нужно получить остаток по стандарту IEEE 754, то можно вызвать метод
IEEEremainder из scala.math
:
math.IEEEremainder(11.0, 4.0) // -1.0: Double
Числовые типы также предлагают прибегнуть к унарным префиксным операторам
+
(метод unary_+
) и
–
(метод unary_–
), позволяющим показать положительное или отрицательное значение числового литерала, как в
–3
или
+4.0
. Если не указать унарный
+
или
–
, то литерал интерпретируется как положительный. Унарный
+
существует исключительно для симметрии с унарным
–
, однако не производит никаких действий. Унарный
–
может
116 Глава 5 • Основные типы и операции также использоваться для смены знака переменной. Вот несколько при
меров:
val neg = 1 + -3 // -2 : Neg val y = +3 // 3: Int
-neg // 2: Int
5 .6 . Отношения и логические операции
Числовые типы можно сравнивать с помощью методов отношений «боль
ше» (
>
), «меньше» (
<
), «больше или равно» (
>=
) и «меньше или равно» (
<=
), которые выдают в качестве результата булево значение. Дополнительно, чтобы инвертировать булево значение, можно использовать унарный опе
ратор
!
(метод unary_!
). Вот несколько примеров:
1 > 2 // false: Boolean
1 < 2 // true: Boolean
1.0 <= 1.0 // true: Boolean
3.5f >= 3.6f // false: Boolean
'a' >= 'A' // true: Boolean val untrue = !true // false: Boolean
Методы «логическое И» (
&&
и
&
) и «логическое ИЛИ» (
||
и
|
) получают операнды типа
Boolean в инфиксной нотации и выдают результат в виде
Boolean
значения, например:
val toBe = true // true: Boolean val question = toBe || !toBe // true: Boolean val paradox = toBe && !toBe // false: Boolean
Операции
&&
и
||
, как и в Java, — сокращенно вычисляемые: выражения, по
строенные с помощью этих операторов, вычисляются, только когда это нуж
но для определения результата. Иными словами, правая часть выражений с использованием
&&
и
||
не будет вычисляться, если результат уже опреде
лился при вычислении левой части. Например, если левая часть выражения с методом
&&
вычисляется в false
, то результатом выражения, несомненно, будет false
, поэтому правая часть не вычисляется. Аналогично этому если левая часть выражения с методом
||
вычисляется в true
, то результатом выражения конечно же будет true
, поэтому правая часть не вычисляется:
scala> def salt() = { println("salt"); false }
def salt(): Boolean scala> def pepper() = { println("pepper"); true }
def pepper(): Boolean
5 .7 . Поразрядные операции 117
scala> pepper() && salt()
pepper salt val res21: Boolean = false scala> salt() && pepper()
salt val res22: Boolean = false
В первом выражении вызываются pepper и salt
, но во втором вызывается только salt
. Поскольку salt возвращает false
, то необходимость в вызове pepper отпадает.
Если правую часть нужно вычислить при любых условиях, то вместо пока
занных выше методов следует обратиться к методам
&
и
|
. Первый выполняет логическую операцию И, а второй — операцию ИЛИ, но при этом они не прибегают к сокращенному вычислению, как это делают методы
&&
и
||
. Вот как выглядит пример их использования:
scala> salt() & pepper()
salt pepper val res23: Boolean = false
ПРИМЕЧАНИЕ
Возникает вопрос: как сокращенное вычисление может работать, если операторы — всего лишь методы? Обычно все аргументы вычисляются перед входом в метод, тогда каким же образом метод может избежать вы- числения своего второго аргумента? Дело в том, что у всех методов Scala есть средство для задержки вычисления его аргументов или даже полной его отмены . Оно называется «параметр, передаваемый по имени» и будет рассмотрено в разделе 9 .5 .
5 .7 . Поразрядные операции
Scala позволяет выполнять операции над отдельными разрядами целочис
ленных типов, используя несколько поразрядных методов. К таким методам относятся поразрядное И (
&
), поразрядное ИЛИ (
|
) и поразрядное исклю
чающее ИЛИ (
^
)
1
. Унарный поразрядный оператор дополнения (
, метод unary_
) инвертирует каждый разряд в своем операнде, например:
1
Метод поразрядного исключающего ИЛИ выполняет соответствующую опера
цию в отношении своих операндов. Из одинаковых разрядов получается
0
, а из разных
-1
. Следовательно, выражение
0011
^
0101
вычисляется в
0110
118 Глава 5 • Основные типы и операции
1 & 2 // 0: Int
1 | 2 // 3: Int
1 ^ 3 // 2: Int
1 // -2: Int
В первом выражении,
1
&
2
, выполняется поразрядное И над каждым раз
рядом чисел
1
(
0001
) и
2
(
0010
) и выдается результат
0
(
0000
). Во втором выражении,
1
|
2
, выполняется поразрядное ИЛИ над теми же операндами и выдается результат
3
(
0011
). В третьем выражении,
1
^
3
, выполняется поразрядное исключающее ИЛИ над каждым разрядом
1
(
0001
) и
3
(
0011
) и выдается результат
2
(
0010
). В последнем выражении,
1
, инвертируется каждый разряд в
1
(
0001
) и выдается результат
–2
, который в двоичной форме выглядит как
11111111111111111111111111111110
Целочисленные типы Scala также предлагают три метода сдвига: влево (
<<
), вправо (
>>
) и беззнаковый сдвиг вправо (
>>>
). Методы сдвига, примененные в инфиксной форме записи операторов, сдвигают разряды целочисленного значения, указанные слева от оператора, на количество разрядов, указанное в целочисленном значении справа от оператора. При сдвиге влево и беззнако
вом сдвиге вправо разряды по мере сдвига заполняются нулями. При сдвиге вправо разряды указанного слева значения по мере сдвига заполняются зна
чением самого старшего разряда (разряда знака). Вот несколько примеров:
-1 >> 31 // -1: Int
-1 >>> 31 // 1: Int
1 << 2 // 4: Int
Число
–1
в двоичном виде выглядит как
11111111111111111111111111111111
В первом примере,
–1
>>
31
, в числе
–1
происходит сдвиг вправо на 31 разряд
ную позицию. В значении типа
Int содержатся 32 разряда, поэтому данная операция, по сути, перемещает самый левый разряд до тех пор, пока тот не станет самым правым
1
. Поскольку метод
>>
выполняет заполнение по мере сдвига единицами ввиду того, что самый левый разряд числа
–1
— это
1
, результат получается идентичным исходному левому операнду и состоит из 32 единичных разрядов, или равняется
–1
. Во втором примере,
–1
>>>
31
, самый левый разряд опять сдвигается вправо в самую правую позицию, од
нако на сей раз освобождающиеся разряды заполняются нулями. Поэтому результат в двоичном виде получается
00000000000000000000000000000001
, или
1
. В последнем примере,
1
<<
2
, левый операнд,
1
, сдвигается влево на две позиции (освобождающиеся позиции заполняются нулями), в результате
1
Самый левый разряд в целочисленном типе является знаковым. Если самый левый разряд установлен в 1, значит, число отрицательное, если в 0, то положительное.
5 .8 . Равенство объектов
1 ... 9 10 11 12 13 14 15 16 ... 64
110 Глава 5 • Основные типы и операции
Проблема во включении в строку пробелов перед второй строкой текста!
Чтобы справиться с этой весьма часто возникающей ситуацией, вы можете вызывать в отношении строк метод stripMargin
. Чтобы им воспользоваться, поставьте символ вертикальной черты (
|
) перед каждой строкой текста, а за
тем в отношении всей строки вызовите метод stripMargin
:
println("""|Welcome to Ultamix 3000.
|Type "HELP" for help.""".stripMargin)
Вот теперь код ведет себя подобающим образом:
Welcome to Ultamix 3000.
Type "HELP" for help.
Булевы литералы
У типа
Boolean имеется два литерала, true и false
:
val bool = true // true: Boolean val fool = false // false: Boolean
Вот, собственно, и все. Теперь вы буквально (или литерально) стали боль
шим специалистом по Scala.
5 .3 . Интерполяция строк
В Scala включен довольно гибкий механизм для интерполяции строк, позволяющий вставлять выражения в строковые литералы. В самом рас
пространенном случае использования этот механизм предоставляет лако
ничную и удобочитаемую альтернативу конкатенации строк. Рассмотрим пример:
val name = "reader"
println(s"Hello, $name!")
Выражение s"Hello,
$name!"
— обрабатываемый строковый литерал. По
скольку за буквой s
стоят открывающие кавычки, то Scala для обработки литерала воспользуется интерполятором строк s
. Он станет вычислять каждое встроенное выражение, вызывая в отношении каждого результата метод toString и заменяя встроенные выражения в литерале этими резуль
татами. Таким образом, из s"Hello,
$name!"
получится "Hello,
reader!"
,
5 .3 . Интерполяция строк 111
то есть точно такой же результат, как при использовании кода "Hello,
"
+
name
+
"!"
После знака доллара (
$
) в обрабатываемом строковом литерале можно ука
зать любое выражение. Для выражений с одной переменной зачастую можно просто поместить после знака доллара имя этой переменной. Все символы, вплоть до первого символа, не относящегося к идентификатору, Scala будет интерпретировать как выражение. Если в него включены символы, не явля
ющиеся идентификаторами, то это выражение следует заключить в фигур
ные скобки, а открывающая фигурная скобка должна ставиться сразу же после знака доллара, например:
scala> s"The answer is ${6 * 7}."
val res0: String = The answer is 42.
Scala содержит еще два интерполятора строк: raw и f
. Интерполятор строк raw ведет себя практически так же, как и s
, за исключением того, что не рас
познает управляющие последовательности символьных литералов (те самые, которые показаны в табл. 5.2). Например, следующая инструкция выводит четыре, а не два обратных слеша:
println(raw"No\\\\escape!") // выводит: No\\\\escape!
Интерполятор строк f
позволяет прикреплять к встроенным выражениям инструкции форматирования в стиле функции printf
. Инструкции ставятся после выражения и начинаются со знака процента (
%
), при этом используется синтаксис, заданный классом java.util.Formatter
. Например, вот как можно было бы отформатировать число
π:
scala> f"${math.Pi}%.5f"
val res1: String = 3.14159
Если для встроенного выражения не указать никаких инструкций форма
тирования, то интерполятор строк f
по умолчанию превратится в
%s
, что означает подстановку значения, полученного в результате выполнения ме
тода toString
, точно так же, как это делает интерполятор строк s
, например:
scala> val pi = "Pi"
val pi: String = Pi scala> f"$pi is approximately ${math.Pi}%.8f."
val res2: String = Pi is approximately 3.14159265.
В Scala интерполяция строк реализуется перезаписью кода в ходе ком
пиляции. Компилятор в качестве выражения интерполятора строк будет
112 Глава 5 • Основные типы и операции рассматривать любое выражение, состоящее из идентификатора, за которым сразу же стоят открывающие двойные кавычки строкового литерала. Интер
поляторы строк s
, f
и raw реализуются с помощью этого общего механизма.
Библиотеки и пользователи могут определять другие интерполяторы строк, применяемые в иных целях.
5 .4 . Все операторы являются методами
Для основных типов Scala предоставляет весьма богатый набор операторов.
Как упоминалось в предыдущих главах, эти операторы — всего лишь прият
ный синтаксис для обычных вызовов методов. Например,
1
+
2
означает то же самое, что и
1.+(2)
. Иными словами, в классе
Int имеется метод по имени
+
, который получает
Int
значение и возвращает
Int
результат. Он вызывается при сложении двух
Int
значений:
val sum = 1 + 2 // Scala вызывает 1.+(2)
Чтобы убедиться в этом, можете набрать выражение, в точности соответ
ствующее вызову метода:
scala> val sumMore = 1.+(2)
val sumMore: Int = 3
Фактически в классе
Int содержится несколько перегруженных методов
+
, получающих различные типы параметров
1
. Например, у
Int есть еще один метод, тоже по имени
+
, который получает и возвращает значения типа
Long
При сложении
Long и
Int будет вызван именно этот альтернативный метод:
scala> val longSum = 1 + 2L // Scala вызывает 1.+(2L)
val longSum: Long = 3
Символ
+
— оператор, точнее, инфиксный оператор. Форма записи опера
торов не ограничивается методами, подобными
+
, которые в других языках выглядят как операторы. Любой метод может использоваться в нотации операторов, если он принимает только один параметр
2
. Например, в классе
String есть метод indexOf
, получающий один параметр типа
Char
. Метод indexOf ведет поиск первого появления в строке указанного символа и воз
1
Перегруженные методы имеют точно такие же имена, но используют другие типы аргументов. Более подробно перегрузка методов рассматривается в разделе 6.11.
2
В будущих версиях Scala методы с несимвольными именами будут разрешены в качестве операторов только в том случае, если они объявлены с модификато
ром infix
5 .4 . Все операторы являются методами 113
вращает его индекс или
–1
, если символ найден не будет. Метод indexOf можно использовать как оператор:
scala> val s = "Hello, world!"
val s: String = Hello, world!
scala> s indexOf 'o' // Scala вызывает s.indexOf('o')
val res0: Int = 4
Любой однопараметрический метод может быть оператором
В Scala операторы не относятся к специальному синтаксису языка.
Любой метод, который содержит один параметр, может быть операто
ром. Однопараметрический метод становится оператором в зависимо
сти от того, как вы его используете. Если вы напишете s.indexOf('o')
, то indexOf не будет являться оператором, но станет им, если запись будет иметь вид формы оператора — s
indexOf
'o'
До сих пор рассматривались только примеры инфиксной формы записи операторов, означающей, что вызываемый метод находится между объектом и параметром или параметрами, которые нужно передать методу, как в вы
ражении
7
+
2
. В Scala также имеются две другие формы записи операторов: префиксная и постфиксная. В префиксной форме записи имя метода ста
вится перед объектом, в отношении которого вызывается этот метод (напри
мер,
–
в выражении
–7
). В постфиксной форме имя метода ставится после объекта (например, toLong в выражении
7
toLong
).
В отличие от инфиксной формы записи, в которой операторы получают два операнда (один слева, другой справа), префиксные и постфиксные операторы являются унарными — получают только один операнд. В префиксной форме записи операнд размещается справа от оператора. В качестве примеров мож
но привести выражения
–2.0
,
!found и
0xFF
. Как и в случае использования инфиксных операторов, эти префиксные операторы являются сокращенной формой вызова методов. Но в данном случае перед символом оператора в име
ни метода ставится приставка unary_
. Например, Scala превратит выражение
–2.0
в вызов метода
(2.0).unary_–
. Вы можете убедиться в этом, набрав вызов метода как с использованием формы записи операторов, так и в явном виде:
scala> -2.0 // Scala вызывает (2.0).unary_- val res2: Double = -2.0
scala> (2.0).unary_- val res3: Double = -2.0
114 Глава 5 • Основные типы и операции
Идентификаторами, которые могут служить в качестве префиксных операто
ров, являются только
+
,
–
,
!
и
. Следовательно, если вы определите метод по имени unary_!
, то сможете вызвать его в отношении значения или переменной подходящего типа, прибегнув к префиксной форме записи операторов, напри
мер
!p
. Но, определив метод по имени unary_*
, вы не сможете использовать префиксную форму записи операторов, поскольку
*
не входит в число четы
рех идентификаторов, которые могут использоваться в качестве префиксных операторов. Метод можно вызвать обычным способом как p.unary_*
, но при попытке вызвать его в виде
*p
Scala воспримет код так, словно он записан в виде
*.p
, что, вероятно, совершенно не совпадает с задуманным
1
!
Постфиксные операторы, будучи вызванными без точки или круглых ско
бок, являются методами, не получающими аргументов. В Scala при вызове метода пустые круглые скобки можно не ставить. Соглашение гласит, что круглые скобки ставятся, если метод имеет побочные эффекты, как в случае с методом println()
. Но их можно не ставить, если метод не имеет побочных эффектов, как в случае с методом toLowerCase
, вызываемым в отношении значения типа
String
:
scala> val s = "Hello, world!"
val s: String = Hello, world!
scala> s.toLowerCase val res4: String = hello, world!
В последнем случае, где методу не требуются аргументы, можно при желании не ставить точку и воспользоваться постфиксной формой записи операто
ров. Однако компилятор потребует, чтобы вы импортировали scala.lan- guage.postfixOps
, прежде чем вызывать метод:
scala> import scala.language.postfixOps scala> s toLowerCase val res5: String = hello, world!
Здесь метод toLowerCase используется в качестве постфиксного оператора в отношении операнда s
Чтобы понять, какие операторы можно использовать с основными типами
Scala, нужно посмотреть на методы, объявленные в классах типов, в доку
ментации по Scala API. Но данная книга — пособие по языку Scala, поэтому в нескольких следующих разделах будет представлен краткий обзор боль
шинства этих методов.
1
Однако не обязательно все будет потеряно. Есть весьма незначительная вероят
ность того, что программа с кодом
*p может скомпилироваться как код C++.
5 .5 . Арифметические операции 115
УСКОРЕННЫЙ РЕЖИМ ЧТЕНИЯ ДЛЯ JAVA-ПРОГРАММИСТОВ
Многие аспекты Scala, рассматриваемые в оставшейся части главы, со- впадают с аналогичными Java-аспектами . Если вы хорошо разбираетесь в Java и у вас мало времени, то можете спокойно перейти к разделу 5 .8, в котором рассматриваются отличия Scala от Java в области равенства объектов .
5 .5 . Арифметические операции
Арифметические методы при работе с любыми числовыми типами можно вызвать в инфиксной форме для сложения (
+
), вычитания (
–
), умноже
ния (
*
), деления (
/
) и получения остатка от деления (
%
). Вот несколько примеров:
1.2 + 2.3 // 3.5: Double
3 — 1 // 2: Int
'b' — 'a' // 1: Int
2L * 3L // 6: Long
11 / 4 // 2: Int
11 % 4 // 3: Int
11.0f / 4.0f // 2.75: Float
11.0 % 4.0 // 3.0: Double
Когда целочисленными типами являются как правый, так и левый операнды
(
Int
,
Long
,
Byte
,
Short или
Char
), оператор
/
выведет всю числовую часть результата деления, исключая остаток. Оператор
%
показывает остаток от предполагаемого целочисленного деления.
Остаток от деления числа с плавающей точкой, полученный с помощью метода
%
, не определен в стандарте IEEE 754. Что касается операции вы
числения остатка, в этом стандарте используется деление с округлением, а не деление с отбрасыванием остатка. Поэтому данная операция сильно от
личается от операции вычисления остатка от целочисленного деления. Если всетаки нужно получить остаток по стандарту IEEE 754, то можно вызвать метод
IEEEremainder из scala.math
:
math.IEEEremainder(11.0, 4.0) // -1.0: Double
Числовые типы также предлагают прибегнуть к унарным префиксным операторам
+
(метод unary_+
) и
–
(метод unary_–
), позволяющим показать положительное или отрицательное значение числового литерала, как в
–3
или
+4.0
. Если не указать унарный
+
или
–
, то литерал интерпретируется как положительный. Унарный
+
существует исключительно для симметрии с унарным
–
, однако не производит никаких действий. Унарный
–
может
116 Глава 5 • Основные типы и операции также использоваться для смены знака переменной. Вот несколько при
меров:
val neg = 1 + -3 // -2 : Neg val y = +3 // 3: Int
-neg // 2: Int
5 .6 . Отношения и логические операции
Числовые типы можно сравнивать с помощью методов отношений «боль
ше» (
>
), «меньше» (
<
), «больше или равно» (
>=
) и «меньше или равно» (
<=
), которые выдают в качестве результата булево значение. Дополнительно, чтобы инвертировать булево значение, можно использовать унарный опе
ратор
!
(метод unary_!
). Вот несколько примеров:
1 > 2 // false: Boolean
1 < 2 // true: Boolean
1.0 <= 1.0 // true: Boolean
3.5f >= 3.6f // false: Boolean
'a' >= 'A' // true: Boolean val untrue = !true // false: Boolean
Методы «логическое И» (
&&
и
&
) и «логическое ИЛИ» (
||
и
|
) получают операнды типа
Boolean в инфиксной нотации и выдают результат в виде
Boolean
значения, например:
val toBe = true // true: Boolean val question = toBe || !toBe // true: Boolean val paradox = toBe && !toBe // false: Boolean
Операции
&&
и
||
, как и в Java, — сокращенно вычисляемые: выражения, по
строенные с помощью этих операторов, вычисляются, только когда это нуж
но для определения результата. Иными словами, правая часть выражений с использованием
&&
и
||
не будет вычисляться, если результат уже опреде
лился при вычислении левой части. Например, если левая часть выражения с методом
&&
вычисляется в false
, то результатом выражения, несомненно, будет false
, поэтому правая часть не вычисляется. Аналогично этому если левая часть выражения с методом
||
вычисляется в true
, то результатом выражения конечно же будет true
, поэтому правая часть не вычисляется:
scala> def salt() = { println("salt"); false }
def salt(): Boolean scala> def pepper() = { println("pepper"); true }
def pepper(): Boolean
5 .7 . Поразрядные операции 117
scala> pepper() && salt()
pepper salt val res21: Boolean = false scala> salt() && pepper()
salt val res22: Boolean = false
В первом выражении вызываются pepper и salt
, но во втором вызывается только salt
. Поскольку salt возвращает false
, то необходимость в вызове pepper отпадает.
Если правую часть нужно вычислить при любых условиях, то вместо пока
занных выше методов следует обратиться к методам
&
и
|
. Первый выполняет логическую операцию И, а второй — операцию ИЛИ, но при этом они не прибегают к сокращенному вычислению, как это делают методы
&&
и
||
. Вот как выглядит пример их использования:
scala> salt() & pepper()
salt pepper val res23: Boolean = false
ПРИМЕЧАНИЕ
Возникает вопрос: как сокращенное вычисление может работать, если операторы — всего лишь методы? Обычно все аргументы вычисляются перед входом в метод, тогда каким же образом метод может избежать вы- числения своего второго аргумента? Дело в том, что у всех методов Scala есть средство для задержки вычисления его аргументов или даже полной его отмены . Оно называется «параметр, передаваемый по имени» и будет рассмотрено в разделе 9 .5 .
5 .7 . Поразрядные операции
Scala позволяет выполнять операции над отдельными разрядами целочис
ленных типов, используя несколько поразрядных методов. К таким методам относятся поразрядное И (
&
), поразрядное ИЛИ (
|
) и поразрядное исклю
чающее ИЛИ (
^
)
1
. Унарный поразрядный оператор дополнения (
, метод unary_
) инвертирует каждый разряд в своем операнде, например:
1
Метод поразрядного исключающего ИЛИ выполняет соответствующую опера
цию в отношении своих операндов. Из одинаковых разрядов получается
0
, а из разных
-1
. Следовательно, выражение
0011
^
0101
вычисляется в
0110
118 Глава 5 • Основные типы и операции
1 & 2 // 0: Int
1 | 2 // 3: Int
1 ^ 3 // 2: Int
1 // -2: Int
В первом выражении,
1
&
2
, выполняется поразрядное И над каждым раз
рядом чисел
1
(
0001
) и
2
(
0010
) и выдается результат
0
(
0000
). Во втором выражении,
1
|
2
, выполняется поразрядное ИЛИ над теми же операндами и выдается результат
3
(
0011
). В третьем выражении,
1
^
3
, выполняется поразрядное исключающее ИЛИ над каждым разрядом
1
(
0001
) и
3
(
0011
) и выдается результат
2
(
0010
). В последнем выражении,
1
, инвертируется каждый разряд в
1
(
0001
) и выдается результат
–2
, который в двоичной форме выглядит как
11111111111111111111111111111110
Целочисленные типы Scala также предлагают три метода сдвига: влево (
<<
), вправо (
>>
) и беззнаковый сдвиг вправо (
>>>
). Методы сдвига, примененные в инфиксной форме записи операторов, сдвигают разряды целочисленного значения, указанные слева от оператора, на количество разрядов, указанное в целочисленном значении справа от оператора. При сдвиге влево и беззнако
вом сдвиге вправо разряды по мере сдвига заполняются нулями. При сдвиге вправо разряды указанного слева значения по мере сдвига заполняются зна
чением самого старшего разряда (разряда знака). Вот несколько примеров:
-1 >> 31 // -1: Int
-1 >>> 31 // 1: Int
1 << 2 // 4: Int
Число
–1
в двоичном виде выглядит как
11111111111111111111111111111111
В первом примере,
–1
>>
31
, в числе
–1
происходит сдвиг вправо на 31 разряд
ную позицию. В значении типа
Int содержатся 32 разряда, поэтому данная операция, по сути, перемещает самый левый разряд до тех пор, пока тот не станет самым правым
1
. Поскольку метод
>>
выполняет заполнение по мере сдвига единицами ввиду того, что самый левый разряд числа
–1
— это
1
, результат получается идентичным исходному левому операнду и состоит из 32 единичных разрядов, или равняется
–1
. Во втором примере,
–1
>>>
31
, самый левый разряд опять сдвигается вправо в самую правую позицию, од
нако на сей раз освобождающиеся разряды заполняются нулями. Поэтому результат в двоичном виде получается
00000000000000000000000000000001
, или
1
. В последнем примере,
1
<<
2
, левый операнд,
1
, сдвигается влево на две позиции (освобождающиеся позиции заполняются нулями), в результате
1
Самый левый разряд в целочисленном типе является знаковым. Если самый левый разряд установлен в 1, значит, число отрицательное, если в 0, то положительное.
5 .8 . Равенство объектов
1 ... 9 10 11 12 13 14 15 16 ... 64
110 Глава 5 • Основные типы и операции
Проблема во включении в строку пробелов перед второй строкой текста!
Чтобы справиться с этой весьма часто возникающей ситуацией, вы можете вызывать в отношении строк метод stripMargin
. Чтобы им воспользоваться, поставьте символ вертикальной черты (
|
) перед каждой строкой текста, а за
тем в отношении всей строки вызовите метод stripMargin
:
println("""|Welcome to Ultamix 3000.
|Type "HELP" for help.""".stripMargin)
Вот теперь код ведет себя подобающим образом:
Welcome to Ultamix 3000.
Type "HELP" for help.
Булевы литералы
У типа
Boolean имеется два литерала, true и false
:
val bool = true // true: Boolean val fool = false // false: Boolean
Вот, собственно, и все. Теперь вы буквально (или литерально) стали боль
шим специалистом по Scala.
5 .3 . Интерполяция строк
В Scala включен довольно гибкий механизм для интерполяции строк, позволяющий вставлять выражения в строковые литералы. В самом рас
пространенном случае использования этот механизм предоставляет лако
ничную и удобочитаемую альтернативу конкатенации строк. Рассмотрим пример:
val name = "reader"
println(s"Hello, $name!")
Выражение s"Hello,
$name!"
— обрабатываемый строковый литерал. По
скольку за буквой s
стоят открывающие кавычки, то Scala для обработки литерала воспользуется интерполятором строк s
. Он станет вычислять каждое встроенное выражение, вызывая в отношении каждого результата метод toString и заменяя встроенные выражения в литерале этими резуль
татами. Таким образом, из s"Hello,
$name!"
получится "Hello,
reader!"
,
5 .3 . Интерполяция строк 111
то есть точно такой же результат, как при использовании кода "Hello,
"
+
name
+
"!"
После знака доллара (
$
) в обрабатываемом строковом литерале можно ука
зать любое выражение. Для выражений с одной переменной зачастую можно просто поместить после знака доллара имя этой переменной. Все символы, вплоть до первого символа, не относящегося к идентификатору, Scala будет интерпретировать как выражение. Если в него включены символы, не явля
ющиеся идентификаторами, то это выражение следует заключить в фигур
ные скобки, а открывающая фигурная скобка должна ставиться сразу же после знака доллара, например:
scala> s"The answer is ${6 * 7}."
val res0: String = The answer is 42.
Scala содержит еще два интерполятора строк: raw и f
. Интерполятор строк raw ведет себя практически так же, как и s
, за исключением того, что не рас
познает управляющие последовательности символьных литералов (те самые, которые показаны в табл. 5.2). Например, следующая инструкция выводит четыре, а не два обратных слеша:
println(raw"No\\\\escape!") // выводит: No\\\\escape!
Интерполятор строк f
позволяет прикреплять к встроенным выражениям инструкции форматирования в стиле функции printf
. Инструкции ставятся после выражения и начинаются со знака процента (
%
), при этом используется синтаксис, заданный классом java.util.Formatter
. Например, вот как можно было бы отформатировать число
π:
scala> f"${math.Pi}%.5f"
val res1: String = 3.14159
Если для встроенного выражения не указать никаких инструкций форма
тирования, то интерполятор строк f
по умолчанию превратится в
%s
, что означает подстановку значения, полученного в результате выполнения ме
тода toString
, точно так же, как это делает интерполятор строк s
, например:
scala> val pi = "Pi"
val pi: String = Pi scala> f"$pi is approximately ${math.Pi}%.8f."
val res2: String = Pi is approximately 3.14159265.
В Scala интерполяция строк реализуется перезаписью кода в ходе ком
пиляции. Компилятор в качестве выражения интерполятора строк будет
112 Глава 5 • Основные типы и операции рассматривать любое выражение, состоящее из идентификатора, за которым сразу же стоят открывающие двойные кавычки строкового литерала. Интер
поляторы строк s
, f
и raw реализуются с помощью этого общего механизма.
Библиотеки и пользователи могут определять другие интерполяторы строк, применяемые в иных целях.
5 .4 . Все операторы являются методами
Для основных типов Scala предоставляет весьма богатый набор операторов.
Как упоминалось в предыдущих главах, эти операторы — всего лишь прият
ный синтаксис для обычных вызовов методов. Например,
1
+
2
означает то же самое, что и
1.+(2)
. Иными словами, в классе
Int имеется метод по имени
+
, который получает
Int
значение и возвращает
Int
результат. Он вызывается при сложении двух
Int
значений:
val sum = 1 + 2 // Scala вызывает 1.+(2)
Чтобы убедиться в этом, можете набрать выражение, в точности соответ
ствующее вызову метода:
scala> val sumMore = 1.+(2)
val sumMore: Int = 3
Фактически в классе
Int содержится несколько перегруженных методов
+
, получающих различные типы параметров
1
. Например, у
Int есть еще один метод, тоже по имени
+
, который получает и возвращает значения типа
Long
При сложении
Long и
Int будет вызван именно этот альтернативный метод:
scala> val longSum = 1 + 2L // Scala вызывает 1.+(2L)
val longSum: Long = 3
Символ
+
— оператор, точнее, инфиксный оператор. Форма записи опера
торов не ограничивается методами, подобными
+
, которые в других языках выглядят как операторы. Любой метод может использоваться в нотации операторов, если он принимает только один параметр
2
. Например, в классе
String есть метод indexOf
, получающий один параметр типа
Char
. Метод indexOf ведет поиск первого появления в строке указанного символа и воз
1
Перегруженные методы имеют точно такие же имена, но используют другие типы аргументов. Более подробно перегрузка методов рассматривается в разделе 6.11.
2
В будущих версиях Scala методы с несимвольными именами будут разрешены в качестве операторов только в том случае, если они объявлены с модификато
ром infix
5 .4 . Все операторы являются методами 113
вращает его индекс или
–1
, если символ найден не будет. Метод indexOf можно использовать как оператор:
scala> val s = "Hello, world!"
val s: String = Hello, world!
scala> s indexOf 'o' // Scala вызывает s.indexOf('o')
val res0: Int = 4
Любой однопараметрический метод может быть оператором
В Scala операторы не относятся к специальному синтаксису языка.
Любой метод, который содержит один параметр, может быть операто
ром. Однопараметрический метод становится оператором в зависимо
сти от того, как вы его используете. Если вы напишете s.indexOf('o')
, то indexOf не будет являться оператором, но станет им, если запись будет иметь вид формы оператора — s
indexOf
'o'
До сих пор рассматривались только примеры инфиксной формы записи операторов, означающей, что вызываемый метод находится между объектом и параметром или параметрами, которые нужно передать методу, как в вы
ражении
7
+
2
. В Scala также имеются две другие формы записи операторов: префиксная и постфиксная. В префиксной форме записи имя метода ста
вится перед объектом, в отношении которого вызывается этот метод (напри
мер,
–
в выражении
–7
). В постфиксной форме имя метода ставится после объекта (например, toLong в выражении
7
toLong
).
В отличие от инфиксной формы записи, в которой операторы получают два операнда (один слева, другой справа), префиксные и постфиксные операторы являются унарными — получают только один операнд. В префиксной форме записи операнд размещается справа от оператора. В качестве примеров мож
но привести выражения
–2.0
,
!found и
0xFF
. Как и в случае использования инфиксных операторов, эти префиксные операторы являются сокращенной формой вызова методов. Но в данном случае перед символом оператора в име
ни метода ставится приставка unary_
. Например, Scala превратит выражение
–2.0
в вызов метода
(2.0).unary_–
. Вы можете убедиться в этом, набрав вызов метода как с использованием формы записи операторов, так и в явном виде:
scala> -2.0 // Scala вызывает (2.0).unary_- val res2: Double = -2.0
scala> (2.0).unary_- val res3: Double = -2.0
114 Глава 5 • Основные типы и операции
Идентификаторами, которые могут служить в качестве префиксных операто
ров, являются только
+
,
–
,
!
и
. Следовательно, если вы определите метод по имени unary_!
, то сможете вызвать его в отношении значения или переменной подходящего типа, прибегнув к префиксной форме записи операторов, напри
мер
!p
. Но, определив метод по имени unary_*
, вы не сможете использовать префиксную форму записи операторов, поскольку
*
не входит в число четы
рех идентификаторов, которые могут использоваться в качестве префиксных операторов. Метод можно вызвать обычным способом как p.unary_*
, но при попытке вызвать его в виде
*p
Scala воспримет код так, словно он записан в виде
*.p
, что, вероятно, совершенно не совпадает с задуманным
1
!
Постфиксные операторы, будучи вызванными без точки или круглых ско
бок, являются методами, не получающими аргументов. В Scala при вызове метода пустые круглые скобки можно не ставить. Соглашение гласит, что круглые скобки ставятся, если метод имеет побочные эффекты, как в случае с методом println()
. Но их можно не ставить, если метод не имеет побочных эффектов, как в случае с методом toLowerCase
, вызываемым в отношении значения типа
String
:
scala> val s = "Hello, world!"
val s: String = Hello, world!
scala> s.toLowerCase val res4: String = hello, world!
В последнем случае, где методу не требуются аргументы, можно при желании не ставить точку и воспользоваться постфиксной формой записи операто
ров. Однако компилятор потребует, чтобы вы импортировали scala.lan- guage.postfixOps
, прежде чем вызывать метод:
scala> import scala.language.postfixOps scala> s toLowerCase val res5: String = hello, world!
Здесь метод toLowerCase используется в качестве постфиксного оператора в отношении операнда s
Чтобы понять, какие операторы можно использовать с основными типами
Scala, нужно посмотреть на методы, объявленные в классах типов, в доку
ментации по Scala API. Но данная книга — пособие по языку Scala, поэтому в нескольких следующих разделах будет представлен краткий обзор боль
шинства этих методов.
1
Однако не обязательно все будет потеряно. Есть весьма незначительная вероят
ность того, что программа с кодом
*p может скомпилироваться как код C++.
5 .5 . Арифметические операции 115
УСКОРЕННЫЙ РЕЖИМ ЧТЕНИЯ ДЛЯ JAVA-ПРОГРАММИСТОВ
Многие аспекты Scala, рассматриваемые в оставшейся части главы, со- впадают с аналогичными Java-аспектами . Если вы хорошо разбираетесь в Java и у вас мало времени, то можете спокойно перейти к разделу 5 .8, в котором рассматриваются отличия Scala от Java в области равенства объектов .
5 .5 . Арифметические операции
Арифметические методы при работе с любыми числовыми типами можно вызвать в инфиксной форме для сложения (
+
), вычитания (
–
), умноже
ния (
*
), деления (
/
) и получения остатка от деления (
%
). Вот несколько примеров:
1.2 + 2.3 // 3.5: Double
3 — 1 // 2: Int
'b' — 'a' // 1: Int
2L * 3L // 6: Long
11 / 4 // 2: Int
11 % 4 // 3: Int
11.0f / 4.0f // 2.75: Float
11.0 % 4.0 // 3.0: Double
Когда целочисленными типами являются как правый, так и левый операнды
(
Int
,
Long
,
Byte
,
Short или
Char
), оператор
/
выведет всю числовую часть результата деления, исключая остаток. Оператор
%
показывает остаток от предполагаемого целочисленного деления.
Остаток от деления числа с плавающей точкой, полученный с помощью метода
%
, не определен в стандарте IEEE 754. Что касается операции вы
числения остатка, в этом стандарте используется деление с округлением, а не деление с отбрасыванием остатка. Поэтому данная операция сильно от
личается от операции вычисления остатка от целочисленного деления. Если всетаки нужно получить остаток по стандарту IEEE 754, то можно вызвать метод
IEEEremainder из scala.math
:
math.IEEEremainder(11.0, 4.0) // -1.0: Double
Числовые типы также предлагают прибегнуть к унарным префиксным операторам
+
(метод unary_+
) и
–
(метод unary_–
), позволяющим показать положительное или отрицательное значение числового литерала, как в
–3
или
+4.0
. Если не указать унарный
+
или
–
, то литерал интерпретируется как положительный. Унарный
+
существует исключительно для симметрии с унарным
–
, однако не производит никаких действий. Унарный
–
может
116 Глава 5 • Основные типы и операции также использоваться для смены знака переменной. Вот несколько при
меров:
val neg = 1 + -3 // -2 : Neg val y = +3 // 3: Int
-neg // 2: Int
5 .6 . Отношения и логические операции
Числовые типы можно сравнивать с помощью методов отношений «боль
ше» (
>
), «меньше» (
<
), «больше или равно» (
>=
) и «меньше или равно» (
<=
), которые выдают в качестве результата булево значение. Дополнительно, чтобы инвертировать булево значение, можно использовать унарный опе
ратор
!
(метод unary_!
). Вот несколько примеров:
1 > 2 // false: Boolean
1 < 2 // true: Boolean
1.0 <= 1.0 // true: Boolean
3.5f >= 3.6f // false: Boolean
'a' >= 'A' // true: Boolean val untrue = !true // false: Boolean
Методы «логическое И» (
&&
и
&
) и «логическое ИЛИ» (
||
и
|
) получают операнды типа
Boolean в инфиксной нотации и выдают результат в виде
Boolean
значения, например:
val toBe = true // true: Boolean val question = toBe || !toBe // true: Boolean val paradox = toBe && !toBe // false: Boolean
Операции
&&
и
||
, как и в Java, — сокращенно вычисляемые: выражения, по
строенные с помощью этих операторов, вычисляются, только когда это нуж
но для определения результата. Иными словами, правая часть выражений с использованием
&&
и
||
не будет вычисляться, если результат уже опреде
лился при вычислении левой части. Например, если левая часть выражения с методом
&&
вычисляется в false
, то результатом выражения, несомненно, будет false
, поэтому правая часть не вычисляется. Аналогично этому если левая часть выражения с методом
||
вычисляется в true
, то результатом выражения конечно же будет true
, поэтому правая часть не вычисляется:
scala> def salt() = { println("salt"); false }
def salt(): Boolean scala> def pepper() = { println("pepper"); true }
def pepper(): Boolean
5 .7 . Поразрядные операции 117
scala> pepper() && salt()
pepper salt val res21: Boolean = false scala> salt() && pepper()
salt val res22: Boolean = false
В первом выражении вызываются pepper и salt
, но во втором вызывается только salt
. Поскольку salt возвращает false
, то необходимость в вызове pepper отпадает.
Если правую часть нужно вычислить при любых условиях, то вместо пока
занных выше методов следует обратиться к методам
&
и
|
. Первый выполняет логическую операцию И, а второй — операцию ИЛИ, но при этом они не прибегают к сокращенному вычислению, как это делают методы
&&
и
||
. Вот как выглядит пример их использования:
scala> salt() & pepper()
salt pepper val res23: Boolean = false
ПРИМЕЧАНИЕ
Возникает вопрос: как сокращенное вычисление может работать, если операторы — всего лишь методы? Обычно все аргументы вычисляются перед входом в метод, тогда каким же образом метод может избежать вы- числения своего второго аргумента? Дело в том, что у всех методов Scala есть средство для задержки вычисления его аргументов или даже полной его отмены . Оно называется «параметр, передаваемый по имени» и будет рассмотрено в разделе 9 .5 .
5 .7 . Поразрядные операции
Scala позволяет выполнять операции над отдельными разрядами целочис
ленных типов, используя несколько поразрядных методов. К таким методам относятся поразрядное И (
&
), поразрядное ИЛИ (
|
) и поразрядное исклю
чающее ИЛИ (
^
)
1
. Унарный поразрядный оператор дополнения (
, метод unary_
) инвертирует каждый разряд в своем операнде, например:
1
Метод поразрядного исключающего ИЛИ выполняет соответствующую опера
цию в отношении своих операндов. Из одинаковых разрядов получается
0
, а из разных
-1
. Следовательно, выражение
0011
^
0101
вычисляется в
0110
118 Глава 5 • Основные типы и операции
1 & 2 // 0: Int
1 | 2 // 3: Int
1 ^ 3 // 2: Int
1 // -2: Int
В первом выражении,
1
&
2
, выполняется поразрядное И над каждым раз
рядом чисел
1
(
0001
) и
2
(
0010
) и выдается результат
0
(
0000
). Во втором выражении,
1
|
2
, выполняется поразрядное ИЛИ над теми же операндами и выдается результат
3
(
0011
). В третьем выражении,
1
^
3
, выполняется поразрядное исключающее ИЛИ над каждым разрядом
1
(
0001
) и
3
(
0011
) и выдается результат
2
(
0010
). В последнем выражении,
1
, инвертируется каждый разряд в
1
(
0001
) и выдается результат
–2
, который в двоичной форме выглядит как
11111111111111111111111111111110
Целочисленные типы Scala также предлагают три метода сдвига: влево (
<<
), вправо (
>>
) и беззнаковый сдвиг вправо (
>>>
). Методы сдвига, примененные в инфиксной форме записи операторов, сдвигают разряды целочисленного значения, указанные слева от оператора, на количество разрядов, указанное в целочисленном значении справа от оператора. При сдвиге влево и беззнако
вом сдвиге вправо разряды по мере сдвига заполняются нулями. При сдвиге вправо разряды указанного слева значения по мере сдвига заполняются зна
чением самого старшего разряда (разряда знака). Вот несколько примеров:
-1 >> 31 // -1: Int
-1 >>> 31 // 1: Int
1 << 2 // 4: Int
Число
–1
в двоичном виде выглядит как
11111111111111111111111111111111
В первом примере,
–1
>>
31
, в числе
–1
происходит сдвиг вправо на 31 разряд
ную позицию. В значении типа
Int содержатся 32 разряда, поэтому данная операция, по сути, перемещает самый левый разряд до тех пор, пока тот не станет самым правым
1
. Поскольку метод
>>
выполняет заполнение по мере сдвига единицами ввиду того, что самый левый разряд числа
–1
— это
1
, результат получается идентичным исходному левому операнду и состоит из 32 единичных разрядов, или равняется
–1
. Во втором примере,
–1
>>>
31
, самый левый разряд опять сдвигается вправо в самую правую позицию, од
нако на сей раз освобождающиеся разряды заполняются нулями. Поэтому результат в двоичном виде получается
00000000000000000000000000000001
, или
1
. В последнем примере,
1
<<
2
, левый операнд,
1
, сдвигается влево на две позиции (освобождающиеся позиции заполняются нулями), в результате
1
Самый левый разряд в целочисленном типе является знаковым. Если самый левый разряд установлен в 1, значит, число отрицательное, если в 0, то положительное.
5 .8 . Равенство объектов
1 ... 9 10 11 12 13 14 15 16 ... 64
Проблема во включении в строку пробелов перед второй строкой текста!
Чтобы справиться с этой весьма часто возникающей ситуацией, вы можете вызывать в отношении строк метод stripMargin
. Чтобы им воспользоваться, поставьте символ вертикальной черты (
|
) перед каждой строкой текста, а за
тем в отношении всей строки вызовите метод stripMargin
:
println("""|Welcome to Ultamix 3000.
|Type "HELP" for help.""".stripMargin)
Вот теперь код ведет себя подобающим образом:
Welcome to Ultamix 3000.
Type "HELP" for help.
Булевы литералы
У типа
Boolean имеется два литерала, true и false
:
val bool = true // true: Boolean val fool = false // false: Boolean
Вот, собственно, и все. Теперь вы буквально (или литерально) стали боль
шим специалистом по Scala.
5 .3 . Интерполяция строк
В Scala включен довольно гибкий механизм для интерполяции строк, позволяющий вставлять выражения в строковые литералы. В самом рас
пространенном случае использования этот механизм предоставляет лако
ничную и удобочитаемую альтернативу конкатенации строк. Рассмотрим пример:
val name = "reader"
println(s"Hello, $name!")
Выражение s"Hello,
$name!"
— обрабатываемый строковый литерал. По
скольку за буквой s
стоят открывающие кавычки, то Scala для обработки литерала воспользуется интерполятором строк s
. Он станет вычислять каждое встроенное выражение, вызывая в отношении каждого результата метод toString и заменяя встроенные выражения в литерале этими резуль
татами. Таким образом, из s"Hello,
$name!"
получится "Hello,
reader!"
,
5 .3 . Интерполяция строк 111
то есть точно такой же результат, как при использовании кода "Hello,
"
+
name
+
"!"
После знака доллара (
$
) в обрабатываемом строковом литерале можно ука
зать любое выражение. Для выражений с одной переменной зачастую можно просто поместить после знака доллара имя этой переменной. Все символы, вплоть до первого символа, не относящегося к идентификатору, Scala будет интерпретировать как выражение. Если в него включены символы, не явля
ющиеся идентификаторами, то это выражение следует заключить в фигур
ные скобки, а открывающая фигурная скобка должна ставиться сразу же после знака доллара, например:
scala> s"The answer is ${6 * 7}."
val res0: String = The answer is 42.
Scala содержит еще два интерполятора строк: raw и f
. Интерполятор строк raw ведет себя практически так же, как и s
, за исключением того, что не рас
познает управляющие последовательности символьных литералов (те самые, которые показаны в табл. 5.2). Например, следующая инструкция выводит четыре, а не два обратных слеша:
println(raw"No\\\\escape!") // выводит: No\\\\escape!
Интерполятор строк f
позволяет прикреплять к встроенным выражениям инструкции форматирования в стиле функции printf
. Инструкции ставятся после выражения и начинаются со знака процента (
%
), при этом используется синтаксис, заданный классом java.util.Formatter
. Например, вот как можно было бы отформатировать число
π:
scala> f"${math.Pi}%.5f"
val res1: String = 3.14159
Если для встроенного выражения не указать никаких инструкций форма
тирования, то интерполятор строк f
по умолчанию превратится в
%s
, что означает подстановку значения, полученного в результате выполнения ме
тода toString
, точно так же, как это делает интерполятор строк s
, например:
scala> val pi = "Pi"
val pi: String = Pi scala> f"$pi is approximately ${math.Pi}%.8f."
val res2: String = Pi is approximately 3.14159265.
В Scala интерполяция строк реализуется перезаписью кода в ходе ком
пиляции. Компилятор в качестве выражения интерполятора строк будет
112 Глава 5 • Основные типы и операции рассматривать любое выражение, состоящее из идентификатора, за которым сразу же стоят открывающие двойные кавычки строкового литерала. Интер
поляторы строк s
, f
и raw реализуются с помощью этого общего механизма.
Библиотеки и пользователи могут определять другие интерполяторы строк, применяемые в иных целях.
5 .4 . Все операторы являются методами
Для основных типов Scala предоставляет весьма богатый набор операторов.
Как упоминалось в предыдущих главах, эти операторы — всего лишь прият
ный синтаксис для обычных вызовов методов. Например,
1
+
2
означает то же самое, что и
1.+(2)
. Иными словами, в классе
Int имеется метод по имени
+
, который получает
Int
значение и возвращает
Int
результат. Он вызывается при сложении двух
Int
значений:
val sum = 1 + 2 // Scala вызывает 1.+(2)
Чтобы убедиться в этом, можете набрать выражение, в точности соответ
ствующее вызову метода:
scala> val sumMore = 1.+(2)
val sumMore: Int = 3
Фактически в классе
Int содержится несколько перегруженных методов
+
, получающих различные типы параметров
1
. Например, у
Int есть еще один метод, тоже по имени
+
, который получает и возвращает значения типа
Long
При сложении
Long и
Int будет вызван именно этот альтернативный метод:
scala> val longSum = 1 + 2L // Scala вызывает 1.+(2L)
val longSum: Long = 3
Символ
+
— оператор, точнее, инфиксный оператор. Форма записи опера
торов не ограничивается методами, подобными
+
, которые в других языках выглядят как операторы. Любой метод может использоваться в нотации операторов, если он принимает только один параметр
2
. Например, в классе
String есть метод indexOf
, получающий один параметр типа
Char
. Метод indexOf ведет поиск первого появления в строке указанного символа и воз
1
Перегруженные методы имеют точно такие же имена, но используют другие типы аргументов. Более подробно перегрузка методов рассматривается в разделе 6.11.
2
В будущих версиях Scala методы с несимвольными именами будут разрешены в качестве операторов только в том случае, если они объявлены с модификато
ром infix
5 .4 . Все операторы являются методами 113
вращает его индекс или
–1
, если символ найден не будет. Метод indexOf можно использовать как оператор:
scala> val s = "Hello, world!"
val s: String = Hello, world!
scala> s indexOf 'o' // Scala вызывает s.indexOf('o')
val res0: Int = 4
Любой однопараметрический метод может быть оператором
В Scala операторы не относятся к специальному синтаксису языка.
Любой метод, который содержит один параметр, может быть операто
ром. Однопараметрический метод становится оператором в зависимо
сти от того, как вы его используете. Если вы напишете s.indexOf('o')
, то indexOf не будет являться оператором, но станет им, если запись будет иметь вид формы оператора — s
indexOf
'o'
До сих пор рассматривались только примеры инфиксной формы записи операторов, означающей, что вызываемый метод находится между объектом и параметром или параметрами, которые нужно передать методу, как в вы
ражении
7
+
2
. В Scala также имеются две другие формы записи операторов: префиксная и постфиксная. В префиксной форме записи имя метода ста
вится перед объектом, в отношении которого вызывается этот метод (напри
мер,
–
в выражении
–7
). В постфиксной форме имя метода ставится после объекта (например, toLong в выражении
7
toLong
).
В отличие от инфиксной формы записи, в которой операторы получают два операнда (один слева, другой справа), префиксные и постфиксные операторы являются унарными — получают только один операнд. В префиксной форме записи операнд размещается справа от оператора. В качестве примеров мож
но привести выражения
–2.0
,
!found и
0xFF
. Как и в случае использования инфиксных операторов, эти префиксные операторы являются сокращенной формой вызова методов. Но в данном случае перед символом оператора в име
ни метода ставится приставка unary_
. Например, Scala превратит выражение
–2.0
в вызов метода
(2.0).unary_–
. Вы можете убедиться в этом, набрав вызов метода как с использованием формы записи операторов, так и в явном виде:
scala> -2.0 // Scala вызывает (2.0).unary_- val res2: Double = -2.0
scala> (2.0).unary_- val res3: Double = -2.0
114 Глава 5 • Основные типы и операции
Идентификаторами, которые могут служить в качестве префиксных операто
ров, являются только
+
,
–
,
!
и
. Следовательно, если вы определите метод по имени unary_!
, то сможете вызвать его в отношении значения или переменной подходящего типа, прибегнув к префиксной форме записи операторов, напри
мер
!p
. Но, определив метод по имени unary_*
, вы не сможете использовать префиксную форму записи операторов, поскольку
*
не входит в число четы
рех идентификаторов, которые могут использоваться в качестве префиксных операторов. Метод можно вызвать обычным способом как p.unary_*
, но при попытке вызвать его в виде
*p
Scala воспримет код так, словно он записан в виде
*.p
, что, вероятно, совершенно не совпадает с задуманным
1
!
Постфиксные операторы, будучи вызванными без точки или круглых ско
бок, являются методами, не получающими аргументов. В Scala при вызове метода пустые круглые скобки можно не ставить. Соглашение гласит, что круглые скобки ставятся, если метод имеет побочные эффекты, как в случае с методом println()
. Но их можно не ставить, если метод не имеет побочных эффектов, как в случае с методом toLowerCase
, вызываемым в отношении значения типа
String
:
scala> val s = "Hello, world!"
val s: String = Hello, world!
scala> s.toLowerCase val res4: String = hello, world!
В последнем случае, где методу не требуются аргументы, можно при желании не ставить точку и воспользоваться постфиксной формой записи операто
ров. Однако компилятор потребует, чтобы вы импортировали scala.lan- guage.postfixOps
, прежде чем вызывать метод:
scala> import scala.language.postfixOps scala> s toLowerCase val res5: String = hello, world!
Здесь метод toLowerCase используется в качестве постфиксного оператора в отношении операнда s
Чтобы понять, какие операторы можно использовать с основными типами
Scala, нужно посмотреть на методы, объявленные в классах типов, в доку
ментации по Scala API. Но данная книга — пособие по языку Scala, поэтому в нескольких следующих разделах будет представлен краткий обзор боль
шинства этих методов.
1
Однако не обязательно все будет потеряно. Есть весьма незначительная вероят
ность того, что программа с кодом
*p может скомпилироваться как код C++.
5 .5 . Арифметические операции 115
УСКОРЕННЫЙ РЕЖИМ ЧТЕНИЯ ДЛЯ JAVA-ПРОГРАММИСТОВ
Многие аспекты Scala, рассматриваемые в оставшейся части главы, со- впадают с аналогичными Java-аспектами . Если вы хорошо разбираетесь в Java и у вас мало времени, то можете спокойно перейти к разделу 5 .8, в котором рассматриваются отличия Scala от Java в области равенства объектов .
5 .5 . Арифметические операции
Арифметические методы при работе с любыми числовыми типами можно вызвать в инфиксной форме для сложения (
+
), вычитания (
–
), умноже
ния (
*
), деления (
/
) и получения остатка от деления (
%
). Вот несколько примеров:
1.2 + 2.3 // 3.5: Double
3 — 1 // 2: Int
'b' — 'a' // 1: Int
2L * 3L // 6: Long
11 / 4 // 2: Int
11 % 4 // 3: Int
11.0f / 4.0f // 2.75: Float
11.0 % 4.0 // 3.0: Double
Когда целочисленными типами являются как правый, так и левый операнды
(
Int
,
Long
,
Byte
,
Short или
Char
), оператор
/
выведет всю числовую часть результата деления, исключая остаток. Оператор
%
показывает остаток от предполагаемого целочисленного деления.
Остаток от деления числа с плавающей точкой, полученный с помощью метода
%
, не определен в стандарте IEEE 754. Что касается операции вы
числения остатка, в этом стандарте используется деление с округлением, а не деление с отбрасыванием остатка. Поэтому данная операция сильно от
личается от операции вычисления остатка от целочисленного деления. Если всетаки нужно получить остаток по стандарту IEEE 754, то можно вызвать метод
IEEEremainder из scala.math
:
math.IEEEremainder(11.0, 4.0) // -1.0: Double
Числовые типы также предлагают прибегнуть к унарным префиксным операторам
+
(метод unary_+
) и
–
(метод unary_–
), позволяющим показать положительное или отрицательное значение числового литерала, как в
–3
или
+4.0
. Если не указать унарный
+
или
–
, то литерал интерпретируется как положительный. Унарный
+
существует исключительно для симметрии с унарным
–
, однако не производит никаких действий. Унарный
–
может
116 Глава 5 • Основные типы и операции также использоваться для смены знака переменной. Вот несколько при
меров:
val neg = 1 + -3 // -2 : Neg val y = +3 // 3: Int
-neg // 2: Int
5 .6 . Отношения и логические операции
Числовые типы можно сравнивать с помощью методов отношений «боль
ше» (
>
), «меньше» (
<
), «больше или равно» (
>=
) и «меньше или равно» (
<=
), которые выдают в качестве результата булево значение. Дополнительно, чтобы инвертировать булево значение, можно использовать унарный опе
ратор
!
(метод unary_!
). Вот несколько примеров:
1 > 2 // false: Boolean
1 < 2 // true: Boolean
1.0 <= 1.0 // true: Boolean
3.5f >= 3.6f // false: Boolean
'a' >= 'A' // true: Boolean val untrue = !true // false: Boolean
Методы «логическое И» (
&&
и
&
) и «логическое ИЛИ» (
||
и
|
) получают операнды типа
Boolean в инфиксной нотации и выдают результат в виде
Boolean
значения, например:
val toBe = true // true: Boolean val question = toBe || !toBe // true: Boolean val paradox = toBe && !toBe // false: Boolean
Операции
&&
и
||
, как и в Java, — сокращенно вычисляемые: выражения, по
строенные с помощью этих операторов, вычисляются, только когда это нуж
но для определения результата. Иными словами, правая часть выражений с использованием
&&
и
||
не будет вычисляться, если результат уже опреде
лился при вычислении левой части. Например, если левая часть выражения с методом
&&
вычисляется в false
, то результатом выражения, несомненно, будет false
, поэтому правая часть не вычисляется. Аналогично этому если левая часть выражения с методом
||
вычисляется в true
, то результатом выражения конечно же будет true
, поэтому правая часть не вычисляется:
scala> def salt() = { println("salt"); false }
def salt(): Boolean scala> def pepper() = { println("pepper"); true }
def pepper(): Boolean
5 .7 . Поразрядные операции 117
scala> pepper() && salt()
pepper salt val res21: Boolean = false scala> salt() && pepper()
salt val res22: Boolean = false
В первом выражении вызываются pepper и salt
, но во втором вызывается только salt
. Поскольку salt возвращает false
, то необходимость в вызове pepper отпадает.
Если правую часть нужно вычислить при любых условиях, то вместо пока
занных выше методов следует обратиться к методам
&
и
|
. Первый выполняет логическую операцию И, а второй — операцию ИЛИ, но при этом они не прибегают к сокращенному вычислению, как это делают методы
&&
и
||
. Вот как выглядит пример их использования:
scala> salt() & pepper()
salt pepper val res23: Boolean = false
ПРИМЕЧАНИЕ
Возникает вопрос: как сокращенное вычисление может работать, если операторы — всего лишь методы? Обычно все аргументы вычисляются перед входом в метод, тогда каким же образом метод может избежать вы- числения своего второго аргумента? Дело в том, что у всех методов Scala есть средство для задержки вычисления его аргументов или даже полной его отмены . Оно называется «параметр, передаваемый по имени» и будет рассмотрено в разделе 9 .5 .
5 .7 . Поразрядные операции
Scala позволяет выполнять операции над отдельными разрядами целочис
ленных типов, используя несколько поразрядных методов. К таким методам относятся поразрядное И (
&
), поразрядное ИЛИ (
|
) и поразрядное исклю
чающее ИЛИ (
^
)
1
. Унарный поразрядный оператор дополнения (
, метод unary_
) инвертирует каждый разряд в своем операнде, например:
1
Метод поразрядного исключающего ИЛИ выполняет соответствующую опера
цию в отношении своих операндов. Из одинаковых разрядов получается
0
, а из разных
-1
. Следовательно, выражение
0011
^
0101
вычисляется в
0110
118 Глава 5 • Основные типы и операции
1 & 2 // 0: Int
1 | 2 // 3: Int
1 ^ 3 // 2: Int
1 // -2: Int
В первом выражении,
1
&
2
, выполняется поразрядное И над каждым раз
рядом чисел
1
(
0001
) и
2
(
0010
) и выдается результат
0
(
0000
). Во втором выражении,
1
|
2
, выполняется поразрядное ИЛИ над теми же операндами и выдается результат
3
(
0011
). В третьем выражении,
1
^
3
, выполняется поразрядное исключающее ИЛИ над каждым разрядом
1
(
0001
) и
3
(
0011
) и выдается результат
2
(
0010
). В последнем выражении,
1
, инвертируется каждый разряд в
1
(
0001
) и выдается результат
–2
, который в двоичной форме выглядит как
11111111111111111111111111111110
Целочисленные типы Scala также предлагают три метода сдвига: влево (
<<
), вправо (
>>
) и беззнаковый сдвиг вправо (
>>>
). Методы сдвига, примененные в инфиксной форме записи операторов, сдвигают разряды целочисленного значения, указанные слева от оператора, на количество разрядов, указанное в целочисленном значении справа от оператора. При сдвиге влево и беззнако
вом сдвиге вправо разряды по мере сдвига заполняются нулями. При сдвиге вправо разряды указанного слева значения по мере сдвига заполняются зна
чением самого старшего разряда (разряда знака). Вот несколько примеров:
-1 >> 31 // -1: Int
-1 >>> 31 // 1: Int
1 << 2 // 4: Int
Число
–1
в двоичном виде выглядит как
11111111111111111111111111111111
В первом примере,
–1
>>
31
, в числе
–1
происходит сдвиг вправо на 31 разряд
ную позицию. В значении типа
Int содержатся 32 разряда, поэтому данная операция, по сути, перемещает самый левый разряд до тех пор, пока тот не станет самым правым
1
. Поскольку метод
>>
выполняет заполнение по мере сдвига единицами ввиду того, что самый левый разряд числа
–1
— это
1
, результат получается идентичным исходному левому операнду и состоит из 32 единичных разрядов, или равняется
–1
. Во втором примере,
–1
>>>
31
, самый левый разряд опять сдвигается вправо в самую правую позицию, од
нако на сей раз освобождающиеся разряды заполняются нулями. Поэтому результат в двоичном виде получается
00000000000000000000000000000001
, или
1
. В последнем примере,
1
<<
2
, левый операнд,
1
, сдвигается влево на две позиции (освобождающиеся позиции заполняются нулями), в результате
1
Самый левый разряд в целочисленном типе является знаковым. Если самый левый разряд установлен в 1, значит, число отрицательное, если в 0, то положительное.
5 .8 . Равенство объектов
1 ... 9 10 11 12 13 14 15 16 ... 64
119
чего в двоичном виде получается число
00000000000000000000000000000100
, или
4 5 .8 . Равенство объектов
Если нужно сравнить два объекта на равенство, то можно воспользоваться либо методом
==
, либо его противоположностью — методом
!=
. Вот несколь
ко простых примеров:
1 == 2 // false: Boolean
1 != 2 // true: Boolean
2 == 2 // true: Boolean
По сути, эти две операции применимы ко всем объектам, а не только к ос
новным типам. Например, оператор
==
можно использовать для сравнения списков:
List(1, 2, 3) == List(1, 2, 3) // true: Boolean
List(1, 2, 3) == List(4, 5, 6) // false: Boolean
Если пойти еще дальше, то можно сравнить два объекта, имеющих разные типы:
1 == 1.0 // true: Boolean
List(1, 2, 3) == "hello" // false: Boolean
Можно даже выполнить сравнение со значением null или с тем, что может иметь данное значение. Никакие исключения при этом выдаваться не будут:
List(1, 2, 3) == null // false: Boolean null == List(1, 2, 3) // false: Boolean
Как видите, оператор
==
реализован весьма искусно, и вы в большинстве слу
чаев получите то сравнение на равенство, которое вам нужно. Все делается по очень простому правилу: сначала левая часть проверяется на null
. Если ее значение не null
, то вызывается метод equals
. Ввиду того что equals
— метод, точность получаемого сравнения зависит от типа левого аргумента. Проверка на null выполняется автоматически, поэтому вам не нужно проводить ее
1
Этот вид сравнения выдает true в отношении различных объектов, если их содержимое одинаково и их методы equals созданы на основе проверки
1
Автоматическая проверка игнорирует правую сторону, но любой корректно реализо
ванный метод equals должен возвращать false
, если его аргумент имеет значение null
120 Глава 5 • Основные типы и операции содержимого. Например, вот как сравниваются две строки, в которых по пять одинаковых букв:
("he" + "llo") == "hello" // true: Boolean
Различия операторов == в Scala и Java
В Java оператор
==
может использоваться для сравнения как при
митивных, так и ссылочных типов. В отношении примитивных ти
пов оператор
==
в Java проверяет равенство значений, как и в Scala.
Но в отношении ссылочных типов оператор
==
в Java проверяет ра-
венство ссылок. Это значит, две переменные указывают на один и тот же объект в куче, принадлежащей JVM. Scala также предоставляет средство eq для сравнения равенства ссылок. Но метод eq и его про
тивоположность, метод ne
, применяются только к объектам, которые непосредственно отображаются на объекты Java. Исчерпывающие подробности о eq и ne приводятся в разделах 17.1 и 17.2. Кроме того, в главе 8 показано, как создавать хорошие методы equals
5 .9 . Приоритет и ассоциативность операторов
Приоритет операторов определяет, какая часть выражения вычисляется самой первой. Например, выражение
2
+
2
*
7
вычисляется в
16
, а не в
28
, поскольку оператор
*
имеет более высокий приоритет, чем оператор
+
Поэтому та часть выражения, в которой требуется перемножить числа, вы
числяется до того, как будет выполнена часть, в которой числа складыва
ются. Разумеется, чтобы уточнить в выражении порядок вычисления или переопределить приоритеты, можно воспользоваться круглыми скобками.
Например, если вы действительно хотите, чтобы результат вычисления ранее показанного выражения был
28
, то можете набрать следующее вы
ражение:
(2 + 2) * 7
Если учесть, что в Scala, по сути, нет операторов, а есть только способ приме
нения методов в форме записи операторов, то возникает вопрос: а как тогда работает приоритет операторов? Scala принимает решение о приоритете на основе первого символа метода, использованного в форме записи операторов
(из этого правила есть одно исключение, рассматриваемое ниже). Если имя метода начинается, к примеру, с
*
, то он получит более высокий приоритет,
5 .9 . Приоритет и ассоциативность операторов 121
чем метод, чье имя начинается на
+
. Следовательно, выражение
2
+
2
*
7
будет вычислено как
2
+
(2
*
7)
. Аналогично этому выражение a
+++
b
***
c
, в котором a
, b
и c
— переменные, а
+++
и
***
— методы, будет вычислено как a
+++
(b
***
c)
, поскольку метод
***
обладает более высоким уровнем прио
ритета, чем метод
+++
В табл. 5.3 показан приоритет применительно к первому символу метода в убывающем порядке, где символы, показанные на одной строке, опре
деляют одинаковый уровень приоритета. Чем выше символ в списке, тем выше приоритет начинающегося с него метода. Вот пример, показывающий влияние приоритета:
2 << 2 + 2 // 32: Int
Таблица 5.3. Приоритет операторов
(Все специальные символы)
* / %
+ –
:
= !
< >
&
^
|
(Все буквы)
(Все операторы присваивания)
Имя метода
<<
начинается с символа
<
, который появляется в приведенном списке ниже символа
+
— первого и единственного символа метода
+
. Следо
вательно,
<<
будет иметь более низкий уровень приоритета, чем
+
, и выраже
ние будет вычислено путем вызова сначала метода
+
, а затем метода
<<
, как в выражении
2
<<
(2
+
2)
. При сложении
2
+
2
в результате математического действия получается
4
, а вычисление выражения
2
<<
4
дает результат
32
Если поменять операторы местами, то будет получен другой результат:
2 + 2 << 2 // 16: Int
Поскольку первые символы, по сравнению с предыдущим примером, не изменились, то методы будут вызваны в том же порядке:
+
, а затем
<<
. Сле
довательно,
2
+
2
опять будет равен
4
, а
4
<<
2
даст результат
16
122 Глава 5 • Основные типы и операции
Единственное исключение из правил, о существовании которого уже гово
рилось, относится к операторам присваивания, заканчивающимся знаком равенства. Если оператор заканчивается знаком равенства (
=
) и не относится к одному из операторов сравнения
<=
,
>=
,
==
и
!=
, то приоритет оператора имеет такой же уровень, что и простое присваивание (
=
). То есть он ниже приоритета любого другого оператора. Например:
x *= y + 1
означает то же самое, что и x *= (y + 1)
поскольку оператор
*=
классифицируется как оператор присваивания, прио
ритет которого ниже, чем у
+
, даже притом что первым символом оператора выступает знак
*
, который обозначил бы приоритет выше, чем у
+
Если в выражении рядом появляются операторы с одинаковым уровнем приоритета, то способ группировки операторов определяется их ассоциатив-
ностью. Ассоциативность оператора в Scala определяется по его последнему символу. Как уже упоминалось в главе 3, любой метод, имя которого за
канчивается символом
:
, вызывается в отношении своего правого операнда с передачей ему левого. Методы, в окончании имени которых используются любые другие символы, действуют наоборот: они вызываются в отношении своего левого операнда с передачей себе правого. То есть из выражения a
*
b получается a.*(b)
, но из a
:::
b получается b.:::(a)
Но независимо от того, какова ассоциативность оператора, его операнды всегда вычисляются слева направо. Следовательно, если a
— выражение, не являющееся простой ссылкой на неизменяемое значение, то выраже
ние a
:::
b при более точном рассмотрении представляется следующим блоком:
{ val x = a; b.:::(x) }
В этом блоке а
попрежнему вычисляется раньше b
, а затем результат данного вычисления передается в качестве операнда принадлежащему b
методу
:::
Это правило ассоциативности играет роль также при появлении в одном выражении рядом сразу нескольких операторов с одинаковым уровнем приоритета. Если имена методов заканчиваются на
:
, они группируются справа налево, в противном случае — слева направо. Например, a
:::
b
:::
c рассматривается как a
:::
(b
:::
c)
. Но a
*
b
*
c
, в отличие от этого, рассма
тривается как
(a
*
b)
*
c
5 .10 . Обогащающие операции 123
Правила приоритета операторов — часть языка Scala, и вам не следует боять
ся применять ими. При этом, чтобы прояснить первоочередность использо
вания операторов, в некоторых выражениях все же лучше прибегнуть к кру
глым скобкам. Пожалуй, единственное, на что можно реально рассчитывать в отношении знания порядка приоритета другими программистами, — то, что мультипликативные операторы
*
,
/
и
%
имеют более высокий уровень приоритета, чем аддитивные
+
и
–
. Таким образом, даже если выражение a
+
b
<<
c выдает нужный результат и без круглых скобок, стоит внести до
полнительную ясность с помощью записи
(a
+
b)
<<
c
. Это снизит количество нелестных отзывов ваших коллег по поводу использованной вами формы записи операторов, которое выражается, к примеру, в недовольном воскли
цании вроде «Опять в его коде невозможно разобраться!» и отправке вам сообщения наподобие bills
!*&ˆ%
code!
1 5 .10 . Обогащающие операции
В отношении основных типов Scala можно вызвать намного больше методов, чем рассмотрено в предыдущих разделах. Некоторые примеры показаны в табл. 5.4. Начиная со Scala 3, эти методы доступны через неявные преоб
разования — устаревшую технику, которая в конечном итоге будет заменена методами расширения, подробно описанными в главе 22. А пока вам нужно знать лишь то, что для каждого основного типа, рассмотренного в текущей
Таблица 5.4. Некоторые обогащающие операции
Код
Результат
0 max 5 5
0 min 5 0
–2.7 abs
2.7
–2.7 round
–3L
1.5 isInfinity
False
(1.0 / 0) isInfinity
True
4 to 6
Range(4, 5, 6)
"bob" capitalize
"Bob"
"robert" drop 2
"bert"
1
Теперь вы уже знаете, что, получив такой код, компилятор Scala создаст вызов
(
bills.!*&^%(code)).!
124 Глава 5 • Основные типы и операции главе, существует обогащающая оболочка, которая предоставляет ряд допол
нительных методов. Поэтому увидеть все доступные методы, применяемые в отношении основных типов, можно, обратившись к документации по API, которая касается обогащающей оболочки для каждого основного типа. Эти классы перечислены в табл. 5.5.
Таблица 5.5. Классы обогащающих оболочек
Основной тип
Обогащающая оболочка
Byte scala.runtime.RichByte
Short scala.runtime.RichShort
Int scala.runtime.RichInt
Long scala.runtime.RichLong
Char scala.runtime.RichChar
Float scala.runtime.RichFloat
Double scala.runtime.RichDouble
Boolean scala.runtime.RichBoolean
String scala.collection.immutable.StringOps
Резюме
Основное, что следует усвоить, прочитав данную главу, — операторы в Scala являются вызовами методов и для основных типов Scala существуют неяв
ные преобразования в обогащенные варианты, которые добавляют дополни
тельные полезные методы. В главе 6 мы покажем, что означает конструиро
вание объектов в функциональном стиле, обеспечивающее новые реализации некоторых операторов, рассмотренных в настоящей главе.
6
Функциональные объекты
Усвоив основы, рассмотренные в предыдущих главах, вы готовы разработать больше полнофункциональных классов Scala. В этой главе основное вни
мание мы уделим классам, определяющим функциональные объекты или объекты, не имеющие никакого изменяемого состояния. Запуская примеры, мы создадим несколько вариантов класса, моделирующего рациональные числа в виде неизменяемых объектов. Попутно будут показаны дополни
тельные аспекты объектноориентированного программирования на Scala: параметры класса и конструкторы, методы и операторы, приватные члены, переопределение, проверка соблюдения предварительных условий, пере
грузка и рекурсивные ссылки.
6 .1 . Спецификация класса Rational
Рациональным называется число, которое может быть выражено соотноше
нием n
/
d
, где n
и d
представлены целыми числами, за исключением того, что d
не может быть нулем. Здесь n
называется числителем, а d
— знаменателем.
Примерами рациональных чисел могут послужить 1/2, 2/3, 112/239 и 2/1.
В сравнении с числами с плавающей точкой рациональные числа имеют то преимущество, что дроби представлены точно, без округлений или при
ближений.
Разрабатываемый в этой главе класс должен моделировать поведение рацио
нальных чисел, позволяя производить над ними арифметические действия по сложению, вычитанию, умножению и делению. Для сложения двух ра
циональных чисел сначала нужно получить общий знаменатель, после чего сложить два числителя. Например, чтобы выполнить сложение 1/2 + 2/3,
126 Глава 6 • Функциональные объекты обе части левого операнда умножаются на 3, а обе части правого операнда — на 2, в результате чего получается 3/6 + 4/6. Сложение двух числителей дает результат 7/6. Для перемножения двух рациональных чисел можно просто перемножить их числители, а затем знаменатели. Таким образом, 1/2 · 2/5 дает число 2/10, которое можно представить более кратко в нормализованном виде как 1/5. Деление выполняется путем перестановки местами числителя и зна
менателя правого операнда с последующим перемножением чисел. Например,
1/2 / 3/5 — то же самое, что и 1/2 · 5/3, в результате получается число 5/6.
Одно, возможно, очевидное наблюдение заключается в том, что в математике рациональные числа не имеют изменяемого состояния. Можно сложить два рациональных числа, и результатом будет новое рациональное число. Исход
ные числа не будут изменены. Неизменяемый класс
Rational
, разрабатыва
емый в данной главе, будет иметь такое же свойство. Каждое рациональное число будет представлено одним объектом
Rational
. При сложении двух объ
ектов
Rational для хранения суммы будет создаваться новый объект
Rational
В этой главе мы представим некоторые допустимые в Scala способы написа
ния библиотек, которые создают впечатление, будто используется поддерж
ка, присущая непосредственно самому языку программирования. Например, в конце этой главы вы сможете сделать с классом
Rational следующее:
scala> val oneHalf = Rational(1, 2)
val oneHalf: Rational = 1/2
scala> val twoThirds = Rational(2, 3)
val twoThirds: Rational = 2/3
scala> (oneHalf / 7) + (1 — twoThirds)
val res0: Rational = 17/42 6 .2 . Конструирование класса Rational
Конструирование класса
Rational неплохо начать с рассмотрения того, как клиентыпрограммисты будут создавать новый объект
Rational
. Было ре
шено создавать объекты
Rational неизменяемыми, и потому мы потребуем, чтобы эти клиенты при создании экземпляра предоставляли все необходи
мые ему данные (в нашем случае числитель и знаменатель). Поэтому начнем конструирование со следующего кода:
class Rational(n: Int, d: Int)
По поводу этой строки кода в первую очередь следует заметить: если у класса нет тела, то вам не нужно ставить пустые фигурные скобки, а также нет необ
6 .2 . Конструирование класса Rational 127
ходимости завершать строку двоеточием. Идентификаторы n
и d
, указанные в круглых скобках после имени класса,
Rational
, называются параметрами
класса. Компилятор Scala подберет эти два параметра и создаст первичный
конструктор, получающий их же.
Плюсы и минусы неизменяемого объекта
Неизменяемые объекты имеют ряд преимуществ над изменяемы
ми и один потенциальный недостаток. Вопервых, о неизменяемых объектах проще говорить, чем об изменяемых, поскольку у них нет изменяемых со временем сложных областей состояния. Вовторых, неизменяемые объекты можно совершенно свободно куданибудь передавать, а перед передачей изменяемых объектов в другой код порой приходится делать страховочные копии. Втретьих, если объ
ект правильно сконструирован, то при одновременном обращении к неизменяемому объекту из двух потоков повредить его состояние невозможно, поскольку никакой поток не может изменить состояние неизменяемого объекта. Вчетвертых, неизменяемые объекты обеспе
чивают безопасность ключей хештаблиц. Если, к примеру, изменяе
мый объект изменился после помещения в
HashSet
, то в следующий раз при поиске там его можно не найти.
Главный недостаток неизменяемых объектов — им иногда требует
ся копирование больших графов объектов, тогда как вместо этого можно было бы сделать обновление. В некоторых случаях это может быть сложно выразить, а также могут выявиться узкие места в про
изводительности. В результате в библиотеки нередко включают из
меняемые альтернативы неизменяемым классам. Например, класс
StringBuilder
— изменяемая альтернатива неизменяемого класса
String
. Дополнительная информация о конструировании изменяемых объектов в Scala будет дана в главе 16.
ПРИМЕЧАНИЕ
Исходный пример с Rational подчеркивает разницу между Java и Scala .
В Java классы имеют конструкторы, которые могут принимать параметры, а в Scala классы могут принимать параметры напрямую . Система записи в Scala куда более лаконична — параметры класса могут использоваться напрямую в теле, нет никакой необходимости определять поля и записывать присваивания, копирующие параметры конструктора в поля . Это может привести к дополнительной экономии на шаблонном коде, особенно когда дело касается небольших классов .
128 Глава 6 • Функциональные объекты
Компилятор Scala скомпилирует любой код, помещенный в тело класса и не являющийся частью поля или определения метода, в первичный конструк
тор. Например, можно вывести такое отладочное сообщение:
class Rational(n: Int, d: Int):
println("Created " + n + "/" + d)
Получив данный код, компилятор Scala поместит вызов println в первич
ный конструктор класса
Rational
. Поэтому при создании нового экземпляра
Rational вызов println приведет к выводу отладочного сообщения:
scala> new Rational(1, 2)
Created 1/2
Val res0: Rational = Rational@6121a7dd
При создании экземпляров классов, таких как
Rational
, вы можете при желании опустить ключевое слово new
. Такое выражение использует так на
зываемый универсальный метод применения. Вот пример:
scala> Rational(1, 2)
Created 1/2
val res1: Rational = Rational@5dc7841c
6 .3 . Переопределение метода toString
При создании экземпляра
Rational в предыдущем примере REPL вывел
Rational@
5dc7841c
. Эта странная строка получилась ввиду вызова в от
ношении объекта
Rational метода toString
. По умолчанию класс
Rational наследует реализацию toString
, определенную в классе java.lang.Object
, которая просто выводит имя класса, символ
@
и шестнадцатеричное число.
Предполагалось, что результат выполнения toString поможет программи
стам, предоставив информацию, которую можно использовать в отладоч
ных инструкциях вывода информации, для ведения логов, в отчетах о сбоях тестов, а также для просмотра выходной информации REPL и отладчика.
Результат, выдаваемый на данный момент методом toString
, не приносит особой пользы, поскольку не дает никакой информации относительно значения рационального числа. Более полезная реализация toString будет выводить значения числителя и знаменателя объекта
Rational
. Переопре-
делить исходную реализацию можно, добавив метод toString к классу
Rational
:
class Rational(n: Int, d: Int):
override def toString = s"$n/$d"
6 .4 . Проверка соблюдения предварительных условий
1 ... 10 11 12 13 14 15 16 17 ... 64
чего в двоичном виде получается число
00000000000000000000000000000100
, или
4 5 .8 . Равенство объектов
Если нужно сравнить два объекта на равенство, то можно воспользоваться либо методом
==
, либо его противоположностью — методом
!=
. Вот несколь
ко простых примеров:
1 == 2 // false: Boolean
1 != 2 // true: Boolean
2 == 2 // true: Boolean
По сути, эти две операции применимы ко всем объектам, а не только к ос
новным типам. Например, оператор
==
можно использовать для сравнения списков:
List(1, 2, 3) == List(1, 2, 3) // true: Boolean
List(1, 2, 3) == List(4, 5, 6) // false: Boolean
Если пойти еще дальше, то можно сравнить два объекта, имеющих разные типы:
1 == 1.0 // true: Boolean
List(1, 2, 3) == "hello" // false: Boolean
Можно даже выполнить сравнение со значением null или с тем, что может иметь данное значение. Никакие исключения при этом выдаваться не будут:
List(1, 2, 3) == null // false: Boolean null == List(1, 2, 3) // false: Boolean
Как видите, оператор
==
реализован весьма искусно, и вы в большинстве слу
чаев получите то сравнение на равенство, которое вам нужно. Все делается по очень простому правилу: сначала левая часть проверяется на null
. Если ее значение не null
, то вызывается метод equals
. Ввиду того что equals
— метод, точность получаемого сравнения зависит от типа левого аргумента. Проверка на null выполняется автоматически, поэтому вам не нужно проводить ее
1
Этот вид сравнения выдает true в отношении различных объектов, если их содержимое одинаково и их методы equals созданы на основе проверки
1
Автоматическая проверка игнорирует правую сторону, но любой корректно реализо
ванный метод equals должен возвращать false
, если его аргумент имеет значение null
120 Глава 5 • Основные типы и операции содержимого. Например, вот как сравниваются две строки, в которых по пять одинаковых букв:
("he" + "llo") == "hello" // true: Boolean
Различия операторов == в Scala и Java
В Java оператор
==
может использоваться для сравнения как при
митивных, так и ссылочных типов. В отношении примитивных ти
пов оператор
==
в Java проверяет равенство значений, как и в Scala.
Но в отношении ссылочных типов оператор
==
в Java проверяет ра-
венство ссылок. Это значит, две переменные указывают на один и тот же объект в куче, принадлежащей JVM. Scala также предоставляет средство eq для сравнения равенства ссылок. Но метод eq и его про
тивоположность, метод ne
, применяются только к объектам, которые непосредственно отображаются на объекты Java. Исчерпывающие подробности о eq и ne приводятся в разделах 17.1 и 17.2. Кроме того, в главе 8 показано, как создавать хорошие методы equals
5 .9 . Приоритет и ассоциативность операторов
Приоритет операторов определяет, какая часть выражения вычисляется самой первой. Например, выражение
2
+
2
*
7
вычисляется в
16
, а не в
28
, поскольку оператор
*
имеет более высокий приоритет, чем оператор
+
Поэтому та часть выражения, в которой требуется перемножить числа, вы
числяется до того, как будет выполнена часть, в которой числа складыва
ются. Разумеется, чтобы уточнить в выражении порядок вычисления или переопределить приоритеты, можно воспользоваться круглыми скобками.
Например, если вы действительно хотите, чтобы результат вычисления ранее показанного выражения был
28
, то можете набрать следующее вы
ражение:
(2 + 2) * 7
Если учесть, что в Scala, по сути, нет операторов, а есть только способ приме
нения методов в форме записи операторов, то возникает вопрос: а как тогда работает приоритет операторов? Scala принимает решение о приоритете на основе первого символа метода, использованного в форме записи операторов
(из этого правила есть одно исключение, рассматриваемое ниже). Если имя метода начинается, к примеру, с
*
, то он получит более высокий приоритет,
5 .9 . Приоритет и ассоциативность операторов 121
чем метод, чье имя начинается на
+
. Следовательно, выражение
2
+
2
*
7
будет вычислено как
2
+
(2
*
7)
. Аналогично этому выражение a
+++
b
***
c
, в котором a
, b
и c
— переменные, а
+++
и
***
— методы, будет вычислено как a
+++
(b
***
c)
, поскольку метод
***
обладает более высоким уровнем прио
ритета, чем метод
+++
В табл. 5.3 показан приоритет применительно к первому символу метода в убывающем порядке, где символы, показанные на одной строке, опре
деляют одинаковый уровень приоритета. Чем выше символ в списке, тем выше приоритет начинающегося с него метода. Вот пример, показывающий влияние приоритета:
2 << 2 + 2 // 32: Int
Таблица 5.3. Приоритет операторов
(Все специальные символы)
* / %
+ –
:
= !
< >
&
^
|
(Все буквы)
(Все операторы присваивания)
Имя метода
<<
начинается с символа
<
, который появляется в приведенном списке ниже символа
+
— первого и единственного символа метода
+
. Следо
вательно,
<<
будет иметь более низкий уровень приоритета, чем
+
, и выраже
ние будет вычислено путем вызова сначала метода
+
, а затем метода
<<
, как в выражении
2
<<
(2
+
2)
. При сложении
2
+
2
в результате математического действия получается
4
, а вычисление выражения
2
<<
4
дает результат
32
Если поменять операторы местами, то будет получен другой результат:
2 + 2 << 2 // 16: Int
Поскольку первые символы, по сравнению с предыдущим примером, не изменились, то методы будут вызваны в том же порядке:
+
, а затем
<<
. Сле
довательно,
2
+
2
опять будет равен
4
, а
4
<<
2
даст результат
16
122 Глава 5 • Основные типы и операции
Единственное исключение из правил, о существовании которого уже гово
рилось, относится к операторам присваивания, заканчивающимся знаком равенства. Если оператор заканчивается знаком равенства (
=
) и не относится к одному из операторов сравнения
<=
,
>=
,
==
и
!=
, то приоритет оператора имеет такой же уровень, что и простое присваивание (
=
). То есть он ниже приоритета любого другого оператора. Например:
x *= y + 1
означает то же самое, что и x *= (y + 1)
поскольку оператор
*=
классифицируется как оператор присваивания, прио
ритет которого ниже, чем у
+
, даже притом что первым символом оператора выступает знак
*
, который обозначил бы приоритет выше, чем у
+
Если в выражении рядом появляются операторы с одинаковым уровнем приоритета, то способ группировки операторов определяется их ассоциатив-
ностью. Ассоциативность оператора в Scala определяется по его последнему символу. Как уже упоминалось в главе 3, любой метод, имя которого за
канчивается символом
:
, вызывается в отношении своего правого операнда с передачей ему левого. Методы, в окончании имени которых используются любые другие символы, действуют наоборот: они вызываются в отношении своего левого операнда с передачей себе правого. То есть из выражения a
*
b получается a.*(b)
, но из a
:::
b получается b.:::(a)
Но независимо от того, какова ассоциативность оператора, его операнды всегда вычисляются слева направо. Следовательно, если a
— выражение, не являющееся простой ссылкой на неизменяемое значение, то выраже
ние a
:::
b при более точном рассмотрении представляется следующим блоком:
{ val x = a; b.:::(x) }
В этом блоке а
попрежнему вычисляется раньше b
, а затем результат данного вычисления передается в качестве операнда принадлежащему b
методу
:::
Это правило ассоциативности играет роль также при появлении в одном выражении рядом сразу нескольких операторов с одинаковым уровнем приоритета. Если имена методов заканчиваются на
:
, они группируются справа налево, в противном случае — слева направо. Например, a
:::
b
:::
c рассматривается как a
:::
(b
:::
c)
. Но a
*
b
*
c
, в отличие от этого, рассма
тривается как
(a
*
b)
*
c
5 .10 . Обогащающие операции 123
Правила приоритета операторов — часть языка Scala, и вам не следует боять
ся применять ими. При этом, чтобы прояснить первоочередность использо
вания операторов, в некоторых выражениях все же лучше прибегнуть к кру
глым скобкам. Пожалуй, единственное, на что можно реально рассчитывать в отношении знания порядка приоритета другими программистами, — то, что мультипликативные операторы
*
,
/
и
%
имеют более высокий уровень приоритета, чем аддитивные
+
и
–
. Таким образом, даже если выражение a
+
b
<<
c выдает нужный результат и без круглых скобок, стоит внести до
полнительную ясность с помощью записи
(a
+
b)
<<
c
. Это снизит количество нелестных отзывов ваших коллег по поводу использованной вами формы записи операторов, которое выражается, к примеру, в недовольном воскли
цании вроде «Опять в его коде невозможно разобраться!» и отправке вам сообщения наподобие bills
!*&ˆ%
code!
1 5 .10 . Обогащающие операции
В отношении основных типов Scala можно вызвать намного больше методов, чем рассмотрено в предыдущих разделах. Некоторые примеры показаны в табл. 5.4. Начиная со Scala 3, эти методы доступны через неявные преоб
разования — устаревшую технику, которая в конечном итоге будет заменена методами расширения, подробно описанными в главе 22. А пока вам нужно знать лишь то, что для каждого основного типа, рассмотренного в текущей
Таблица 5.4. Некоторые обогащающие операции
Код
Результат
0 max 5 5
0 min 5 0
–2.7 abs
2.7
–2.7 round
–3L
1.5 isInfinity
False
(1.0 / 0) isInfinity
True
4 to 6
Range(4, 5, 6)
"bob" capitalize
"Bob"
"robert" drop 2
"bert"
1
Теперь вы уже знаете, что, получив такой код, компилятор Scala создаст вызов
(
bills.!*&^%(code)).!
124 Глава 5 • Основные типы и операции главе, существует обогащающая оболочка, которая предоставляет ряд допол
нительных методов. Поэтому увидеть все доступные методы, применяемые в отношении основных типов, можно, обратившись к документации по API, которая касается обогащающей оболочки для каждого основного типа. Эти классы перечислены в табл. 5.5.
Таблица 5.5. Классы обогащающих оболочек
Основной тип
Обогащающая оболочка
Byte scala.runtime.RichByte
Short scala.runtime.RichShort
Int scala.runtime.RichInt
Long scala.runtime.RichLong
Char scala.runtime.RichChar
Float scala.runtime.RichFloat
Double scala.runtime.RichDouble
Boolean scala.runtime.RichBoolean
String scala.collection.immutable.StringOps
Резюме
Основное, что следует усвоить, прочитав данную главу, — операторы в Scala являются вызовами методов и для основных типов Scala существуют неяв
ные преобразования в обогащенные варианты, которые добавляют дополни
тельные полезные методы. В главе 6 мы покажем, что означает конструиро
вание объектов в функциональном стиле, обеспечивающее новые реализации некоторых операторов, рассмотренных в настоящей главе.
6
Функциональные объекты
Усвоив основы, рассмотренные в предыдущих главах, вы готовы разработать больше полнофункциональных классов Scala. В этой главе основное вни
мание мы уделим классам, определяющим функциональные объекты или объекты, не имеющие никакого изменяемого состояния. Запуская примеры, мы создадим несколько вариантов класса, моделирующего рациональные числа в виде неизменяемых объектов. Попутно будут показаны дополни
тельные аспекты объектноориентированного программирования на Scala: параметры класса и конструкторы, методы и операторы, приватные члены, переопределение, проверка соблюдения предварительных условий, пере
грузка и рекурсивные ссылки.
6 .1 . Спецификация класса Rational
Рациональным называется число, которое может быть выражено соотноше
нием n
/
d
, где n
и d
представлены целыми числами, за исключением того, что d
не может быть нулем. Здесь n
называется числителем, а d
— знаменателем.
Примерами рациональных чисел могут послужить 1/2, 2/3, 112/239 и 2/1.
В сравнении с числами с плавающей точкой рациональные числа имеют то преимущество, что дроби представлены точно, без округлений или при
ближений.
Разрабатываемый в этой главе класс должен моделировать поведение рацио
нальных чисел, позволяя производить над ними арифметические действия по сложению, вычитанию, умножению и делению. Для сложения двух ра
циональных чисел сначала нужно получить общий знаменатель, после чего сложить два числителя. Например, чтобы выполнить сложение 1/2 + 2/3,
126 Глава 6 • Функциональные объекты обе части левого операнда умножаются на 3, а обе части правого операнда — на 2, в результате чего получается 3/6 + 4/6. Сложение двух числителей дает результат 7/6. Для перемножения двух рациональных чисел можно просто перемножить их числители, а затем знаменатели. Таким образом, 1/2 · 2/5 дает число 2/10, которое можно представить более кратко в нормализованном виде как 1/5. Деление выполняется путем перестановки местами числителя и зна
менателя правого операнда с последующим перемножением чисел. Например,
1/2 / 3/5 — то же самое, что и 1/2 · 5/3, в результате получается число 5/6.
Одно, возможно, очевидное наблюдение заключается в том, что в математике рациональные числа не имеют изменяемого состояния. Можно сложить два рациональных числа, и результатом будет новое рациональное число. Исход
ные числа не будут изменены. Неизменяемый класс
Rational
, разрабатыва
емый в данной главе, будет иметь такое же свойство. Каждое рациональное число будет представлено одним объектом
Rational
. При сложении двух объ
ектов
Rational для хранения суммы будет создаваться новый объект
Rational
В этой главе мы представим некоторые допустимые в Scala способы написа
ния библиотек, которые создают впечатление, будто используется поддерж
ка, присущая непосредственно самому языку программирования. Например, в конце этой главы вы сможете сделать с классом
Rational следующее:
scala> val oneHalf = Rational(1, 2)
val oneHalf: Rational = 1/2
scala> val twoThirds = Rational(2, 3)
val twoThirds: Rational = 2/3
scala> (oneHalf / 7) + (1 — twoThirds)
val res0: Rational = 17/42 6 .2 . Конструирование класса Rational
Конструирование класса
Rational неплохо начать с рассмотрения того, как клиентыпрограммисты будут создавать новый объект
Rational
. Было ре
шено создавать объекты
Rational неизменяемыми, и потому мы потребуем, чтобы эти клиенты при создании экземпляра предоставляли все необходи
мые ему данные (в нашем случае числитель и знаменатель). Поэтому начнем конструирование со следующего кода:
class Rational(n: Int, d: Int)
По поводу этой строки кода в первую очередь следует заметить: если у класса нет тела, то вам не нужно ставить пустые фигурные скобки, а также нет необ
6 .2 . Конструирование класса Rational 127
ходимости завершать строку двоеточием. Идентификаторы n
и d
, указанные в круглых скобках после имени класса,
Rational
, называются параметрами
класса. Компилятор Scala подберет эти два параметра и создаст первичный
конструктор, получающий их же.
Плюсы и минусы неизменяемого объекта
Неизменяемые объекты имеют ряд преимуществ над изменяемы
ми и один потенциальный недостаток. Вопервых, о неизменяемых объектах проще говорить, чем об изменяемых, поскольку у них нет изменяемых со временем сложных областей состояния. Вовторых, неизменяемые объекты можно совершенно свободно куданибудь передавать, а перед передачей изменяемых объектов в другой код порой приходится делать страховочные копии. Втретьих, если объ
ект правильно сконструирован, то при одновременном обращении к неизменяемому объекту из двух потоков повредить его состояние невозможно, поскольку никакой поток не может изменить состояние неизменяемого объекта. Вчетвертых, неизменяемые объекты обеспе
чивают безопасность ключей хештаблиц. Если, к примеру, изменяе
мый объект изменился после помещения в
HashSet
, то в следующий раз при поиске там его можно не найти.
Главный недостаток неизменяемых объектов — им иногда требует
ся копирование больших графов объектов, тогда как вместо этого можно было бы сделать обновление. В некоторых случаях это может быть сложно выразить, а также могут выявиться узкие места в про
изводительности. В результате в библиотеки нередко включают из
меняемые альтернативы неизменяемым классам. Например, класс
StringBuilder
— изменяемая альтернатива неизменяемого класса
String
. Дополнительная информация о конструировании изменяемых объектов в Scala будет дана в главе 16.
ПРИМЕЧАНИЕ
Исходный пример с Rational подчеркивает разницу между Java и Scala .
В Java классы имеют конструкторы, которые могут принимать параметры, а в Scala классы могут принимать параметры напрямую . Система записи в Scala куда более лаконична — параметры класса могут использоваться напрямую в теле, нет никакой необходимости определять поля и записывать присваивания, копирующие параметры конструктора в поля . Это может привести к дополнительной экономии на шаблонном коде, особенно когда дело касается небольших классов .
128 Глава 6 • Функциональные объекты
Компилятор Scala скомпилирует любой код, помещенный в тело класса и не являющийся частью поля или определения метода, в первичный конструк
тор. Например, можно вывести такое отладочное сообщение:
class Rational(n: Int, d: Int):
println("Created " + n + "/" + d)
Получив данный код, компилятор Scala поместит вызов println в первич
ный конструктор класса
Rational
. Поэтому при создании нового экземпляра
Rational вызов println приведет к выводу отладочного сообщения:
scala> new Rational(1, 2)
Created 1/2
Val res0: Rational = Rational@6121a7dd
При создании экземпляров классов, таких как
Rational
, вы можете при желании опустить ключевое слово new
. Такое выражение использует так на
зываемый универсальный метод применения. Вот пример:
scala> Rational(1, 2)
Created 1/2
val res1: Rational = Rational@5dc7841c
6 .3 . Переопределение метода toString
При создании экземпляра
Rational в предыдущем примере REPL вывел
Rational@
5dc7841c
. Эта странная строка получилась ввиду вызова в от
ношении объекта
Rational метода toString
. По умолчанию класс
Rational наследует реализацию toString
, определенную в классе java.lang.Object
, которая просто выводит имя класса, символ
@
и шестнадцатеричное число.
Предполагалось, что результат выполнения toString поможет программи
стам, предоставив информацию, которую можно использовать в отладоч
ных инструкциях вывода информации, для ведения логов, в отчетах о сбоях тестов, а также для просмотра выходной информации REPL и отладчика.
Результат, выдаваемый на данный момент методом toString
, не приносит особой пользы, поскольку не дает никакой информации относительно значения рационального числа. Более полезная реализация toString будет выводить значения числителя и знаменателя объекта
Rational
. Переопре-
делить исходную реализацию можно, добавив метод toString к классу
Rational
:
class Rational(n: Int, d: Int):
override def toString = s"$n/$d"
6 .4 . Проверка соблюдения предварительных условий
1 ... 10 11 12 13 14 15 16 17 ... 64
129
Модификатор override перед определением метода показывает, что преды
дущее определение метода переопределяется (более подробно этот вопрос рассматривается в главе 10). Поскольку отныне числа типа
Rational будут выводиться совершенно отчетливо, мы удаляем отладочную инструкцию println
, помещенную в тело предыдущей версии класса
Rational
. Теперь новое поведение
Rational можно протестировать в REPL:
scala> val x = Rational(1, 3)
x: Rational = 1/3
scala> val y = Rational(5, 7)
y: Rational = 5/7 6 .4 . Проверка соблюдения предварительных условий
В качестве следующего шага переключим внимание на проблему, связанную с текущим поведением первичного конструктора. Как упоминалось в на
чале главы, рациональные числа не должны содержать ноль в знаменателе.
Но пока первичный конструктор может принимать ноль, передаваемый в качестве параметра d
:
scala> new Rational(5, 0) // 5/0
val res1: Rational = 5/0
Одно из преимуществ объектноориентированного программирования — возможность инкапсуляции данных внутри объектов, чтобы можно было гарантировать, что данные корректны в течение всей жизни объекта. В дан
ном случае для такого неизменяемого объекта, как
Rational
, это значит, что вы должны гарантировать корректность данных на этапе конструирования объекта при условии, что нулевой знаменатель — недопустимое состояние числа типа
Rational и такое число не должно создаваться, если в качестве параметра d
передается ноль.
Лучше всего решить эту проблему, определив для первичного конструктора
предусловие, согласно которому d
должен иметь ненулевое значение. Предус
ловие — ограничение, накладываемое на значения, передаваемые в метод или конструктор, то есть требование, которое должно выполняться вызыва ющим кодом. Один из способов решить задачу — использовать метод require
1
:
1
Метод require определен в самостоятельном объекте
Predef
. Как упоминалось в разделе 4.5, элементы класса
Predef автоматически импортируются в каждый исходный файл Scala.
130 Глава 6 • Функциональные объекты class Rational(n: Int, d: Int):
require(d != 0)
override def toString = s"$n/$d"
Метод require получает один булев параметр. Если переданное значение приведет к вычислению в true
, то из метода require произойдет нормальный выход. В противном случае объект не создастся и будет выдано исключение
IllegalArgumentException
6 .5 . Добавление полей
Теперь, когда первичный конструктор выдвигает нужные предусловия, мы переключимся на поддержку сложения. Для этого определим в классе
Rational публичный метод add
, получающий в качестве параметра еще одно значение типа
Rational
. Чтобы сохранить неизменяемость класса
Rational
, метод add не должен прибавлять переданное рациональное число к объекту, в отношении которого он вызван. Ему нужно создать и вернуть новый объ
ект
Rational
, содержащий сумму. Можно подумать, что метод add создается следующим образом:
class Rational(n: Int, d: Int): // Этот код не будет скомпилирован require(d != 0)
override def toString = s"$n/$d def add(that: Rational): Rational =
Rational(n * that.d + that.n * d, d * that.d)
Но, получив этот код, компилятор выдаст свои возражения:
5 | Rational(n * that.d + that.n * d, d * that.d)
| ˆˆˆˆˆˆ
|value n in class Rational cannot be accessed as a member
| of (that : Rational) from class Rational.
5 | Rational(n * that.d + that.n * d, d * that.d)
| ˆˆˆˆˆˆ
|value d in class Rational cannot be accessed as a member
| of (that : Rational) from class Rational.
5 | Rational(n * that.d + that.n * d, d * that.d)
| ˆˆˆˆˆˆ
|value d in class Rational cannot be accessed as a member
| of (that : Rational) from class Rational.
Хотя параметры n
и d
класса находятся в области видимости кода вашего метода add
, получить доступ к их значениям можно только в объекте, в от
ношении которого вызван данный метод. Следовательно, когда в реализации последнего указывается n
или d
, компилятор рад предоставить вам значения