Главная страница
Навигация по странице:

  • Листинг 4.1.

  • Правила расстановки точек с запятой

  • Листинг 4.2.

  • Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста


    Скачать 6.24 Mb.
    НазваниеОдерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
    Дата27.04.2023
    Размер6.24 Mb.
    Формат файлаpdf
    Имя файлаScala. Профессиональное программирование 2022.pdf
    ТипДокументы
    #1094967
    страница11 из 64
    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


    написать администратору сайта