Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 731
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
90 Глава 3 • Дальнейшие шаги в Scala возвращает новый
Option
, содержащий результат передачи исходного эле
мента
Some в функцию, переданную в map
. Вот пример преобразования startsW
в
Some
, содержащего строку
WHO
:
startsW.map(word => word.toUpperCase) // Some(WHO)
Как и в случае с
List и
Vector
, вы можете добиться того же преобразования в
Option с помощью for-yield
:
for word <- startsW yield word.toUpperCase // Some(WHO)
Если вы вызываете map с параметром
None
(параметр, который не определен), то вернете значение
None
. Например:
startsH.map(word => word.toUpperCase) // None
А вот такое же преобразование с использованием for-yield
:
for word <- startsH yield word.toUpperCase // None
Вы можете преобразовать многие другие типы с помощью map и for-yield
, но пока этого достаточно. Этот шаг дал вам представление о том, сколько кода Scala написано в виде функциональных преобразований неизменяемых структур данных.
Резюме
Знания, полученные в этой главе, позволят вам начать применять Scala для решения небольших задач, в особенности тех, для которых используются скрипты. В последующих главах мы глубже разберем рассмотренные темы, а также представим другие, не затронутые здесь.
4
Классы и объекты
В предыдущих двух главах вы разобрались в основах классов и объектов.
В этой главе вам предстоит углубленно проработать данную тему. Здесь мы дадим дополнительные сведения о классах, полях и методах, а также общее представление о том, когда подразумевается использование точки с запятой.
Кроме того, рассмотрим объектыодиночки (singleton) и то, как с их помо
щью писать и запускать приложения на Scala. Если вам уже знаком язык
Java, то вы увидите, что в Scala фигурируют похожие, но все же немного отличающиеся концепции. Поэтому чтение данной главы пойдет на пользу даже великим знатокам языка Java.
4 .1 . Классы, поля и методы
Классы — «чертежи» объектов. После определения класса из него, как по чертежу, можно создавать объекты, воспользовавшись для этого ключевым словом new
. Например, при наличии следующего определения класса:
class ChecksumAccumulator:
// Сюда помещается определение класса с отступом с помощью кода new ChecksumAccumulator можно создавать объекты
ChecksumAccumulator
Внутри определения класса помещаются поля и методы, которые в общем называются членами класса. Поля, которые определяются либо как val
, либо как var
переменные, являются переменными, относящимися к объектам.
92 Глава 4 • Классы и объекты
Методы, определяемые с помощью ключевого слова def
, содержат испол
няемый код. В полях хранятся состояние или данные объекта, а методы используют эти данные для выполнения в отношении объекта вычислений.
При создании экземпляра класса среда выполнения приложения резервирует часть памяти для хранения образа состояния получающегося при этом объ
екта (то есть содержимого его полей). Например, если вы определите класс
ChecksumAccumulator и дадите ему var
поле по имени sum
:
class ChecksumAccumulator:
var sum = 0
а потом дважды создадите его экземпляры с помощью следующего кода:
val acc = new ChecksumAccumulator val csa = new ChecksumAccumulator то образ объектов в памяти может выглядеть так:
Поскольку sum
— поле, определенное внутри класса
ChecksumAccumulator
, и относится к var
, а не к val
переменным, то впоследствии ему (полю) можно заново присвоить другое
Int
значение:
acc.sum = 3
Теперь картинка может выглядеть так:
4 .1 . Классы, поля и методы 93
По поводу этой картинки нужно отметить следующее: на ней показаны две переменные sum
. Одна из них находится в объекте, на который ссылается acc
, а другая — в объекте, на который ссылается csa
. Поля также называют пере-
менными экземпляра, поскольку каждый экземпляр получает собственный набор переменных. Все переменные экземпляра объекта составляют образ объекта в памяти. То, что здесь показано, свидетельствует не только о на
личии двух переменных sum
, но и о том, что изменение одной из них никак не отражается на другой.
В этом примере следует также отметить: у вас есть возможность изменить объект, на который ссылается acc
, даже несмотря на то, что acc относится к val
переменным. Но с учетом того, что acc
(или csa
) являются val
, а не var
переменными, вы не можете присвоить им какойнибудь другой объект.
Например, попытка, показанная ниже, не будет успешной:
// Не пройдет компиляцию, поскольку acc является val-переменной acc = new ChecksumAccumulator
Теперь вы можете рассчитывать на то, что переменная acc всегда будет ссылаться на тот же объект
ChecksumAccumulator
, с помощью которого вы ее инициализировали; поля же, содержащиеся внутри этого объекта, могут со временем измениться.
Один из важных способов обеспечения надежности объекта — гарантия того, что состояние этого объекта, то есть значения его переменных экземпляра, остается корректным в течение всего его жизненного цикла. Первый шаг к предотвращению непосредственного стороннего доступа к полям — созда
ние приватных (private) полей. Доступ к приватным полям можно получить только методами, определенными в том же самом классе, поэтому весь код, который может обновить состояние, будет локализован в классе. Чтобы объ
явить поле приватным, перед ним нужно поставить модификатор доступа private
:
class ChecksumAccumulator:
private var sum = 0
С таким определением
ChecksumAccumulator любая попытка доступа к sum за пределами класса будет неудачной:
val acc = new ChecksumAccumulator acc.sum = 5 // Не пройдет компиляцию, поскольку поле sum
// является приватным
Теперь, когда поле sum стало приватным, доступ к нему можно получить только из кода, определенного внутри тела самого класса. Следовательно,
94 Глава 4 • Классы и объекты класс
ChecksumAccumulator не будет особо полезен, пока внутри него не будут определены некоторые методы:
class ChecksumAccumulator:
private var sum = 0
def add(b: Byte): Unit =
sum += b def checksum(): Int =
return (sum & 0xFF) + 1
ПРИМЕЧАНИЕ
В Scala элементы класса делают публичными, если нет явного указания какого-либо модификатора доступа . Иначе говоря, там, где в Java ставится модификатор public, в Scala вы обходитесь простым замалчиванием . Публич- ный (public) доступ в Scala — уровень доступа по умолчанию .
Теперь у
ChecksumAccumulator есть два метода: add и checksum
, оба они демон
стрируют основную форму определения функции, показанную на рис. 2.1 1
Внутри этого метода могут использоваться любые параметры метода. Од
ной из важных характеристик параметров метода в Scala является то, что они относятся к val
, а не к var
переменным
2
. При попытке повторного присваивания значения параметру внутри метода в Scala произойдет сбой компиляции:
def add(b: Byte): Unit = b = 1 // Не пройдет компиляцию, поскольку b относится к val-переменным sum += b
Хотя методы add и checksum в данной версии
ChecksumAccumulator реали
зуют желаемые функциональные свойства вполне корректно, их можно определить в более лаконичном стиле. Вопервых, в конце метода checksum можно избавиться от лишнего слова return
. В отсутствие явно указанной инструкции return метод в Scala возвращает последнее вычисленное им значение.
1
В методе checksum используются два оператора: тильда (
) для побитового допол
нения и амперсанд (
&
) для побитового И. Оба оператора описаны в разделе 5.7.
2
Причина, по которой параметры имеют значение val
, заключается в том, что о val легче рассуждать. Вам не нужно ничего дополнительно изучать, как это делается с var
, чтобы определить, переназначается ли val
4 .1 . Классы, поля и методы 95
При написании методов рекомендуется применять стиль, исключающий явное и особенно многократное использование инструкции return
. Каждый метод нужно рассматривать в качестве выражения, выдающего одно значение, которое и является возвращаемым. Эта философия будет побуждать вас соз
давать небольшие методы и разбивать слишком крупные методы на несколько мелких. В то же время выбор конструктивного решения зависит от контекста решаемых задач, и, если того требуют условия, Scala упрощает написание методов, которые имеют несколько явно указанных возвращаемых значений.
Поскольку checksum выполняет только вычисление значений, ему не тре
буется прямая инструкция return
. Еще одним способом обобщения мето
дов является то, что если метод вычисляет только одно результирующее выражение и оно короткое, его можно поместить в ту же строку, что и сам def
. Для максимальной краткости вы можете не указывать тип результата, и Scala самостоятельно сделает его вывод. С этими изменениями класс
ChecksumAccumulator выглядит так:
class ChecksumAccumulator:
private var sum = 0
def add(b: Byte) = sum += b def checksum() = (sum & 0xFF) + 1
Несмотря на то что компилятор Scala вполне корректно выполнит вывод ре
зультирующих типов методов add и checksum
, показанных в предыдущем при
мере, читатели кода будут вынуждены вывести результирующие типы путем
логических умозаключений на основе изучения тел методов. Поэтому лучше всетаки будет всегда явно указывать результирующие типы для публичных методов, объявленных в классе, даже когда компилятор может вывести их для вас самостоятельно. Применение этого стиля показано в листинге 4.1.
Листинг 4.1. Окончательная версия класса ChecksumAccumulator
// Этот код находится в файле ChecksumAccumulator.scala class ChecksumAccumulator:
private var sum = 0
def add(b: Byte): Unit = sum += b def checksum(): Int = (sum & 0xFF) + 1
Методы с результирующим типом
Unit
, к которым относится и метод add класса
ChecksumAccumulator
, выполняются для получения побочного эф
фекта. Последний обычно определяется в виде изменения внешнего по от
ношению к методу состояния или в виде выполнения какойлибо операции вводавывода. Что касается метода add
, то побочный эффект заключается в присваивании sum нового значения. Метод, который выполняется только для получения его побочного эффекта, называется процедурой.
96 Глава 4 • Классы и объекты
4 .2 . Когда подразумевается использование точки с запятой
В программе на Scala точку с запятой в конце инструкции обычно можно не ставить. Если вся инструкция помещается на одной строке, то при же
лании можете поставить в конце данной строки точку с запятой, но это не обязательно. В то же время точка с запятой нужна, если на одной строке размещаются сразу несколько инструкций:
val s = "hello"; println(s)
Если требуется набрать инструкцию, занимающую несколько строк, то в большинстве случаев вы можете просто ее ввести, а Scala разделит инструк
ции в нужном месте. Например, следующий код рассматривается как одна инструкция, расположенная на четырех строках:
if x < 2 then
"too small"
else
"ok"
Правила расстановки точек с запятой
Правила разделения операторов удивительно просты. Вкратце, точка с запятой всегда обозначает конец строки, кроме случаев, когда не вы
полняется одно из следующих условий.
1. Рассматриваемая строка заканчивается словом или символом, который недопустим в качестве конца оператора, например точкой или инфиксным оператором.
2. Следующая строка начинается со слова, с которого не может на
чинаться оператор.
3. Строка заканчивается внутри круглых
(...)
или квадратных
[...] скобок, потому что они не могут содержать несколько операторов.
4 .3 . Объекты-одиночки
Как упоминалось в главе 1, один из аспектов, позволяющих Scala быть бо
лее объектноориентированным языком, чем Java, заключается в том, что в классах Scala не могут содержаться статические элементы. Вместо этого
4 .3 . Объекты-одиночки 97
в Scala есть объекты-одиночки, или синглтоны. Определение объектаоди
ночки выглядит так же, как определение класса, за исключением того, что вместо ключевого слова class используется ключевое слово object
. Пример показан в листинге 4.2.
Объектодиночка в этом листинге называется
ChecksumAccumulator
, то есть носит имя, совпадающее с именем класса в предыдущем примере. Когда объ
ектодиночка использует общее с классом имя, то для класса он называется
объектом-компаньоном. И класс, и его объекткомпаньон нужно определять в одном и том же исходном файле. Класс по отношению к объектуодиночке называется классом-компаньоном. Класс и его объекткомпаньон могут об
ращаться к приватным элементам друг друга.
Листинг 4.2. Объект-компаньон для класса ChecksumAccumulator
// Этот код находится в файле ChecksumAccumulator.scala import scala.collection.mutable object ChecksumAccumulator:
private val cache = mutable.Map.empty[String, Int]
def calculate(s: String): Int =
if cache.contains(s) then cache(s)
else val acc = new ChecksumAccumulator for c <- s do acc.add((c >> 8).toByte)
acc.add(c.toByte)
val cs = acc.checksum()
cache += (s –> cs)
cs
Объектодиночка
ChecksumAccumulator располагает одним методом по имени calculate
, который получает строку
String и вычисляет контрольную сумму символов этой строки. Вдобавок он имеет одно приватное поле cache
, пред
ставленное изменяемым отображением, в котором кэшируются ранее вычис
ленные контрольные суммы
1
. В первой строке метода,
"if cache.contains(s)
1
Здесь cache используется, чтобы показать объектодиночку с полем. Кэширование с помощью поля cache помогает оптимизировать производительность, сокращая за счет расхода памяти время вычисления и разменивая расход памяти на время вычисления. Как правило, использовать кэшпамять таким образом целесообразно только в том случае, если с ее помощью можно решить проблемы производительно
сти и воспользоваться отображением со слабыми ссылками, например
WeakHashMap в scala.collection.mutable
, чтобы записи в кэшпамяти могли попадать в сборщик мусора при наличии дефицита памяти.
98 Глава 4 • Классы и объекты then"
, определяется, не содержится ли в отображении cache переданная строка в качестве ключа. Если да, то просто возвращается отображенное на этот ключ значение cache(s)
. В противном случае выполняется условие else
, которое вычисляет контрольную сумму. В первой строке условия else определяется val
переменная по имени acc
, которая инициализируется новым экземпля
ром
ChecksumAccumulator
1
. В следующей строке находится выражение for
Оно выполняет последовательный перебор каждого символа в переданной строке, преобразует символ в значение типа
Byte
, вызывая в отношении это
го символа метод toByte
, и передает результат в метод add того экземпляра
ChecksumAccumulator
, на который ссылается acc
2
. Когда завершится вычисле
ние выражения for
, в следующей строке метода в отношении acc будет вызван метод checksum
, который берет контрольную сумму для переданного значения типа
String и сохраняет ее в val
переменной по имени cs
В следующей строке, cache
+=
(s
–>
cs)
, переданный строковый ключ отобра
жается на целочисленное значение контрольной суммы, и эта пара «ключ — значение» добавляется в отображение cache
. В последнем выражении метода, cs
, обеспечивается использование контрольной суммы в качестве результата выполнения метода.
Если у вас есть опыт программирования на Java, то объектыодиночки можно представить в качестве хранилища для любых статических методов, которые вполне могли быть написаны на Java. Методы в объектаходиночках можно вызывать с помощью такого синтаксиса: имя объекта, точка, имя метода.
Например, метод calculate объектаодиночки
ChecksumAccumulator можно вызвать следующим образом:
ChecksumAccumulator.calculate("Every value is an object.")
Но объектодиночка не только хранилище статических методов. Он объект первого класса. Поэтому имя объектаодиночки можно рассматривать в ка
честве «этикетки», прикрепленной к объекту.
1
Поскольку ключевое слово new используется только для создания экземпляров классов, новый объект, созданный здесь в качестве экземпляра класса
ChecksumAc- cumulator
, не является одноименным объектомодиночкой.
2
Оператор
>>
, выполняющий побитовый сдвиг вправо, описан в разделе 5.7.
4 .4 . Case-классы 99
Определение объектаодиночки не является определением типа на том уровне абстракции, который используется в Scala. Имея лишь определение объекта
ChecksumAccumulator
, невозможно создать одноименную переменную типа.
Точнее, тип с именем
ChecksumAccumulator определяется классомкомпаньоном объектаодиночки. Тем не менее объектыодиночки расширяют суперкласс и могут подмешивать трейты. Учитывая то, что каждый объектодиночка — эк
земпляр своего суперкласса и подмешанных в него трейтов, его методы можно вызывать через эти типы, ссылаясь на него из переменных этих типов и пере
давая ему методы, ожидающие использования этих типов. Примеры объектов
одиночек, являющихся наследниками классов и трейтов, показаны в главе 12.
Одно из отличий классов от объектоводиночек состоит в том, что объекты
одиночки не могут принимать параметры, а классы — могут. Создать экзем
пляр объектаодиночки с помощью ключевого слова new нельзя, поэтому передать ему параметры не представляется возможным. Каждый объектоди
ночка реализуется как экземпляр синтетического класса, ссылка на который находится в статической переменной, поэтому у них и у статических классов
Java одинаковая семантика инициализации
1
. В частности, объектодиночка инициализируется при первом обращении к нему какоголибо кода.
Объектодиночка, который не имеет общего имени с классомкомпаньоном, называется самостоятельным. Такие объекты можно применять для решения многих задач, включая сбор в одно целое родственных вспомогательных ме
тодов или определение точки входа в приложение Scala. Именно этот случай мы и рассмотрим в следующем разделе.
4 .4 . Case-классы
Часто при написании класса вам потребуется реализация таких методов, как equals
, hashCode
, toString
— методы доступа или фабричные методы.
Их написание может занять много времени и привести к ошибкам. Scala предлагает такой инструмент, как case
классы (классыобразцы), которые могут генерировать реализации нескольких методов на основе значений, переданных его основному конструктору. Вы создаете класс case
, помещая модификатор case перед class
, например:
case class Person(name: String, age: Int)
1
В качестве имени синтетического класса используется имя объекта со знаком дол
лара. Следовательно, синтетический класс, применяемый для объектаодиночки
Check sumAccumulator
, называется
ChecksumAccumulator$
1 ... 7 8 9 10 11 12 13 14 ... 64
90 Глава 3 • Дальнейшие шаги в Scala возвращает новый
Option
, содержащий результат передачи исходного эле
мента
Some в функцию, переданную в map
. Вот пример преобразования startsW
в
Some
, содержащего строку
WHO
:
startsW.map(word => word.toUpperCase) // Some(WHO)
Как и в случае с
List и
Vector
, вы можете добиться того же преобразования в
Option с помощью for-yield
:
for word <- startsW yield word.toUpperCase // Some(WHO)
Если вы вызываете map с параметром
None
(параметр, который не определен), то вернете значение
None
. Например:
startsH.map(word => word.toUpperCase) // None
А вот такое же преобразование с использованием for-yield
:
for word <- startsH yield word.toUpperCase // None
Вы можете преобразовать многие другие типы с помощью map и for-yield
, но пока этого достаточно. Этот шаг дал вам представление о том, сколько кода Scala написано в виде функциональных преобразований неизменяемых структур данных.
Резюме
Знания, полученные в этой главе, позволят вам начать применять Scala для решения небольших задач, в особенности тех, для которых используются скрипты. В последующих главах мы глубже разберем рассмотренные темы, а также представим другие, не затронутые здесь.
4
Классы и объекты
В предыдущих двух главах вы разобрались в основах классов и объектов.
В этой главе вам предстоит углубленно проработать данную тему. Здесь мы дадим дополнительные сведения о классах, полях и методах, а также общее представление о том, когда подразумевается использование точки с запятой.
Кроме того, рассмотрим объектыодиночки (singleton) и то, как с их помо
щью писать и запускать приложения на Scala. Если вам уже знаком язык
Java, то вы увидите, что в Scala фигурируют похожие, но все же немного отличающиеся концепции. Поэтому чтение данной главы пойдет на пользу даже великим знатокам языка Java.
4 .1 . Классы, поля и методы
Классы — «чертежи» объектов. После определения класса из него, как по чертежу, можно создавать объекты, воспользовавшись для этого ключевым словом new
. Например, при наличии следующего определения класса:
class ChecksumAccumulator:
// Сюда помещается определение класса с отступом с помощью кода new ChecksumAccumulator можно создавать объекты
ChecksumAccumulator
Внутри определения класса помещаются поля и методы, которые в общем называются членами класса. Поля, которые определяются либо как val
, либо как var
переменные, являются переменными, относящимися к объектам.
92 Глава 4 • Классы и объекты
Методы, определяемые с помощью ключевого слова def
, содержат испол
няемый код. В полях хранятся состояние или данные объекта, а методы используют эти данные для выполнения в отношении объекта вычислений.
При создании экземпляра класса среда выполнения приложения резервирует часть памяти для хранения образа состояния получающегося при этом объ
екта (то есть содержимого его полей). Например, если вы определите класс
ChecksumAccumulator и дадите ему var
поле по имени sum
:
class ChecksumAccumulator:
var sum = 0
а потом дважды создадите его экземпляры с помощью следующего кода:
val acc = new ChecksumAccumulator val csa = new ChecksumAccumulator то образ объектов в памяти может выглядеть так:
Поскольку sum
— поле, определенное внутри класса
ChecksumAccumulator
, и относится к var
, а не к val
переменным, то впоследствии ему (полю) можно заново присвоить другое
Int
значение:
acc.sum = 3
Теперь картинка может выглядеть так:
4 .1 . Классы, поля и методы 93
По поводу этой картинки нужно отметить следующее: на ней показаны две переменные sum
. Одна из них находится в объекте, на который ссылается acc
, а другая — в объекте, на который ссылается csa
. Поля также называют пере-
менными экземпляра, поскольку каждый экземпляр получает собственный набор переменных. Все переменные экземпляра объекта составляют образ объекта в памяти. То, что здесь показано, свидетельствует не только о на
личии двух переменных sum
, но и о том, что изменение одной из них никак не отражается на другой.
В этом примере следует также отметить: у вас есть возможность изменить объект, на который ссылается acc
, даже несмотря на то, что acc относится к val
переменным. Но с учетом того, что acc
(или csa
) являются val
, а не var
переменными, вы не можете присвоить им какойнибудь другой объект.
Например, попытка, показанная ниже, не будет успешной:
// Не пройдет компиляцию, поскольку acc является val-переменной acc = new ChecksumAccumulator
Теперь вы можете рассчитывать на то, что переменная acc всегда будет ссылаться на тот же объект
ChecksumAccumulator
, с помощью которого вы ее инициализировали; поля же, содержащиеся внутри этого объекта, могут со временем измениться.
Один из важных способов обеспечения надежности объекта — гарантия того, что состояние этого объекта, то есть значения его переменных экземпляра, остается корректным в течение всего его жизненного цикла. Первый шаг к предотвращению непосредственного стороннего доступа к полям — созда
ние приватных (private) полей. Доступ к приватным полям можно получить только методами, определенными в том же самом классе, поэтому весь код, который может обновить состояние, будет локализован в классе. Чтобы объ
явить поле приватным, перед ним нужно поставить модификатор доступа private
:
class ChecksumAccumulator:
private var sum = 0
С таким определением
ChecksumAccumulator любая попытка доступа к sum за пределами класса будет неудачной:
val acc = new ChecksumAccumulator acc.sum = 5 // Не пройдет компиляцию, поскольку поле sum
// является приватным
Теперь, когда поле sum стало приватным, доступ к нему можно получить только из кода, определенного внутри тела самого класса. Следовательно,
94 Глава 4 • Классы и объекты класс
ChecksumAccumulator не будет особо полезен, пока внутри него не будут определены некоторые методы:
class ChecksumAccumulator:
private var sum = 0
def add(b: Byte): Unit =
sum += b def checksum(): Int =
return (sum & 0xFF) + 1
ПРИМЕЧАНИЕ
В Scala элементы класса делают публичными, если нет явного указания какого-либо модификатора доступа . Иначе говоря, там, где в Java ставится модификатор public, в Scala вы обходитесь простым замалчиванием . Публич- ный (public) доступ в Scala — уровень доступа по умолчанию .
Теперь у
ChecksumAccumulator есть два метода: add и checksum
, оба они демон
стрируют основную форму определения функции, показанную на рис. 2.1 1
Внутри этого метода могут использоваться любые параметры метода. Од
ной из важных характеристик параметров метода в Scala является то, что они относятся к val
, а не к var
переменным
2
. При попытке повторного присваивания значения параметру внутри метода в Scala произойдет сбой компиляции:
def add(b: Byte): Unit = b = 1 // Не пройдет компиляцию, поскольку b относится к val-переменным sum += b
Хотя методы add и checksum в данной версии
ChecksumAccumulator реали
зуют желаемые функциональные свойства вполне корректно, их можно определить в более лаконичном стиле. Вопервых, в конце метода checksum можно избавиться от лишнего слова return
. В отсутствие явно указанной инструкции return метод в Scala возвращает последнее вычисленное им значение.
1
В методе checksum используются два оператора: тильда (
) для побитового допол
нения и амперсанд (
&
) для побитового И. Оба оператора описаны в разделе 5.7.
2
Причина, по которой параметры имеют значение val
, заключается в том, что о val легче рассуждать. Вам не нужно ничего дополнительно изучать, как это делается с var
, чтобы определить, переназначается ли val
4 .1 . Классы, поля и методы 95
При написании методов рекомендуется применять стиль, исключающий явное и особенно многократное использование инструкции return
. Каждый метод нужно рассматривать в качестве выражения, выдающего одно значение, которое и является возвращаемым. Эта философия будет побуждать вас соз
давать небольшие методы и разбивать слишком крупные методы на несколько мелких. В то же время выбор конструктивного решения зависит от контекста решаемых задач, и, если того требуют условия, Scala упрощает написание методов, которые имеют несколько явно указанных возвращаемых значений.
Поскольку checksum выполняет только вычисление значений, ему не тре
буется прямая инструкция return
. Еще одним способом обобщения мето
дов является то, что если метод вычисляет только одно результирующее выражение и оно короткое, его можно поместить в ту же строку, что и сам def
. Для максимальной краткости вы можете не указывать тип результата, и Scala самостоятельно сделает его вывод. С этими изменениями класс
ChecksumAccumulator выглядит так:
class ChecksumAccumulator:
private var sum = 0
def add(b: Byte) = sum += b def checksum() = (sum & 0xFF) + 1
Несмотря на то что компилятор Scala вполне корректно выполнит вывод ре
зультирующих типов методов add и checksum
, показанных в предыдущем при
мере, читатели кода будут вынуждены вывести результирующие типы путем
логических умозаключений на основе изучения тел методов. Поэтому лучше всетаки будет всегда явно указывать результирующие типы для публичных методов, объявленных в классе, даже когда компилятор может вывести их для вас самостоятельно. Применение этого стиля показано в листинге 4.1.
Листинг 4.1. Окончательная версия класса ChecksumAccumulator
// Этот код находится в файле ChecksumAccumulator.scala class ChecksumAccumulator:
private var sum = 0
def add(b: Byte): Unit = sum += b def checksum(): Int = (sum & 0xFF) + 1
Методы с результирующим типом
Unit
, к которым относится и метод add класса
ChecksumAccumulator
, выполняются для получения побочного эф
фекта. Последний обычно определяется в виде изменения внешнего по от
ношению к методу состояния или в виде выполнения какойлибо операции вводавывода. Что касается метода add
, то побочный эффект заключается в присваивании sum нового значения. Метод, который выполняется только для получения его побочного эффекта, называется процедурой.
96 Глава 4 • Классы и объекты
4 .2 . Когда подразумевается использование точки с запятой
В программе на Scala точку с запятой в конце инструкции обычно можно не ставить. Если вся инструкция помещается на одной строке, то при же
лании можете поставить в конце данной строки точку с запятой, но это не обязательно. В то же время точка с запятой нужна, если на одной строке размещаются сразу несколько инструкций:
val s = "hello"; println(s)
Если требуется набрать инструкцию, занимающую несколько строк, то в большинстве случаев вы можете просто ее ввести, а Scala разделит инструк
ции в нужном месте. Например, следующий код рассматривается как одна инструкция, расположенная на четырех строках:
if x < 2 then
"too small"
else
"ok"
Правила расстановки точек с запятой
Правила разделения операторов удивительно просты. Вкратце, точка с запятой всегда обозначает конец строки, кроме случаев, когда не вы
полняется одно из следующих условий.
1. Рассматриваемая строка заканчивается словом или символом, который недопустим в качестве конца оператора, например точкой или инфиксным оператором.
2. Следующая строка начинается со слова, с которого не может на
чинаться оператор.
3. Строка заканчивается внутри круглых
(...)
или квадратных
[...] скобок, потому что они не могут содержать несколько операторов.
4 .3 . Объекты-одиночки
Как упоминалось в главе 1, один из аспектов, позволяющих Scala быть бо
лее объектноориентированным языком, чем Java, заключается в том, что в классах Scala не могут содержаться статические элементы. Вместо этого
4 .3 . Объекты-одиночки 97
в Scala есть объекты-одиночки, или синглтоны. Определение объектаоди
ночки выглядит так же, как определение класса, за исключением того, что вместо ключевого слова class используется ключевое слово object
. Пример показан в листинге 4.2.
Объектодиночка в этом листинге называется
ChecksumAccumulator
, то есть носит имя, совпадающее с именем класса в предыдущем примере. Когда объ
ектодиночка использует общее с классом имя, то для класса он называется
объектом-компаньоном. И класс, и его объекткомпаньон нужно определять в одном и том же исходном файле. Класс по отношению к объектуодиночке называется классом-компаньоном. Класс и его объекткомпаньон могут об
ращаться к приватным элементам друг друга.
Листинг 4.2. Объект-компаньон для класса ChecksumAccumulator
// Этот код находится в файле ChecksumAccumulator.scala import scala.collection.mutable object ChecksumAccumulator:
private val cache = mutable.Map.empty[String, Int]
def calculate(s: String): Int =
if cache.contains(s) then cache(s)
else val acc = new ChecksumAccumulator for c <- s do acc.add((c >> 8).toByte)
acc.add(c.toByte)
val cs = acc.checksum()
cache += (s –> cs)
cs
Объектодиночка
ChecksumAccumulator располагает одним методом по имени calculate
, который получает строку
String и вычисляет контрольную сумму символов этой строки. Вдобавок он имеет одно приватное поле cache
, пред
ставленное изменяемым отображением, в котором кэшируются ранее вычис
ленные контрольные суммы
1
. В первой строке метода,
"if cache.contains(s)
1
Здесь cache используется, чтобы показать объектодиночку с полем. Кэширование с помощью поля cache помогает оптимизировать производительность, сокращая за счет расхода памяти время вычисления и разменивая расход памяти на время вычисления. Как правило, использовать кэшпамять таким образом целесообразно только в том случае, если с ее помощью можно решить проблемы производительно
сти и воспользоваться отображением со слабыми ссылками, например
WeakHashMap в scala.collection.mutable
, чтобы записи в кэшпамяти могли попадать в сборщик мусора при наличии дефицита памяти.
98 Глава 4 • Классы и объекты then"
, определяется, не содержится ли в отображении cache переданная строка в качестве ключа. Если да, то просто возвращается отображенное на этот ключ значение cache(s)
. В противном случае выполняется условие else
, которое вычисляет контрольную сумму. В первой строке условия else определяется val
переменная по имени acc
, которая инициализируется новым экземпля
ром
ChecksumAccumulator
1
. В следующей строке находится выражение for
Оно выполняет последовательный перебор каждого символа в переданной строке, преобразует символ в значение типа
Byte
, вызывая в отношении это
го символа метод toByte
, и передает результат в метод add того экземпляра
ChecksumAccumulator
, на который ссылается acc
2
. Когда завершится вычисле
ние выражения for
, в следующей строке метода в отношении acc будет вызван метод checksum
, который берет контрольную сумму для переданного значения типа
String и сохраняет ее в val
переменной по имени cs
В следующей строке, cache
+=
(s
–>
cs)
, переданный строковый ключ отобра
жается на целочисленное значение контрольной суммы, и эта пара «ключ — значение» добавляется в отображение cache
. В последнем выражении метода, cs
, обеспечивается использование контрольной суммы в качестве результата выполнения метода.
Если у вас есть опыт программирования на Java, то объектыодиночки можно представить в качестве хранилища для любых статических методов, которые вполне могли быть написаны на Java. Методы в объектаходиночках можно вызывать с помощью такого синтаксиса: имя объекта, точка, имя метода.
Например, метод calculate объектаодиночки
ChecksumAccumulator можно вызвать следующим образом:
ChecksumAccumulator.calculate("Every value is an object.")
Но объектодиночка не только хранилище статических методов. Он объект первого класса. Поэтому имя объектаодиночки можно рассматривать в ка
честве «этикетки», прикрепленной к объекту.
1
Поскольку ключевое слово new используется только для создания экземпляров классов, новый объект, созданный здесь в качестве экземпляра класса
ChecksumAc- cumulator
, не является одноименным объектомодиночкой.
2
Оператор
>>
, выполняющий побитовый сдвиг вправо, описан в разделе 5.7.
4 .4 . Case-классы 99
Определение объектаодиночки не является определением типа на том уровне абстракции, который используется в Scala. Имея лишь определение объекта
ChecksumAccumulator
, невозможно создать одноименную переменную типа.
Точнее, тип с именем
ChecksumAccumulator определяется классомкомпаньоном объектаодиночки. Тем не менее объектыодиночки расширяют суперкласс и могут подмешивать трейты. Учитывая то, что каждый объектодиночка — эк
земпляр своего суперкласса и подмешанных в него трейтов, его методы можно вызывать через эти типы, ссылаясь на него из переменных этих типов и пере
давая ему методы, ожидающие использования этих типов. Примеры объектов
одиночек, являющихся наследниками классов и трейтов, показаны в главе 12.
Одно из отличий классов от объектоводиночек состоит в том, что объекты
одиночки не могут принимать параметры, а классы — могут. Создать экзем
пляр объектаодиночки с помощью ключевого слова new нельзя, поэтому передать ему параметры не представляется возможным. Каждый объектоди
ночка реализуется как экземпляр синтетического класса, ссылка на который находится в статической переменной, поэтому у них и у статических классов
Java одинаковая семантика инициализации
1
. В частности, объектодиночка инициализируется при первом обращении к нему какоголибо кода.
Объектодиночка, который не имеет общего имени с классомкомпаньоном, называется самостоятельным. Такие объекты можно применять для решения многих задач, включая сбор в одно целое родственных вспомогательных ме
тодов или определение точки входа в приложение Scala. Именно этот случай мы и рассмотрим в следующем разделе.
4 .4 . Case-классы
Часто при написании класса вам потребуется реализация таких методов, как equals
, hashCode
, toString
— методы доступа или фабричные методы.
Их написание может занять много времени и привести к ошибкам. Scala предлагает такой инструмент, как case
классы (классыобразцы), которые могут генерировать реализации нескольких методов на основе значений, переданных его основному конструктору. Вы создаете класс case
, помещая модификатор case перед class
, например:
case class Person(name: String, age: Int)
1
В качестве имени синтетического класса используется имя объекта со знаком дол
лара. Следовательно, синтетический класс, применяемый для объектаодиночки
Check sumAccumulator
, называется
ChecksumAccumulator$
1 ... 7 8 9 10 11 12 13 14 ... 64
Option
, содержащий результат передачи исходного эле
мента
Some в функцию, переданную в map
. Вот пример преобразования startsW
в
Some
, содержащего строку
WHO
:
startsW.map(word => word.toUpperCase) // Some(WHO)
Как и в случае с
List и
Vector
, вы можете добиться того же преобразования в
Option с помощью for-yield
:
for word <- startsW yield word.toUpperCase // Some(WHO)
Если вы вызываете map с параметром
None
(параметр, который не определен), то вернете значение
None
. Например:
startsH.map(word => word.toUpperCase) // None
А вот такое же преобразование с использованием for-yield
:
for word <- startsH yield word.toUpperCase // None
Вы можете преобразовать многие другие типы с помощью map и for-yield
, но пока этого достаточно. Этот шаг дал вам представление о том, сколько кода Scala написано в виде функциональных преобразований неизменяемых структур данных.
Резюме
Знания, полученные в этой главе, позволят вам начать применять Scala для решения небольших задач, в особенности тех, для которых используются скрипты. В последующих главах мы глубже разберем рассмотренные темы, а также представим другие, не затронутые здесь.
4
Классы и объекты
В предыдущих двух главах вы разобрались в основах классов и объектов.
В этой главе вам предстоит углубленно проработать данную тему. Здесь мы дадим дополнительные сведения о классах, полях и методах, а также общее представление о том, когда подразумевается использование точки с запятой.
Кроме того, рассмотрим объектыодиночки (singleton) и то, как с их помо
щью писать и запускать приложения на Scala. Если вам уже знаком язык
Java, то вы увидите, что в Scala фигурируют похожие, но все же немного отличающиеся концепции. Поэтому чтение данной главы пойдет на пользу даже великим знатокам языка Java.
4 .1 . Классы, поля и методы
Классы — «чертежи» объектов. После определения класса из него, как по чертежу, можно создавать объекты, воспользовавшись для этого ключевым словом new
. Например, при наличии следующего определения класса:
class ChecksumAccumulator:
// Сюда помещается определение класса с отступом с помощью кода new ChecksumAccumulator можно создавать объекты
ChecksumAccumulator
Внутри определения класса помещаются поля и методы, которые в общем называются членами класса. Поля, которые определяются либо как val
, либо как var
переменные, являются переменными, относящимися к объектам.
92 Глава 4 • Классы и объекты
Методы, определяемые с помощью ключевого слова def
, содержат испол
няемый код. В полях хранятся состояние или данные объекта, а методы используют эти данные для выполнения в отношении объекта вычислений.
При создании экземпляра класса среда выполнения приложения резервирует часть памяти для хранения образа состояния получающегося при этом объ
екта (то есть содержимого его полей). Например, если вы определите класс
ChecksumAccumulator и дадите ему var
поле по имени sum
:
class ChecksumAccumulator:
var sum = 0
а потом дважды создадите его экземпляры с помощью следующего кода:
val acc = new ChecksumAccumulator val csa = new ChecksumAccumulator то образ объектов в памяти может выглядеть так:
Поскольку sum
— поле, определенное внутри класса
ChecksumAccumulator
, и относится к var
, а не к val
переменным, то впоследствии ему (полю) можно заново присвоить другое
Int
значение:
acc.sum = 3
Теперь картинка может выглядеть так:
4 .1 . Классы, поля и методы 93
По поводу этой картинки нужно отметить следующее: на ней показаны две переменные sum
. Одна из них находится в объекте, на который ссылается acc
, а другая — в объекте, на который ссылается csa
. Поля также называют пере-
менными экземпляра, поскольку каждый экземпляр получает собственный набор переменных. Все переменные экземпляра объекта составляют образ объекта в памяти. То, что здесь показано, свидетельствует не только о на
личии двух переменных sum
, но и о том, что изменение одной из них никак не отражается на другой.
В этом примере следует также отметить: у вас есть возможность изменить объект, на который ссылается acc
, даже несмотря на то, что acc относится к val
переменным. Но с учетом того, что acc
(или csa
) являются val
, а не var
переменными, вы не можете присвоить им какойнибудь другой объект.
Например, попытка, показанная ниже, не будет успешной:
// Не пройдет компиляцию, поскольку acc является val-переменной acc = new ChecksumAccumulator
Теперь вы можете рассчитывать на то, что переменная acc всегда будет ссылаться на тот же объект
ChecksumAccumulator
, с помощью которого вы ее инициализировали; поля же, содержащиеся внутри этого объекта, могут со временем измениться.
Один из важных способов обеспечения надежности объекта — гарантия того, что состояние этого объекта, то есть значения его переменных экземпляра, остается корректным в течение всего его жизненного цикла. Первый шаг к предотвращению непосредственного стороннего доступа к полям — созда
ние приватных (private) полей. Доступ к приватным полям можно получить только методами, определенными в том же самом классе, поэтому весь код, который может обновить состояние, будет локализован в классе. Чтобы объ
явить поле приватным, перед ним нужно поставить модификатор доступа private
:
class ChecksumAccumulator:
private var sum = 0
С таким определением
ChecksumAccumulator любая попытка доступа к sum за пределами класса будет неудачной:
val acc = new ChecksumAccumulator acc.sum = 5 // Не пройдет компиляцию, поскольку поле sum
// является приватным
Теперь, когда поле sum стало приватным, доступ к нему можно получить только из кода, определенного внутри тела самого класса. Следовательно,
94 Глава 4 • Классы и объекты класс
ChecksumAccumulator не будет особо полезен, пока внутри него не будут определены некоторые методы:
class ChecksumAccumulator:
private var sum = 0
def add(b: Byte): Unit =
sum += b def checksum(): Int =
return (sum & 0xFF) + 1
ПРИМЕЧАНИЕ
В Scala элементы класса делают публичными, если нет явного указания какого-либо модификатора доступа . Иначе говоря, там, где в Java ставится модификатор public, в Scala вы обходитесь простым замалчиванием . Публич- ный (public) доступ в Scala — уровень доступа по умолчанию .
Теперь у
ChecksumAccumulator есть два метода: add и checksum
, оба они демон
стрируют основную форму определения функции, показанную на рис. 2.1 1
Внутри этого метода могут использоваться любые параметры метода. Од
ной из важных характеристик параметров метода в Scala является то, что они относятся к val
, а не к var
переменным
2
. При попытке повторного присваивания значения параметру внутри метода в Scala произойдет сбой компиляции:
def add(b: Byte): Unit = b = 1 // Не пройдет компиляцию, поскольку b относится к val-переменным sum += b
Хотя методы add и checksum в данной версии
ChecksumAccumulator реали
зуют желаемые функциональные свойства вполне корректно, их можно определить в более лаконичном стиле. Вопервых, в конце метода checksum можно избавиться от лишнего слова return
. В отсутствие явно указанной инструкции return метод в Scala возвращает последнее вычисленное им значение.
1
В методе checksum используются два оператора: тильда (
) для побитового допол
нения и амперсанд (
&
) для побитового И. Оба оператора описаны в разделе 5.7.
2
Причина, по которой параметры имеют значение val
, заключается в том, что о val легче рассуждать. Вам не нужно ничего дополнительно изучать, как это делается с var
, чтобы определить, переназначается ли val
4 .1 . Классы, поля и методы 95
При написании методов рекомендуется применять стиль, исключающий явное и особенно многократное использование инструкции return
. Каждый метод нужно рассматривать в качестве выражения, выдающего одно значение, которое и является возвращаемым. Эта философия будет побуждать вас соз
давать небольшие методы и разбивать слишком крупные методы на несколько мелких. В то же время выбор конструктивного решения зависит от контекста решаемых задач, и, если того требуют условия, Scala упрощает написание методов, которые имеют несколько явно указанных возвращаемых значений.
Поскольку checksum выполняет только вычисление значений, ему не тре
буется прямая инструкция return
. Еще одним способом обобщения мето
дов является то, что если метод вычисляет только одно результирующее выражение и оно короткое, его можно поместить в ту же строку, что и сам def
. Для максимальной краткости вы можете не указывать тип результата, и Scala самостоятельно сделает его вывод. С этими изменениями класс
ChecksumAccumulator выглядит так:
class ChecksumAccumulator:
private var sum = 0
def add(b: Byte) = sum += b def checksum() = (sum & 0xFF) + 1
Несмотря на то что компилятор Scala вполне корректно выполнит вывод ре
зультирующих типов методов add и checksum
, показанных в предыдущем при
мере, читатели кода будут вынуждены вывести результирующие типы путем
логических умозаключений на основе изучения тел методов. Поэтому лучше всетаки будет всегда явно указывать результирующие типы для публичных методов, объявленных в классе, даже когда компилятор может вывести их для вас самостоятельно. Применение этого стиля показано в листинге 4.1.
Листинг 4.1. Окончательная версия класса ChecksumAccumulator
// Этот код находится в файле ChecksumAccumulator.scala class ChecksumAccumulator:
private var sum = 0
def add(b: Byte): Unit = sum += b def checksum(): Int = (sum & 0xFF) + 1
Методы с результирующим типом
Unit
, к которым относится и метод add класса
ChecksumAccumulator
, выполняются для получения побочного эф
фекта. Последний обычно определяется в виде изменения внешнего по от
ношению к методу состояния или в виде выполнения какойлибо операции вводавывода. Что касается метода add
, то побочный эффект заключается в присваивании sum нового значения. Метод, который выполняется только для получения его побочного эффекта, называется процедурой.
96 Глава 4 • Классы и объекты
4 .2 . Когда подразумевается использование точки с запятой
В программе на Scala точку с запятой в конце инструкции обычно можно не ставить. Если вся инструкция помещается на одной строке, то при же
лании можете поставить в конце данной строки точку с запятой, но это не обязательно. В то же время точка с запятой нужна, если на одной строке размещаются сразу несколько инструкций:
val s = "hello"; println(s)
Если требуется набрать инструкцию, занимающую несколько строк, то в большинстве случаев вы можете просто ее ввести, а Scala разделит инструк
ции в нужном месте. Например, следующий код рассматривается как одна инструкция, расположенная на четырех строках:
if x < 2 then
"too small"
else
"ok"
Правила расстановки точек с запятой
Правила разделения операторов удивительно просты. Вкратце, точка с запятой всегда обозначает конец строки, кроме случаев, когда не вы
полняется одно из следующих условий.
1. Рассматриваемая строка заканчивается словом или символом, который недопустим в качестве конца оператора, например точкой или инфиксным оператором.
2. Следующая строка начинается со слова, с которого не может на
чинаться оператор.
3. Строка заканчивается внутри круглых
(...)
или квадратных
[...] скобок, потому что они не могут содержать несколько операторов.
4 .3 . Объекты-одиночки
Как упоминалось в главе 1, один из аспектов, позволяющих Scala быть бо
лее объектноориентированным языком, чем Java, заключается в том, что в классах Scala не могут содержаться статические элементы. Вместо этого
4 .3 . Объекты-одиночки 97
в Scala есть объекты-одиночки, или синглтоны. Определение объектаоди
ночки выглядит так же, как определение класса, за исключением того, что вместо ключевого слова class используется ключевое слово object
. Пример показан в листинге 4.2.
Объектодиночка в этом листинге называется
ChecksumAccumulator
, то есть носит имя, совпадающее с именем класса в предыдущем примере. Когда объ
ектодиночка использует общее с классом имя, то для класса он называется
объектом-компаньоном. И класс, и его объекткомпаньон нужно определять в одном и том же исходном файле. Класс по отношению к объектуодиночке называется классом-компаньоном. Класс и его объекткомпаньон могут об
ращаться к приватным элементам друг друга.
Листинг 4.2. Объект-компаньон для класса ChecksumAccumulator
// Этот код находится в файле ChecksumAccumulator.scala import scala.collection.mutable object ChecksumAccumulator:
private val cache = mutable.Map.empty[String, Int]
def calculate(s: String): Int =
if cache.contains(s) then cache(s)
else val acc = new ChecksumAccumulator for c <- s do acc.add((c >> 8).toByte)
acc.add(c.toByte)
val cs = acc.checksum()
cache += (s –> cs)
cs
Объектодиночка
ChecksumAccumulator располагает одним методом по имени calculate
, который получает строку
String и вычисляет контрольную сумму символов этой строки. Вдобавок он имеет одно приватное поле cache
, пред
ставленное изменяемым отображением, в котором кэшируются ранее вычис
ленные контрольные суммы
1
. В первой строке метода,
"if cache.contains(s)
1
Здесь cache используется, чтобы показать объектодиночку с полем. Кэширование с помощью поля cache помогает оптимизировать производительность, сокращая за счет расхода памяти время вычисления и разменивая расход памяти на время вычисления. Как правило, использовать кэшпамять таким образом целесообразно только в том случае, если с ее помощью можно решить проблемы производительно
сти и воспользоваться отображением со слабыми ссылками, например
WeakHashMap в scala.collection.mutable
, чтобы записи в кэшпамяти могли попадать в сборщик мусора при наличии дефицита памяти.
98 Глава 4 • Классы и объекты then"
, определяется, не содержится ли в отображении cache переданная строка в качестве ключа. Если да, то просто возвращается отображенное на этот ключ значение cache(s)
. В противном случае выполняется условие else
, которое вычисляет контрольную сумму. В первой строке условия else определяется val
переменная по имени acc
, которая инициализируется новым экземпля
ром
ChecksumAccumulator
1
. В следующей строке находится выражение for
Оно выполняет последовательный перебор каждого символа в переданной строке, преобразует символ в значение типа
Byte
, вызывая в отношении это
го символа метод toByte
, и передает результат в метод add того экземпляра
ChecksumAccumulator
, на который ссылается acc
2
. Когда завершится вычисле
ние выражения for
, в следующей строке метода в отношении acc будет вызван метод checksum
, который берет контрольную сумму для переданного значения типа
String и сохраняет ее в val
переменной по имени cs
В следующей строке, cache
+=
(s
–>
cs)
, переданный строковый ключ отобра
жается на целочисленное значение контрольной суммы, и эта пара «ключ — значение» добавляется в отображение cache
. В последнем выражении метода, cs
, обеспечивается использование контрольной суммы в качестве результата выполнения метода.
Если у вас есть опыт программирования на Java, то объектыодиночки можно представить в качестве хранилища для любых статических методов, которые вполне могли быть написаны на Java. Методы в объектаходиночках можно вызывать с помощью такого синтаксиса: имя объекта, точка, имя метода.
Например, метод calculate объектаодиночки
ChecksumAccumulator можно вызвать следующим образом:
ChecksumAccumulator.calculate("Every value is an object.")
Но объектодиночка не только хранилище статических методов. Он объект первого класса. Поэтому имя объектаодиночки можно рассматривать в ка
честве «этикетки», прикрепленной к объекту.
1
Поскольку ключевое слово new используется только для создания экземпляров классов, новый объект, созданный здесь в качестве экземпляра класса
ChecksumAc- cumulator
, не является одноименным объектомодиночкой.
2
Оператор
>>
, выполняющий побитовый сдвиг вправо, описан в разделе 5.7.
4 .4 . Case-классы 99
Определение объектаодиночки не является определением типа на том уровне абстракции, который используется в Scala. Имея лишь определение объекта
ChecksumAccumulator
, невозможно создать одноименную переменную типа.
Точнее, тип с именем
ChecksumAccumulator определяется классомкомпаньоном объектаодиночки. Тем не менее объектыодиночки расширяют суперкласс и могут подмешивать трейты. Учитывая то, что каждый объектодиночка — эк
земпляр своего суперкласса и подмешанных в него трейтов, его методы можно вызывать через эти типы, ссылаясь на него из переменных этих типов и пере
давая ему методы, ожидающие использования этих типов. Примеры объектов
одиночек, являющихся наследниками классов и трейтов, показаны в главе 12.
Одно из отличий классов от объектоводиночек состоит в том, что объекты
одиночки не могут принимать параметры, а классы — могут. Создать экзем
пляр объектаодиночки с помощью ключевого слова new нельзя, поэтому передать ему параметры не представляется возможным. Каждый объектоди
ночка реализуется как экземпляр синтетического класса, ссылка на который находится в статической переменной, поэтому у них и у статических классов
Java одинаковая семантика инициализации
1
. В частности, объектодиночка инициализируется при первом обращении к нему какоголибо кода.
Объектодиночка, который не имеет общего имени с классомкомпаньоном, называется самостоятельным. Такие объекты можно применять для решения многих задач, включая сбор в одно целое родственных вспомогательных ме
тодов или определение точки входа в приложение Scala. Именно этот случай мы и рассмотрим в следующем разделе.
4 .4 . Case-классы
Часто при написании класса вам потребуется реализация таких методов, как equals
, hashCode
, toString
— методы доступа или фабричные методы.
Их написание может занять много времени и привести к ошибкам. Scala предлагает такой инструмент, как case
классы (классыобразцы), которые могут генерировать реализации нескольких методов на основе значений, переданных его основному конструктору. Вы создаете класс case
, помещая модификатор case перед class
, например:
case class Person(name: String, age: Int)
1
В качестве имени синтетического класса используется имя объекта со знаком дол
лара. Следовательно, синтетический класс, применяемый для объектаодиночки
Check sumAccumulator
, называется
ChecksumAccumulator$
1 ... 7 8 9 10 11 12 13 14 ... 64
100 Глава 4 • Классы и объекты
С добавлением модификатора case компилятор сгенерирует для вас несколь
ко полезных методов. Вопервых, компилятор создаст объекткомпаньон и поместит в него фабричный метод с именем apply
. Таким образом, вы можете создать новый объект
Person следующим образом:
val p = Person("Sally", 39)
Компилятор перепишет эту строку кода в вызов сгенерированного фабрич
ного метода:
Person.apply("Sally",
39)
Вовторых, компилятор будет хранить все параметры класса в полях и ге
нерировать методы доступа с тем же именем, что и у заданного параметра
1
Например, вы можете получить доступ к заданным в
Person значениям име
ни и возраста следующим образом:
p.name // Sally p.age // 39
Втретьих, компилятор предоставит вам реализацию toString
:
p.toString // Person(Sally,39)
Вчетвертых, компилятор сгенерирует реализацию hashCode и equals для вашего класса. Эти методы будут основывать свой результат на параметрах, переданных конструктору. Например, объект
Person будет учитывать и имя, и возраст при сравнении:
p == Person("Sally", 21) // false p.hashCode == Person("Sally", 21).hashCode // false p == Person("James", 39) // false p.hashCode == Person("James", 39).hashCode // false p == Person("Sally", 39) // true p.hashCode == Person("Sally", 39).hashCode // true
Компилятор не будет генерировать метод, который вы реализуете самостоя
тельно. Он будет использовать вашу реализацию. Вы также можете добавить другие поля и методы к классу и его компаньону. Вот пример, в котором вы определяете метод apply в сопутствующем объекте
Person
(компилятор не будет его генерировать) и добавляете метод appendToName в класс:
case class Person(name: String, age: Int):
def appendToName(suffix: String): Person =
Person(s"$name$suffix", age)
1
Они называются параметрическими полями, которые будут описаны в разделе 10.6.
4 .5 . Приложение на языке Scala 101
object Person:
// Убедитесь, что непустое имя написано с заглавной буквы def apply(name: String, age: Int): Person =
val capitalizedName =
if !name.isEmpty then val firstChar = name.charAt(0).toUpper val restOfName = name.substring(1)
s"$firstChar$restOfName"
else throw new IllegalArgumentException("Empty name")
new Person(capitalizedName, age)
Этот apply
метод гарантирует, что первый символ имени будет начинаться с заглавной буквы:
val q = Person("sally", 39) // Person(Sally,39)
Вы также можете вызвать метод appendToName
, который вы определили в классе:
q.appendToName(" Smith") // Person(Sally Smith,39)
Наконец, компилятор добавляет метод copy в ваш класс и метод unapply к компаньону. Они будут описаны в главе 13.
Все эти условности облегчают работу, но с небольшой оговоркой: вам всего лишь понадобится написать модификатор case
, а ваши классы и объекты при этом станут немного больше. Они вырастают, потому что генерируются дополнительные методы и для каждого параметра конструктора добавляется неявное поле.
4 .5 . Приложение на языке Scala
Чтобы запустить программу на Scala, нужно предоставить имя автономного объектаодиночки с методом main
, который получает один параметр с типом
Array[String]
и имеет результирующий тип
Unit
. Точкой входа в приложе
ние может стать любой самостоятельный объект с методом main
, имеющим надлежащую сигнатуру
1
. Пример показан в листинге 4.3.
Листинг 4.3. Приложение Summer
// Код находится в файле Summer.scala import ChecksumAccumulator.calculate
1
Вы можете обозначить методы другими именами в качестве основных функций с помощью
@main
. Этот метод будет описан в разделе 23.3.
102 Глава 4 • Классы и объекты object Summer:
def main(args: Array[String]): Unit =
for arg <- args do println(arg + ": " + calculate(arg))
Объектодиночка, показанный в данном листинге, называется
Summer
. Его метод main имеет надлежащую сигнатуру, поэтому его можно задействовать в качестве приложения. Первая инструкция в файле импортирует метод calculate
, который определен в объекте
ChecksumAccumulator из предыду
щего примера. Инструкция import позволяет далее использовать в файле простое имя метода
1
. Тело метода main всего лишь выводит на стандартное устройство каждый аргумент и контрольную сумму для аргумента, разделяя их двоеточием.
ПРИМЕЧАНИЕ
Подразумевается, что в каждый свой исходный файл Scala импортирует элементы пакетов java .lang и scala, а также элементы объекта-одиночки по имени Predef . В Predef, который находится в пакете scala, содержится множество полезных методов . Например, когда в исходном файле Scala встречается println, фактически вызывается println из Predef . (А метод
Predef .println, в свою очередь, вызывает метод Console .println, который фактически и выполняет всю работу .) Когда же встречается assert, вы- зывается метод Predef .assert .
Чтобы запустить приложение Summer, поместите код из листинга 4.3 в файл
Summer.scala
. В Summer используется
ChecksumAccumulator
, поэтому поме
стите код для
ChecksumAccumulator как для класса, показанного в листин
ге 4.1, так и для его объектакомпаньона, показанного в листинге 4.2, в файл
ChecksumAccumulator.scala
Одним из отличий Scala от Java является то, что в Java от вас требуется поме
стить публичный класс в файл, названный по имени класса, например, класс
SpeedRacer
— в файл
SpeedRacer.java
. А в Scala файл с расширением
.scala можно называть как угодно независимо от того, какие классы Scala или код в них помещаются. Но обычно, когда речь идет не о скриптах, рекоменду
ется придерживаться стиля, при котором файлы называются по именам включенных в них классов, как это делается в Java, чтобы программистам было легче искать классы по именам их файлов. Именно этим подходом мы
1
Наличие опыта программирования на Java позволяет сопоставить такой импорт с объявлением статического импорта, введенным в Java 5. Единственное отли
чие — в Scala импортировать элементы можно из любого объекта, а не только из объектоводиночек.
Резюме 103
и воспользовались в отношении двух файлов в данном примере. Имеются в виду файлы
Summer.scala и
ChecksumAccumulator.scala
Ни
ChecksumAccumulator.scala
, ни
Summer.scala не являются скриптами, поскольку заканчиваются определением. В отличие от этого скрипт должен заканчиваться выражением, выдающим результат. Поэтому при попытке запустить
Summer.scala в качестве скрипта интерпретатор Scala пожалуется на то, что
Summer.scala не заканчивается выражением, выдающим результат.
(Конечно, если предположить, что вы самостоятельно не добавили какое
либо выражение после определения объекта
Summer
.) Вместо этого нужно будет скомпилировать данные файлы с помощью компилятора Scala, а затем запустить получившиеся в результате файлы классов. Для этого можно вос
пользоваться основным компилятором Scala по имени scalac
:
$ scalac ChecksumAccumulator.scala Summer.scala
Эта команда скомпилирует ваши исходные файлы и приведет к созданию файлов классов Java, которые затем можно будет запускать через команду scala
— ту же самую, с помощью которой вы вызывали интерпретатор в пре
дыдущих примерах. Однако вместо того, чтобы указывать ему имя файла с расширением
.scala
, содержащим код Scala для интерпретации (как вы делали в каждом предыдущем примере)
1
, вы дадите ему имя отдельного объекта, содержащего метод main с соответствующей сигнатурой. Следова
тельно, приложение
Summer можно запустить, набрав команду:
$ scala Summer of love
Вы сможете увидеть контрольные суммы, выведенные для двух аргументов командной строки:
of: -213
love: -182
Резюме
В этой главе мы рассмотрели основы классов и объектов в Scala и показали приемы компиляции и запуска приложений. В следующей главе рассмотрим основные типы данных и варианты их использования.
1
Фактический механизм, который программа Scala использует для «интерпрета
ции» исходного файла Scala, заключается в том, что она компилирует исходный код Scala в байткоды Java, немедленно загружает их через загрузчик классов и выполняет их.
5
Основные типы и операции
После того как были рассмотрены в действии классы и объекты, самое время поглубже изучить имеющиеся в Scala основные типы и операции.
Если вы хорошо знакомы с Java, то вас может обрадовать тот факт, что в Scala и в Java основные типы и операторы имеют тот же смысл. И все же есть интересные различия, ради которых с этой главой стоит ознакомиться даже тем, кто считает себя опытным разработчиком Javaприложений. Не
которые аспекты Scala, рассматриваемые в данной главе, в основном такие же, как и в Java, поэтому мы указываем, какие разделы Javaразработчики могут пропустить.
В текущей главе мы представим обзор основных типов Scala, включая стро
ки типа
String и типы значений
Int
,
Long
,
Short
,
Byte
,
Float
,
Double
,
Char и
Boolean
. Кроме того, рассмотрим операции, которые могут выполняться с этими типами, и вопросы соблюдения приоритета операторов в выражени
ях Scala. Поговорим мы и о том, как Scala «обогащает» варианты основных типов, позволяя выполнять дополнительные операции вдобавок к тем, что поддерживаются в Java.
5 .1 . Некоторые основные типы
В табл. 5.1 показан ряд основных типов, используемых в Scala, а также диа
пазоны значений, которые могут принимать их экземпляры. В совокупности типы
Byte
,
Short
,
Int
,
Long и
Char называются целочисленными. Целочислен
ные типы плюс
Float и
Double называются числовыми.
5 .2 . Литералы 105
Таблица 5.1. Некоторые основные типы
Основной тип
Диапазон
Byte
8битовое знаковое целое число в дополнительном коде
(от –2 7
до 2 7
– 1 включительно)
Short
16битовое знаковое целое число в дополнительном коде
(от –2 15
до 2 15
– 1 включительно)
Int
32битовое знаковое целое число в дополнительном коде
(от –2 31
до 2 31
– 1 включительно)
Long
64битовое знаковое целое число в дополнительном коде
(от –2 63
до 2 63
– 1 включительно)
Char
16битовый беззнаковый Unicodeсимвол
(от 0 до 2 16
– 1 включительно)
String
Последовательность из Char
Float
32битовое число с плавающей точкой одинарной точности, которое соответствует стандарту IEEE 754
Double
64битовое число с плавающей точкой двойной точности, которое соответствует стандарту IEEE 754
Boolean true или false
За исключением типа
String
, который находится в пакете java.lang
, все типы, показанные в данной таблице, входят в пакет scala
1
. Например, полное имя типа
Int обозначается scala.Int
. Но, учитывая, что все элементы пакета scala и java.lang автоматически импортируются в каждый исходный файл
Scala, можно повсеместно использовать только простые имена, то есть имена вида
Boolean
,
Char или
String
Опытные Javaразработчики заметят, что основные типы Scala имеют в точно
сти такие же диапазоны, как и соответствующие им типы в Java. Это позволяет компилятору Scala в создаваемом им байткоде преобразовывать экземпляры
типов значений Scala, например
Int или
Double
, в примитивные типы Java.
5 .2 . Литералы
Все основные типы, перечисленные в табл. 5.1, можно записать с помощью
литералов. Литерал представляет собой способ записи постоянного значения непосредственно в коде.
1
Пакеты, кратко рассмотренные в шаге 1 главы 2, более подробно рассматриваются в главе 12.
106 Глава 5 • Основные типы и операции
УСКОРЕННЫЙ РЕЖИМ ЧТЕНИЯ ДЛЯ JAVA-ПРОГРАММИСТОВ
Синтаксис большинства литералов, показанных в данном разделе, совпадает с синтаксисом, применяемым в Java, поэтому знатоки Java могут спокойно пропустить практически весь раздел . Отдельные различия, о которых стоит прочитать, касаются используемых в Scala неформатированных строк (рас- сматриваются в подразделе «Строковые литералы»), а также интерполяции строк . Кроме того, в Scala не поддерживаются восьмеричные литералы, а целочисленные, начинающиеся с нуля, например 031, не проходят ком- пиляцию .
Целочисленные литералы
Целочисленные литералы для типов
Int
,
Long
,
Short и
Byte используются в двух видах: десятичном и шестнадцатеричном. Способ, применяемый для начала записи целочисленного литерала, показывает основание числа. Если число начинается с
0x или
0X
, то оно шестнадцатеричное (по основанию 16) и может содержать цифры от 0 до 9, а также буквы от A до F в верхнем или нижнем регистре. Вы можете использовать символы подчеркивания (_), чтобы улучшить читаемость больших значений, например:
val hex = 0x5 // 5: Int val hex2 = 0x00FF // 255: Int val magic = 0xcafebabe // -889275714: Int val billion = 1_000_000_000 // 1000000000: Int
Обратите внимание на то, что оболочка Scala REPL всегда выводит целочис
ленные значения в десятичном виде независимо от формы литерала, которую вы могли задействовать для инициализации этих значений. Таким образом,
REPL показывает значение переменной hex2
, которая была инициализиро
вана с помощью литерала
0x00FF
, как десятичное число
255
. (Разумеется, не нужно все принимать на веру. Хорошим способом начать осваивать язык станет практическая работа с этими инструкциями в интерпретаторе по мере чтения данной главы.) Если цифра, с которой начинается число, не ноль и не имеет никаких других знаков отличия, значит, число десятичное (по основанию 10), например:
val dec1 = 31 // 31: Int val dec2 = 255 // 255: Int val dec3 = 20 // 20: Int
Если целочисленный литерал заканчивается на
L
или l
, значит, показывает число типа
Long
, в противном случае это число относится к типу
Int
. По
смотрите на примеры целочисленных литералов
Long
:
5 .2 . Литералы 107
val prog = 0XCAFEBABEL // 3405691582: Long val tower = 35L // 35: Long val of = 31l // 31: Long
Если
Int
литерал присваивается переменной типа
Short или
Byte
, то рас
сматривается как принадлежащий к типу
Short или
Byte
, если, конечно, его значение находится внутри диапазона, допустимого для данного типа, например:
val little: Short = 367 // 367: Short val littler: Byte = 38 // 38: Byte
Литералы чисел с плавающей точкой
Литералы чисел с плавающей точкой состоят из десятичных цифр, которые также могут содержать необязательный символ десятичной точки, и после них может стоять необязательный символ
E
или e
и экспонента. Посмотрите на примеры литералов чисел с плавающей точкой:
val big = 1.2345 // 1.2345: Double val bigger = 1.2345e1 // 12.345: Double val biggerStill = 123E45 // 1.23E47: Double val trillion = 1_000_000_000e3 // 1.0E12: Double
Обратите внимание: экспонента означает степень числа 10, на которую умножается остальная часть числа. Следовательно,
1.2345e1
равняется числу 1,2345, умноженному на 10, то есть получается число 12,345. Если литерал числа с плавающей точкой заканчивается на
F
или f
, значит, число относится к типу
Float
, в противном случае оно относится к типу
Double
Дополнительно литералы чисел с плавающей точкой могут заканчиваться на
D
или d
. Посмотрите на примеры литералов чисел с плавающей точкой:
val little = 1.2345F // 1.2345: Float val littleBigger = 3e5f // 300000.0: Float
Последнее значение, выраженное как тип
Double
, может также принимать иную форму:
val anotherDouble = 3e5 // 300000.0: Double val yetAnother = 3e5D // 300000.0: Double
Большие числовые литералы
В Scala 3 добавлена экспериментальная функция, которая устраняет огра
ничения на размер числовых литералов и позволяет использовать их для