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

  • Различия операторов == в Scala и Java

  • Таблица 5.3.

  • Таблица 5.4.

  • Таблица 5.5.

  • Плюсы и минусы неизменяемого объекта

  • 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
    страница14 из 64
    1   ...   10   11   12   13   14   15   16   17   ...   64

    119
    чего в двоичном виде получается число
    00000000000000000000000000000100
    , или
    4 5 .8 . Равенство объектов
    Если нужно сравнить два объекта на равенство, то можно воспользоваться либо методом
    ==
    , либо его противоположностью — методом
    !=
    . Вот несколь­
    ко простых примеров:
    1 == 2 // false: Boolean
    1 != 2 // true: Boolean
    2 == 2 // true: Boolean
    По сути, эти две операции применимы ко всем объектам, а не только к ос­
    новным типам. Например, оператор
    ==
    можно использовать для сравнения списков:
    List(1, 2, 3) == List(1, 2, 3) // true: Boolean
    List(1, 2, 3) == List(4, 5, 6) // false: Boolean
    Если пойти еще дальше, то можно сравнить два объекта, имеющих разные типы:
    1 == 1.0 // true: Boolean
    List(1, 2, 3) == "hello" // false: Boolean
    Можно даже выполнить сравнение со значением null или с тем, что может иметь данное значение. Никакие исключения при этом выдаваться не будут:
    List(1, 2, 3) == null // false: Boolean null == List(1, 2, 3) // false: Boolean
    Как видите, оператор
    ==
    реализован весьма искусно, и вы в большинстве слу­
    чаев получите то сравнение на равенство, которое вам нужно. Все делается по очень простому правилу: сначала левая часть проверяется на null
    . Если ее значение не null
    , то вызывается метод equals
    . Ввиду того что equals
    — метод, точность получаемого сравнения зависит от типа левого аргумента. Проверка на null выполняется автоматически, поэтому вам не нужно проводить ее
    1
    Этот вид сравнения выдает true в отношении различных объектов, если их содержимое одинаково и их методы equals созданы на основе проверки
    1
    Автоматическая проверка игнорирует правую сторону, но любой корректно реализо­
    ванный метод equals должен возвращать false
    , если его аргумент имеет значение null

    120 Глава 5 • Основные типы и операции содержимого. Например, вот как сравниваются две строки, в которых по пять одинаковых букв:
    ("he" + "llo") == "hello" // true: Boolean
    Различия операторов == в Scala и Java
    В Java оператор
    ==
    может использоваться для сравнения как при­
    митивных, так и ссылочных типов. В отношении примитивных ти­
    пов оператор
    ==
    в Java проверяет равенство значений, как и в Scala.
    Но в отношении ссылочных типов оператор
    ==
    в Java проверяет ра-
    венство ссылок. Это значит, две переменные указывают на один и тот же объект в куче, принадлежащей JVM. Scala также предоставляет средство eq для сравнения равенства ссылок. Но метод eq и его про­
    тивоположность, метод ne
    , применяются только к объектам, которые непосредственно отображаются на объекты Java. Исчерпывающие подробности о eq и ne приводятся в разделах 17.1 и 17.2. Кроме того, в главе 8 показано, как создавать хорошие методы equals
    5 .9 . Приоритет и ассоциативность операторов
    Приоритет операторов определяет, какая часть выражения вычисляется самой первой. Например, выражение
    2
    +
    2
    *
    7
    вычисляется в
    16
    , а не в
    28
    , поскольку оператор
    *
    имеет более высокий приоритет, чем оператор
    +
    Поэтому та часть выражения, в которой требуется перемножить числа, вы­
    числяется до того, как будет выполнена часть, в которой числа складыва­
    ются. Разумеется, чтобы уточнить в выражении порядок вычисления или переопределить приоритеты, можно воспользоваться круглыми скобками.
    Например, если вы действительно хотите, чтобы результат вычисления ранее показанного выражения был
    28
    , то можете набрать следующее вы­
    ражение:
    (2 + 2) * 7
    Если учесть, что в Scala, по сути, нет операторов, а есть только способ приме­
    нения методов в форме записи операторов, то возникает вопрос: а как тогда работает приоритет операторов? Scala принимает решение о приоритете на основе первого символа метода, использованного в форме записи операторов
    (из этого правила есть одно исключение, рассматриваемое ниже). Если имя метода начинается, к примеру, с
    *
    , то он получит более высокий приоритет,

    5 .9 . Приоритет и ассоциативность операторов 121
    чем метод, чье имя начинается на
    +
    . Следовательно, выражение
    2
    +
    2
    *
    7
    будет вычислено как
    2
    +
    (2
    *
    7)
    . Аналогично этому выражение a
    +++
    b
    ***
    c
    , в котором a
    , b
    и c
    — переменные, а
    +++
    и
    ***
    — методы, будет вычислено как a
    +++
    (b
    ***
    c)
    , поскольку метод
    ***
    обладает более высоким уровнем прио­
    ритета, чем метод
    +++
    В табл. 5.3 показан приоритет применительно к первому символу метода в убывающем порядке, где символы, показанные на одной строке, опре­
    деляют одинаковый уровень приоритета. Чем выше символ в списке, тем выше приоритет начинающегося с него метода. Вот пример, показывающий влияние приоритета:
    2 << 2 + 2 // 32: Int
    Таблица 5.3. Приоритет операторов
    (Все специальные символы)
    * / %
    + –
    :
    = !
    < >
    &
    ^
    |
    (Все буквы)
    (Все операторы присваивания)
    Имя метода
    <<
    начинается с символа
    <
    , который появляется в приведенном списке ниже символа
    +
    — первого и единственного символа метода
    +
    . Следо­
    вательно,
    <<
    будет иметь более низкий уровень приоритета, чем
    +
    , и выраже­
    ние будет вычислено путем вызова сначала метода
    +
    , а затем метода
    <<
    , как в выражении
    2
    <<
    (2
    +
    2)
    . При сложении
    2
    +
    2
    в результате математического действия получается
    4
    , а вычисление выражения
    2
    <<
    4
    дает результат
    32
    Если поменять операторы местами, то будет получен другой результат:
    2 + 2 << 2 // 16: Int
    Поскольку первые символы, по сравнению с предыдущим примером, не изменились, то методы будут вызваны в том же порядке:
    +
    , а затем
    <<
    . Сле­
    довательно,
    2
    +
    2
    опять будет равен
    4
    , а
    4
    <<
    2
    даст результат
    16

    122 Глава 5 • Основные типы и операции
    Единственное исключение из правил, о существовании которого уже гово­
    рилось, относится к операторам присваивания, заканчивающимся знаком равенства. Если оператор заканчивается знаком равенства (
    =
    ) и не относится к одному из операторов сравнения
    <=
    ,
    >=
    ,
    ==
    и
    !=
    , то приоритет оператора имеет такой же уровень, что и простое присваивание (
    =
    ). То есть он ниже приоритета любого другого оператора. Например:
    x *= y + 1
    означает то же самое, что и x *= (y + 1)
    поскольку оператор
    *=
    классифицируется как оператор присваивания, прио­
    ритет которого ниже, чем у
    +
    , даже притом что первым символом оператора выступает знак
    *
    , который обозначил бы приоритет выше, чем у
    +
    Если в выражении рядом появляются операторы с одинаковым уровнем приоритета, то способ группировки операторов определяется их ассоциатив-
    ностью. Ассоциативность оператора в Scala определяется по его последнему символу. Как уже упоминалось в главе 3, любой метод, имя которого за­
    канчивается символом
    :
    , вызывается в отношении своего правого операнда с передачей ему левого. Методы, в окончании имени которых используются любые другие символы, действуют наоборот: они вызываются в отношении своего левого операнда с передачей себе правого. То есть из выражения a
    *
    b получается a.*(b)
    , но из a
    :::
    b получается b.:::(a)
    Но независимо от того, какова ассоциативность оператора, его операнды всегда вычисляются слева направо. Следовательно, если a
    — выражение, не являющееся простой ссылкой на неизменяемое значение, то выраже­
    ние a
    :::
    b при более точном рассмотрении представляется следующим блоком:
    { val x = a; b.:::(x) }
    В этом блоке а
    по­прежнему вычисляется раньше b
    , а затем результат данного вычисления передается в качестве операнда принадлежащему b
    методу
    :::
    Это правило ассоциативности играет роль также при появлении в одном выражении рядом сразу нескольких операторов с одинаковым уровнем приоритета. Если имена методов заканчиваются на
    :
    , они группируются справа налево, в противном случае — слева направо. Например, a
    :::
    b
    :::
    c рассматривается как a
    :::
    (b
    :::
    c)
    . Но a
    *
    b
    *
    c
    , в отличие от этого, рассма­
    тривается как
    (a
    *
    b)
    *
    c

    5 .10 . Обогащающие операции 123
    Правила приоритета операторов — часть языка Scala, и вам не следует боять­
    ся применять ими. При этом, чтобы прояснить первоочередность использо­
    вания операторов, в некоторых выражениях все же лучше прибегнуть к кру­
    глым скобкам. Пожалуй, единственное, на что можно реально рассчитывать в отношении знания порядка приоритета другими программистами, — то, что мультипликативные операторы
    *
    ,
    /
    и
    %
    имеют более высокий уровень приоритета, чем аддитивные
    +
    и

    . Таким образом, даже если выражение a
    +
    b
    <<
    c выдает нужный результат и без круглых скобок, стоит внести до­
    полнительную ясность с помощью записи
    (a
    +
    b)
    <<
    c
    . Это снизит количество нелестных отзывов ваших коллег по поводу использованной вами формы записи операторов, которое выражается, к примеру, в недовольном воскли­
    цании вроде «Опять в его коде невозможно разобраться!» и отправке вам сообщения наподобие bills
    !*&ˆ%


    code!
    1 5 .10 . Обогащающие операции
    В отношении основных типов Scala можно вызвать намного больше методов, чем рассмотрено в предыдущих разделах. Некоторые примеры показаны в табл. 5.4. Начиная со Scala 3, эти методы доступны через неявные преоб­
    разования — устаревшую технику, которая в конечном итоге будет заменена методами расширения, подробно описанными в главе 22. А пока вам нужно знать лишь то, что для каждого основного типа, рассмотренного в текущей
    Таблица 5.4. Некоторые обогащающие операции
    Код
    Результат
    0 max 5 5
    0 min 5 0
    –2.7 abs
    2.7
    –2.7 round
    –3L
    1.5 isInfinity
    False
    (1.0 / 0) isInfinity
    True
    4 to 6
    Range(4, 5, 6)
    "bob" capitalize
    "Bob"
    "robert" drop 2
    "bert"
    1
    Теперь вы уже знаете, что, получив такой код, компилятор Scala создаст вызов
    (
    bills.!*&^%(code)).!

    124 Глава 5 • Основные типы и операции главе, существует обогащающая оболочка, которая предоставляет ряд допол­
    нительных методов. Поэтому увидеть все доступные методы, применяемые в отношении основных типов, можно, обратившись к документации по API, которая касается обогащающей оболочки для каждого основного типа. Эти классы перечислены в табл. 5.5.
    Таблица 5.5. Классы обогащающих оболочек
    Основной тип
    Обогащающая оболочка
    Byte scala.runtime.RichByte
    Short scala.runtime.RichShort
    Int scala.runtime.RichInt
    Long scala.runtime.RichLong
    Char scala.runtime.RichChar
    Float scala.runtime.RichFloat
    Double scala.runtime.RichDouble
    Boolean scala.runtime.RichBoolean
    String scala.collection.immutable.StringOps
    Резюме
    Основное, что следует усвоить, прочитав данную главу, — операторы в Scala являются вызовами методов и для основных типов Scala существуют неяв­
    ные преобразования в обогащенные варианты, которые добавляют дополни­
    тельные полезные методы. В главе 6 мы покажем, что означает конструиро­
    вание объектов в функциональном стиле, обеспечивающее новые реализации некоторых операторов, рассмотренных в настоящей главе.

    6
    Функциональные объекты
    Усвоив основы, рассмотренные в предыдущих главах, вы готовы разработать больше полнофункциональных классов Scala. В этой главе основное вни­
    мание мы уделим классам, определяющим функциональные объекты или объекты, не имеющие никакого изменяемого состояния. Запуская примеры, мы создадим несколько вариантов класса, моделирующего рациональные числа в виде неизменяемых объектов. Попутно будут показаны дополни­
    тельные аспекты объектно­ориентированного программирования на Scala: параметры класса и конструкторы, методы и операторы, приватные члены, переопределение, проверка соблюдения предварительных условий, пере­
    грузка и рекурсивные ссылки.
    6 .1 . Спецификация класса Rational
    Рациональным называется число, которое может быть выражено соотноше­
    нием n
    /
    d
    , где n
    и d
    представлены целыми числами, за исключением того, что d
    не может быть нулем. Здесь n
    называется числителем, а d
    знаменателем.
    Примерами рациональных чисел могут послужить 1/2, 2/3, 112/239 и 2/1.
    В сравнении с числами с плавающей точкой рациональные числа имеют то преимущество, что дроби представлены точно, без округлений или при­
    ближений.
    Разрабатываемый в этой главе класс должен моделировать поведение рацио­
    нальных чисел, позволяя производить над ними арифметические действия по сложению, вычитанию, умножению и делению. Для сложения двух ра­
    циональных чисел сначала нужно получить общий знаменатель, после чего сложить два числителя. Например, чтобы выполнить сложение 1/2 + 2/3,

    126 Глава 6 • Функциональные объекты обе части левого операнда умножаются на 3, а обе части правого операнда — на 2, в результате чего получается 3/6 + 4/6. Сложение двух числителей дает результат 7/6. Для перемножения двух рациональных чисел можно просто перемножить их числители, а затем знаменатели. Таким образом, 1/2 · 2/5 дает число 2/10, которое можно представить более кратко в нормализованном виде как 1/5. Деление выполняется путем перестановки местами числителя и зна­
    менателя правого операнда с последующим перемножением чисел. Например,
    1/2 / 3/5 — то же самое, что и 1/2 · 5/3, в результате получается число 5/6.
    Одно, возможно, очевидное наблюдение заключается в том, что в математике рациональные числа не имеют изменяемого состояния. Можно сложить два рациональных числа, и результатом будет новое рациональное число. Исход­
    ные числа не будут изменены. Неизменяемый класс
    Rational
    , разрабатыва­
    емый в данной главе, будет иметь такое же свойство. Каждое рациональное число будет представлено одним объектом
    Rational
    . При сложении двух объ­
    ектов
    Rational для хранения суммы будет создаваться новый объект
    Rational
    В этой главе мы представим некоторые допустимые в Scala способы написа­
    ния библиотек, которые создают впечатление, будто используется поддерж­
    ка, присущая непосредственно самому языку программирования. Например, в конце этой главы вы сможете сделать с классом
    Rational следующее:
    scala> val oneHalf = Rational(1, 2)
    val oneHalf: Rational = 1/2
    scala> val twoThirds = Rational(2, 3)
    val twoThirds: Rational = 2/3
    scala> (oneHalf / 7) + (1 — twoThirds)
    val res0: Rational = 17/42 6 .2 . Конструирование класса Rational
    Конструирование класса
    Rational неплохо начать с рассмотрения того, как клиенты­программисты будут создавать новый объект
    Rational
    . Было ре­
    шено создавать объекты
    Rational неизменяемыми, и потому мы потребуем, чтобы эти клиенты при создании экземпляра предоставляли все необходи­
    мые ему данные (в нашем случае числитель и знаменатель). Поэтому начнем конструирование со следующего кода:
    class Rational(n: Int, d: Int)
    По поводу этой строки кода в первую очередь следует заметить: если у класса нет тела, то вам не нужно ставить пустые фигурные скобки, а также нет необ­

    6 .2 . Конструирование класса Rational 127
    ходимости завершать строку двоеточием. Идентификаторы n
    и d
    , указанные в круглых скобках после имени класса,
    Rational
    , называются параметрами
    класса. Компилятор Scala подберет эти два параметра и создаст первичный
    конструктор, получающий их же.
    Плюсы и минусы неизменяемого объекта
    Неизменяемые объекты имеют ряд преимуществ над изменяемы­
    ми и один потенциальный недостаток. Во­первых, о неизменяемых объектах проще говорить, чем об изменяемых, поскольку у них нет изменяемых со временем сложных областей состояния. Во­вторых, неизменяемые объекты можно совершенно свободно куда­нибудь передавать, а перед передачей изменяемых объектов в другой код порой приходится делать страховочные копии. В­третьих, если объ­
    ект правильно сконструирован, то при одновременном обращении к неизменяемому объекту из двух потоков повредить его состояние невозможно, поскольку никакой поток не может изменить состояние неизменяемого объекта. В­четвертых, неизменяемые объекты обеспе­
    чивают безопасность ключей хеш­таблиц. Если, к примеру, изменяе­
    мый объект изменился после помещения в
    HashSet
    , то в следующий раз при поиске там его можно не найти.
    Главный недостаток неизменяемых объектов — им иногда требует­
    ся копирование больших графов объектов, тогда как вместо этого можно было бы сделать обновление. В некоторых случаях это может быть сложно выразить, а также могут выявиться узкие места в про­
    изводительности. В результате в библиотеки нередко включают из­
    меняемые альтернативы неизменяемым классам. Например, класс
    StringBuilder
    — изменяемая альтернатива неизменяемого класса
    String
    . Дополнительная информация о конструировании изменяемых объектов в Scala будет дана в главе 16.
    ПРИМЕЧАНИЕ
    Исходный пример с Rational подчеркивает разницу между Java и Scala .
    В Java классы имеют конструкторы, которые могут принимать параметры, а в Scala классы могут принимать параметры напрямую . Система записи в Scala куда более лаконична — параметры класса могут использоваться напрямую в теле, нет никакой необходимости определять поля и записывать присваивания, копирующие параметры конструктора в поля . Это может привести к дополнительной экономии на шаблонном коде, особенно когда дело касается небольших классов .

    128 Глава 6 • Функциональные объекты
    Компилятор Scala скомпилирует любой код, помещенный в тело класса и не являющийся частью поля или определения метода, в первичный конструк­
    тор. Например, можно вывести такое отладочное сообщение:
    class Rational(n: Int, d: Int):
    println("Created " + n + "/" + d)
    Получив данный код, компилятор Scala поместит вызов println в первич­
    ный конструктор класса
    Rational
    . Поэтому при создании нового экземпляра
    Rational вызов println приведет к выводу отладочного сообщения:
    scala> new Rational(1, 2)
    Created 1/2
    Val res0: Rational = Rational@6121a7dd
    При создании экземпляров классов, таких как
    Rational
    , вы можете при желании опустить ключевое слово new
    . Такое выражение использует так на­
    зываемый универсальный метод применения. Вот пример:
    scala> Rational(1, 2)
    Created 1/2
    val res1: Rational = Rational@5dc7841c
    6 .3 . Переопределение метода toString
    При создании экземпляра
    Rational в предыдущем примере REPL вывел
    Rational@
    5dc7841c
    . Эта странная строка получилась ввиду вызова в от­
    ношении объекта
    Rational метода toString
    . По умолчанию класс
    Rational наследует реализацию toString
    , определенную в классе java.lang.Object
    , которая просто выводит имя класса, символ
    @
    и шестнадцатеричное число.
    Предполагалось, что результат выполнения toString поможет программи­
    стам, предоставив информацию, которую можно использовать в отладоч­
    ных инструкциях вывода информации, для ведения логов, в отчетах о сбоях тестов, а также для просмотра выходной информации REPL и отладчика.
    Результат, выдаваемый на данный момент методом toString
    , не приносит особой пользы, поскольку не дает никакой информации относительно значения рационального числа. Более полезная реализация toString будет выводить значения числителя и знаменателя объекта
    Rational
    . Переопре-
    делить исходную реализацию можно, добавив метод toString к классу
    Rational
    :
    class Rational(n: Int, d: Int):
    override def toString = s"$n/$d"

    6 .4 . Проверка соблюдения предварительных условий
    1   ...   10   11   12   13   14   15   16   17   ...   64


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