Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
Скачать 6.24 Mb.
|
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$ |