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

  • Листинг 16.9.

  • Множественное равенство

  • 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
    страница39 из 64
    1   ...   35   36   37   38   39   40   41   42   ...   64
    362 Глава 16 • Изменяемые объекты
    Список agenda будет храниться в надлежащем отсортированном порядке благодаря использованию метода insert
    , который обновляет этот список.
    Вызов метода insert в качестве единственного способа добавить рабочий элемент к плану действий можно увидеть в методе afterDelay
    :
    def afterDelay(delay: Int)(block: => Unit) =
    val item = WorkItem(currentTime + delay, () => block)
    agenda = insert(agenda, item)
    Как следует из названия, этот метод вставляет действие, задаваемое блоком, в план действий, планируя время задержки его выполнения delay после теку­
    щего момента моделируемого времени. Например, следующий вызов создаст новый рабочий элемент к выполнению в моделируемое время currentTime
    +
    delay
    :
    afterDelay(delay) { count += 1 }
    Код, предназначенный для выполнения, содержится во втором аргументе ме­
    тода. Формальный параметр имеет тип
    =>
    Unit
    , то есть это вычисление типа
    Unit
    , передаваемое по имени. Следует напомнить, что параметры, передава­
    емые по имени (by­name parameters), при передаче методу не вычисляются.
    Следовательно, в показанном ранее вызове значение count будет увеличено на единицу, только когда среда моделирования вызовет действие, сохранен­
    ное в рабочем элементе. Обратите внимание: afterDelay
    — каррированная функция. Это хороший пример разъясненного в разделе 9.5 принципа, со­
    гласно которому карринг может использоваться для выполнения вызовов методов, больше похожих на встроенный синтаксис языка.
    Созданный рабочий элемент еще нужно вставить в план действий. Это дела­
    ется с помощью метода insert
    , в котором поддерживается предварительное условие отсортированности плана по времени:
    private def insert(ag: List[WorkItem],
    item: WorkItem): List[WorkItem] =
    if ag.isEmpty || item.time < ag.head.time then item :: ag else ag.head :: insert(ag.tail, item)
    Ядро класса
    Simulation определяется методом run
    :
    def run() =
    afterDelay(0) {
    println("*** simulation started, time = " +
    currentTime + " ***")
    }
    while !agenda.isEmpty do next()

    16 .5 . API моделирования 363
    Этот метод периодически берет первый элемент из плана действий, удаляет его из данного плана и выполняет. Он продолжает свою работу до тех пор, пока в плане не останется элементов для выполнения. Каждый шаг выпол­
    няется с помощью вызова метода next
    , имеющего такое определение:
    private def next() =
    (agenda: @unchecked) match case item :: rest =>
    agenda = rest curtime = item.time item.action()
    В методе next текущий план действий разбивается с помощью сопоставления с образцом на первый элемент item и весь остальной список рабочих эле­
    ментов rest
    . Первый элемент из текущего плана удаляется, моделируемое время curtime устанавливается на время рабочего элемента, и выполняется действие рабочего элемента.
    Обратите внимание: next может быть вызван, только если план действий еще не пуст. Варианта для пустого плана нет, поэтому при попытке запуска next в отношении пустого списка agenda будет выдано исключение
    MatchError
    В действительности же компилятор Scala обычно выдает предупреждение о том, что для списка не указан один из возможных паттернов:
    27 | agenda match
    | ˆˆˆˆˆˆ
    | match may not be exhaustive.
    |
    | It would fail on pattern case: Nil
    В данном случае неуказанный вариант никакой проблемы не создает, по­
    скольку известно, что next вызывается только в отношении непустого плана действий. Поэтому может возникнуть желание отключить предупреждение.
    Как было показано в разделе 13.5, это можно сделать, добавив к выражению селектора сопоставления с образцом аннотацию
    @unchecked
    . Именно поэтому в коде
    Simulation используется
    (agenda:
    @unchecked)
    match
    , а не agenda match
    И это правильно. Объем кода для среды моделирования может показаться весьма скромным. Может возникнуть вопрос: а как эта среда вообще может поддерживать содержательное моделирование, если всего лишь выполняет список рабочих элементов? В действительности же эффективность среды моделирования определяется тем фактом, что действия, сохраненные в ра­
    бочих элементах, в ходе своего выполнения могут самостоятельно добавлять следующие рабочие элементы в план действий. Тем самым открывается

    364 Глава 16 • Изменяемые объекты возможность получить из вычисления простых начальных действий доволь­
    но продолжительную симуляцию.
    16 .6 . Моделирование электронной логической схемы
    Следующим шагом станет использование среды моделирования в целях реализации предметно­ориентированного языка для логических схем, пока­
    занного в разделе 16.4. Следует напомнить, что DSL логических схем состоит из класса для проводников и методов, создающих логические элементы «И»,
    «ИЛИ» и «НЕ». Все это содержится в классе
    BasicCircuitSimulation
    , кото­
    рый расширяет среду моделирования. Он показан в листинге 16.9.
    Листинг 16.9. Класс BasicCircuitSimulation package org.stairwaybook.simulation abstract class BasicCircuitSimulation extends Simulation:
    def InverterDelay: Int def AndGateDelay: Int def OrGateDelay: Int class Wire:
    private var sigVal = false private var actions: List[Action] = List.empty def getSignal = sigVal def setSignal(s: Boolean) =
    if s != sigVal then sigVal = s actions.foreach(_())
    def addAction(a: Action) =
    actions = a :: actions a()
    def inverter(input: Wire, output: Wire) =
    def invertAction() =
    val inputSig = input.getSignal afterDelay(InverterDelay) {
    output setSignal !inputSig
    }
    input addAction invertAction

    16 .6 . Моделирование электронной логической схемы 365
    def andGate(a1: Wire, a2: Wire, output: Wire) =
    def andAction() =
    val a1Sig = a1.getSignal val a2Sig = a2.getSignal afterDelay(AndGateDelay) {
    output setSignal (a1Sig & a2Sig)
    }
    a1 addAction andAction a2 addAction andAction def orGate(o1: Wire, o2: Wire, output: Wire) =
    def orAction() =
    val o1Sig = o1.getSignal val o2Sig = o2.getSignal afterDelay(OrGateDelay) {
    output setSignal (o1Sig | o2Sig)
    }
    o1 addAction orAction o2 addAction orAction def probe(name: String, wire: Wire) =
    def probeAction() =
    println(name + " " + currentTime +
    " new-value = " + wire.getSignal)
    wire addAction probeAction
    В классе
    BasicCircuitSimulation объявляются три абстрактных метода, представляющих задержки основных логических элементов:
    InverterDelay
    ,
    AndGateDelay и
    OrGateDelay
    . Настоящие задержки на уровне этого клас­
    са неизвестны, поскольку зависят от технологии моделируемых логи­
    ческих микросхем. Поэтому задержки в классе
    BasicCircuitSimulation остаются абстрактными, и их конкретное определение делегируется под­
    классам
    1
    . Далее мы рассмотрим реализацию остальных членов класса
    BasicCircuitSimulation
    Класс Wire
    Проводникам нужно поддерживать три основных действия:
    z z
    getSignal:
    Boolean возвращает текущий сигнал в проводнике;
    z z
    setSignal(sig:
    Boolean)
    выставляет сигнал проводника в sig
    ;
    1
    Имена этих методов задержки начинаются с прописных букв, поскольку представ­
    ляют собой константы. Но это методы, и они могут быть переопределены в под­
    классах. Как те же вопросы решаются с помощью val
    ­переменных, мы покажем в разделе 20.3.

    366 Глава 16 • Изменяемые объекты z
    z addAction(p:
    Action)
    прикрепляет указанную процедуру p
    к действиям проводника. Замысел заключается в том, чтобы все процедуры действий, прикрепленные к какому­либо проводнику, выполнялись всякий раз, ко­
    гда сигнал на проводнике изменяется. Как правило, действия добавляют­
    ся к проводнику подключенными к нему компонентами. Прикрепленное действие выполняется в момент его добавления к проводнику, а после этого всякий раз при изменении сигнала в проводнике.
    Реализация класса
    Wire имеет следующий вид:
    class Wire:
    private var sigVal = false private var actions: List[Action] = List.empty def getSignal = sigVal def setSignal(s: Boolean) =
    if s != sigVal then sigVal = s actions.foreach(_())
    def addAction(a: Action) =
    actions = a :: actions a()
    Состояние проводника формируется двумя приватными переменными.
    Переменная sigVal представляет текущий сигнал, а переменная actions
    — процедуры действий, прикрепленные в данный момент к проводнику. В реа­
    лизациях методов представляет интерес только та часть, которая относится к методу setSignal
    : когда сигнал проводника изменяется, в переменной sigVal сохраняется новое значение. Кроме того, выполняются все действия, прикрепленные к проводнику. Обратите внимание на используемую для этого сокращенную форму синтаксиса: выражение actions foreach
    (_())
    вызывает применение функции
    _()
    к каждому элементу в списке действий.
    В соответствии с описанием, приведенным в разделе 8.5, функция
    _()
    явля­
    ется сокращенной формой записи для f
    =>
    f
    ()
    , то есть получает функцию
    (назовем ее f
    ) и применяет ее к пустому списку параметров.
    Метод inverter
    Единственный результат создания инвертора — то, что действие устанав­
    ливается на его входном проводнике. Данное действие вызывается при его установке, а затем всякий раз при изменении сигнала на входе. Эффект

    16 .6 . Моделирование электронной логической схемы 367
    от действия заключается в установке выходного значения (с помощью setSignal
    ) на отрицание его входного значения. Поскольку у логического элемента «НЕ» имеется задержка, это изменение должно наступить только по прошествии определенного количества единиц моделируемого времени, хранящегося в переменной
    InverterDelay
    , после изменения входного значе­
    ния и выполнения действия. Эти обстоятельства подсказывают следующий вариант реализации:
    def inverter(input: Wire, output: Wire) =
    def invertAction() =
    val inputSig = input.getSignal afterDelay(InverterDelay) {
    output setSignal !inputSig
    }
    input addAction invertAction
    Эффект метода inverter заключается в добавлении действия invertAction к input
    . При вызове данного действия берется входной сигнал и устанавлива­
    ется еще одно действие, инвертирующее выходной сигнал в плане действий моделирования. Это другое действие должно быть выполнено по прошествии того количества единиц времени моделирования, которое хранится в пере­
    менной
    InverterDelay
    . Обратите внимание на то, как для создания нового рабочего элемента, предназначенного для выполнения в будущем, в методе используется метод afterDelay
    Методы andGate и orGate
    Реализация моделирования логического элемента «И» аналогична реали­
    зации моделирования элемента «НЕ». Цель — выставить на выходе конъ­
    юнкцию его входных сигналов. Это должно произойти по прошествии того количества единиц времени моделирования, которое хранится в переменной
    AndGateDelay
    , после изменения любого из его двух входных сигналов. Стало быть, подойдет следующая реализация:
    def andGate(a1: Wire, a2: Wire, output: Wire) =
    def andAction() =
    val a1Sig = a1.getSignal val a2Sig = a2.getSignal afterDelay(AndGateDelay) {
    output setSignal (a1Sig & a2Sig)
    }
    a1 addAction andAction a2 addAction andAction

    368 Глава 16 • Изменяемые объекты
    Эффект от вызова метода andGate заключается в добавлении действия andAction к обоим входным проводникам, a1
    и a2
    . При вызове данного действия берутся оба входных сигнала и устанавливается еще одно дей­
    ствие, которое выдает выходной сигнал в виде конъюнкции обоих входных сигналов. Это другое действие должно быть выполнено по прошествии того количества единиц времени моделирования, которое хранится в пере­
    менной
    AndGateDelay
    . Учтите, что при смене любого сигнала на входных проводниках выход должен вычисляться заново. Именно поэтому одно и то же действие andAction устанавливается на каждом из двух входных проводников, a1
    и a2
    . Метод orGate реализуется аналогичным образом, за исключением того, что моделирует логическую операцию «ИЛИ», а не «И».
    Вывод симуляции
    Для запуска симулятора нужен способ проверки изменения сигналов на проводниках. Чтобы выполнить эту задачу, можно смоделировать действие проверки (пробы) проводника:
    def probe(name: String, wire: Wire) =
    def probeAction() =
    println(name + " " + currentTime +
    " new-value = " + wire.getSignal)
    wire addAction probeAction
    Эффект от процедуры probe заключается в установке на заданный проводник действия probeAction
    . Как обычно, установленное действие выполняется всякий раз при изменении сигнала на проводнике. В данном случае он про­
    сто выводит на стандартное устройство название проводника (которое пере­
    дается probe в качестве первого параметра), а также текущее моделируемое время и новое значение проводника.
    Запуск симулятора
    После всех этих приготовлений настало время посмотреть на симулятор в действии. Для определения конкретной симуляции нужно выполнить наследование из класса среды моделирования. Чтобы увидеть кое­что ин­
    тересное, будет создан абстрактный класс моделирования, расширяющий
    BasicCircuitSimulation и содержащий определения методов для полу­
    сумматора и сумматора в том виде, в котором они были представлены

    16 .6 . Моделирование электронной логической схемы 369
    в листингах 16.6 и 16.7 соответственно. Этот класс, который будет назван
    CircuitSimulation
    , показан в листинге 16.10.
    Листинг 16.10. Класс CircuitSimulation package org.stairwaybook.simulation abstract class CircuitSimulation extends BasicCircuitSimulation:
    def halfAdder(a: Wire, b: Wire, s: Wire, c: Wire) =
    val d, e = new Wire orGate(a, b, d)
    andGate(a, b, c)
    inverter(c, e)
    andGate(d, e, s)
    def fullAdder(a: Wire, b: Wire, cin: Wire,
    sum: Wire, cout: Wire) =
    val s, c1, c2 = new Wire halfAdder(a, cin, s, c1)
    halfAdder(b, s, sum, c2)
    orGate(c1, c2, cout)
    Конкретная модель логической схемы будет объектом­наследником класса
    CircuitSimulation
    . Этому объекту по­прежнему необходимо зафиксировать задержки на логических элементах в соответствии с технологией реализа­
    ции моделируемой логической микросхемы. И наконец, понадобится также определить конкретную моделируемую схему.
    Эти шаги можно проделать в интерактивном режиме в интерпретаторе Scala:
    scala> import org.stairwaybook.simulation.*
    Сначала займемся задержками логических элементов. Определим объект
    (назовем его
    MySimulation
    ), предоставляющий несколько чисел:
    scala> object MySimulation extends CircuitSimulation:
    def InverterDelay = 1
    def AndGateDelay = 3
    def OrGateDelay = 5
    // Определяем объект MySimulation
    Поскольку предполагается периодически получать доступ к элементам объ­
    екта
    MySimulation
    , импортирование этого объекта укоротит последующий код:
    scala> import MySimulation.*

    370 Глава 16 • Изменяемые объекты
    Далее займемся схемой. Определим четыре проводника и поместим пробы на два из них:
    scala> val input1, input2, sum, carry = new Wire val input1: MySimulation.Wire = ...
    val input2: MySimulation.Wire = ...
    val sum: MySimulation.Wire = ...
    val carry: MySimulation.Wire = ...
    scala> probe("sum", sum)
    sum 0 new-value = false scala> probe("carry", carry)
    carry 0 new-value = false
    Обратите внимание: пробы немедленно выводят выходные данные. Дело в том, что каждое действие, установленное на проводнике, первый раз вы­
    полняется при его установке.
    Теперь определим подключение к проводникам полусумматора:
    scala> halfAdder(input1, input2, sum, carry)
    И наконец, установим один за другим сигналы на двух входящих проводни­
    ках на true и запустим моделирование:
    scala> input1 setSignal true scala> run()
    *** simulation started, time = 0 ***
    sum 8 new-value = true scala> input2 setSignal true scala> run()
    *** simulation started, time = 8 ***
    carry 11 new-value = true sum 15 new-value = false
    Резюме
    В данной главе мы собрали воедино две на первый взгляд несопоставимые технологии: изменяемое состояние и функции высшего порядка. Изменяемое состояние было использовано для моделирования физических объектов, со­
    стояние которых со временем изменяется. Функции высшего порядка были применены в среде моделирования в целях выполнения действий в указан­

    Резюме 371
    ные моменты моделируемого времени. Они также были использованы в мо­
    делировании логических схем в качестве триггеров, связывающих действия с изменениями состояния. Попутно мы показали простой способ определить предметно­ориентированный язык в виде библиотеки. Вероятно, для одной главы этого вполне достаточно.
    Если вас привлекла эта тема, то можете попробовать создать дополнительные примеры моделирования. Можно объединить полусумматоры и сумматоры для формирования более крупных схем или на основе ранее определенных логических элементов разработать новые схемы и смоделировать их. В гла­
    ве 19 мы рассмотрим имеющуюся в Scala параметризацию типов и покажем еще один пример, который сочетает в себе функциональный и императивный подходы, дающие весьма неплохое решение.

    17
    Иерархия Scala
    В этой главе мы рассмотрим иерархию классов Scala в целом. В Scala каждый класс наследуется от общего суперкласса по имени
    Any
    . Поскольку каждый класс является подклассом
    Any
    , то методы, определенные в классе
    Any
    , универ­
    сальны: их можно вызвать в отношении любого объекта. В самом низу иерар­
    хии в Scala также определяются довольно интересные классы
    Null и
    Nothing
    , которые, по сути, выступают в роли общих подклассов. Например, в то время, как
    Any
    — суперкласс для всех классов,
    Nothing
    — подкласс для любого класса.
    В данной главе мы проведем экскурсию по имеющейся в Scala иерархии классов.
    17 .1 . Иерархия классов Scala
    На рис. 17.1 в общих чертах показана иерархия классов Scala. На вершине иерархии находится класс
    Any
    ; в нем определяются методы, в число которых входят:
    final def ==(that: Any): Boolean final def !=(that: Any): Boolean def equals(that: Any): Boolean def ##: Int def hashCode: Int def toString: String
    Все классы — наследники класса
    Any
    , поэтому каждый объект в программе на Scala можно подвергнуть сравнению с помощью
    ==
    ,
    !=
    или equals
    , хеши­
    рованию с использованием
    ##
    или hashCode и форматированию, прибегнув к toString
    . Методы определения равенства
    ==
    и неравенства
    !=
    объявлены в классе
    Any как final
    , следовательно, переопределить их в подклассах не­
    возможно.

    17 .1 . Иерархия классов Scala 373
    Рис.
    17.1.
    Иерархия классов S
    cala

    374 Глава 17 • Иерархия Scala
    Метод
    ==
    — по сути то же самое, что и equals
    , а
    !=
    всегда является отрица­
    нием метода equals
    1
    . Таким образом, отдельные классы могут перекроить смысл значения метода
    ==
    или
    !=
    , переопределив equals
    Множественное равенство
    В Scala 3 вводится понятие «множественное равенство», вызывающее ошибку компилятора при использовании методов
    ==
    и
    =
    , которые от­
    ражают вероятные ошибки, например, при сравнении
    String и
    Int на равенство. Этот механизм будет описан в главе 23.
    У корневого класса
    Any имеется два подкласса:
    AnyVal и
    AnyRef
    . Класс
    AnyVal является родительским для классов значений в Scala. Наряду с возможно­
    стью определять собственные классы значений (см. раздел 17.4) Scala имеет девять встроенных:
    Byte
    ,
    Short
    ,
    Char
    ,
    Int
    ,
    Long
    ,
    Float
    ,
    Double
    ,
    Boolean и
    Unit
    Первые восемь соответствуют примитивным типам Java, и их значения во время выполнения программы представляются в виде примитивных значе­
    ний Java. Все экземпляры этих классов написаны в Scala в виде литералов.
    Например,
    42
    ,
    'x'
    , false
    — экземпляры классов
    Int
    ,
    Char и
    Boolean соот­
    ветственно. Создать их, используя ключевое слово new
    , невозможно. Этому препятствует особый прием, в котором все классы значений определены одновременно и как абстрактные, и как финальные.
    Поэтому, если воспользоваться следующим кодом:
    scala> new Int то будет получен такой результат:
    1 |new Int
    | ˆˆˆ
    | Int is abstract; it cannot be instantiated
    1
    Единственный случай, когда использование
    ==
    не приводит к непосредственному вызову equals
    , относится к упакованным числовым классам Java, таким как
    Integer или
    Long
    . В Java new
    Integer(1)
    не эквивалентен new
    Long(1)
    даже в случае примене­
    ния примитивных значений
    1
    ==
    1L
    . Поскольку Scala — более регулярный язык, чем
    Java, появилась необходимость скорректировать это несоответствие, задействовав для этих классов особую версию метода
    ==
    . Точно так же метод
    ##
    обеспечивает
    Scala­версию хеширования и похож на Java­метод hashCode
    , за исключением того, что для упакованных числовых типов он всегда работает с методом
    ==
    . Например, для new
    Integer(1)
    и new
    Long(1)
    метод
    ##
    вычисляет один и тот же хеш, тогда как
    Java­методы hashCode вычисляют разный хеш­код.

    17 .1 . Иерархия классов Scala 375
    Класс значений
    Unit примерно соответствует имеющемуся в Java типу void
    — он используется в качестве результирующего типа выполнения ме­
    тода, который не возвращает содержательного результата. Как упоминалось в разделе 7.2, у
    Unit имеется единственное значение экземпляра, оно запи­
    сывается как
    ()
    Как объяснялось в главе 5, в классах значений в качестве методов поддер­
    живаются обычные арифметические и логические (булевы) операторы.
    Например, у класса
    Int имеются методы
    +
    и
    *
    , а у класса
    Boolean
    — мето­
    ды
    ||
    и
    &&
    . Классы значений также наследуют все методы из класса
    Any
    Например:
    42.toString // 42 42.hashCode // 42 42.equals(42) // true
    Следует отметить, что пространство классов значений плоское: все классы значений являются подтипами scala.AnyVal
    , но не являются подклассами друг друга. Вместо этого между различными типами классов значений существует неявное преобразование типов. Например, экземпляр класса scala.Int
    , когда это требуется, автоматически расширяется (путем неявного преобразования) в экземпляр класса scala.Long
    Как упоминалось в разделе 5.10, неявное преобразование используется также для добавления большей функциональности к типам значений. Например, тип
    Int поддерживает все показанные далее операции:
    42.max(43) // 43 42.min(43) // 42 1 until 5 // Range 1 until 5 1 to 5 // Range 1 to 5 3.abs // 3
    -3.abs // 3
    Работает это следующим образом: все методы min
    , max
    , until
    , to и abs опреде­
    лены в классе scala.runtime.RichInt
    , а между классами
    Int и
    RichInt суще­
    ствует неявное преобразование. Оно применяется при вызове в отношении
    Int
    ­объекта метода, который определен не в классе
    Int
    , а в
    RichInt
    . По ана­
    логии с этим «классы­усилители» и неявные преобразования существуют и для других классов значений
    1 1
    Этот вариант использования неявных преобразований со временем будет заменен методами расширения, описанными в главе 22.

    376 Глава 17 • Иерархия Scala
    Другим подклассом корневого класса
    Any является
    AnyRef
    — база всех ссылоч-
    ных классов в Scala. Как упоминалось ранее, на платформе Java
    AnyRef факти­
    чески является псевдонимом класса java.lang.Object
    , а значит, все классы, написанные на Java и Scala, — наследники
    AnyRef
    1
    . Поэтому java.lang.Object считается способом реализации
    AnyRef на платформе Java. Таким образом, хоть
    Object и
    AnyRef и можно взаимозаменяемо использовать в программах
    Scala на платформе Java, рекомендуемым стилем будет повсеместное при­
    менение
    AnyRef
    17 .2 . Как реализованы примитивы
    Как все это реализовано? Фактически в Scala целочисленные значения хра­
    нятся так же, как и в Java, — в виде 32­разрядных слов. Это необходимо для эффективной работы виртуальной машины Java (JVM) и обеспечения воз­
    можности совместной работы с библиотеками Java. Такие стандартные опе­
    рации, как сложение или умножение, реализуются в качестве примитивных операций. Однако Scala использует «резервный» класс java.lang.In teger везде, где целое число должно выглядеть как (Java) объект. Так происходит, например, при вызове метода toString для целого числа или присваива­
    нии этого числа переменной типа
    Any
    . При необходимости целочисленные значения типа
    Int явно преобразуются в упакованные целые числа типа java.lang.Integer
    Это во многом походит на автоупаковку (auto­boxing) в Java, два процесса действительно очень похожи. Но все­таки есть одно коренное различие: упаковка в Scala гораздо менее заметна, чем в Java. Попробуйте выполнить в Java следующий код:
    // Это код на языке Java boolean isEqual(int x, int y) {
    return x == y;
    }
    System.out.println(isEqual(421, 421));
    В результате, конечно же, будет получено значение true
    . А теперь измените типы аргументов isEqual на java.lang.Integer
    (или с аналогичным резуль­
    татом на
    Object
    ):
    1
    Одна из причин существования псевдонима AnyRef, заменяющего использование имени java.lang.Object
    , заключается в том, что Scala изначально разрабатывался для работы как на платформе Java, так и на платформе .NET. На платформе .NET
    AnyRef был псевдонимом для
    System.Object

    17 .2 . Как реализованы примитивы 377
    // Это код на языке Java boolean isEqual(Integer x, Integer y) {
    return x == y;
    }
    System.out.println(isEqual(421, 421));
    В итоге получите результат false
    ! Оказывается, число
    421
    было упаковано дважды, поэтому аргументами для x
    и y
    стали два разных объекта. При­
    менение
    ==
    в отношении ссылочных типов означает равенство ссылок, а
    Integer
    — ссылочный тип, вследствие чего в результате получается false
    Это один из аспектов, свидетельствующих о том, что Java не является чистым объектно­ориентированным языком. Существует четко видимая разница между примитивными и ссылочными типами.
    Теперь попробуйте провести тот же самый эксперимент на Scala:
    def isEqual(x: Int, y: Int) = x == y isEqual(421, 421) // true def isEqual(x: Any, y: Any) = x == y isEqual(421, 421) // true
    Операция
    ==
    в Scala разработана так, чтобы быть понятной относительно представления типа. Для типов значений (числовых или логических) это вполне естественное равенство. Для ссылочных типов, отличающихся от упа­
    кованных числовых типов Java,
    ==
    рассматривается в качестве псевдонима метода equals
    , унаследованного от класса
    Object
    . Данный метод изначально определен в целях выявления равенства ссылок, но во многих подклассах переопределяется для реализации их естественных представлений о равен­
    стве. Это также означает, что в Scala вы никогда не попадете в хорошо из­
    вестную в Java ловушку, касающуюся сравнения строк. В Scala оно работает вполне корректно:
    val x = "abcd".substring(2) // cd val y = "abcd".substring(2) // cd x == y // true
    В Java результатом сравнения x
    с y
    будет false
    . В этом случае программист должен был воспользоваться методом equals
    , но данный нюанс нетрудно упустить из виду.
    Может сложиться и такая ситуация, при которой вместо равенства, опре­
    деляемого пользователем, нужно проверить равенство ссылок. Так, в неко­
    торых ситуациях, когда эффективность важнее всего, вы можете использо­
    вать хеш конс (hash cons) некоторых классов и сопоставить их экземпляры

    378 Глава 17 • Иерархия Scala с помощью равенства ссылок
    1
    . Для таких случаев в классе
    AnyRef определен дополнительный метод eq
    , который не может быть переопределен и реализо­
    ван как проверка равенства ссылок (то есть для ссылочных типов ведет себя подобно
    ==
    в Java). Существует также отрицание eq
    , которое называется ne
    , например:
    val x = new String("abc") // abc val y = new String("abc") // abc x == y // true x eq y // false x ne y // true
    Более подробно равенство в Scala рассматривается в главе 8.
    17 .3 . Низшие типы
    Внизу иерархии на рис. 17.1 показаны два класса: scala.Null и scala.No- thing
    . Это особые типы, единообразно сглаживающие острые углы объектно­
    ориентированной системы типов в Scala.
    Класс
    Null
    — тип нулевой ссылки null
    : он представляет собой подкласс каждого ссылочного класса (то есть каждого класса, который сам является наследником класса
    AnyRef)
    2
    Null несовместим с типами значений. Нельзя, к примеру, присвоить значение null целочисленной переменной:
    scala> val i: Int = null
    1 |val i: Int = null
    | ˆˆˆˆ
    | Found: Null
    | Required: Int
    Тип
    Nothing находится в самом низу иерархии классов Scala: он представляет собой подтип любого другого типа, значений которого вообще не существу­
    1
    Вы можете выполнить хеш конс экземпляров класса путем кэширования всех соз­
    данных экземпляров в слабую коллекцию. Затем, как только потребуется новый экземпляр класса, сначала проверяется кэш. Если в нем уже есть элемент, равный тому, который вы намереваетесь создать, то можно повторно воспользоваться су­
    ществующим экземпляром. В результате такой систематизации любые два экзем­
    пляра, равенство которых определяется с помощью метода equals()
    , также равны на основе равенства ссылок.
    2
    В Scala 3 есть опция
    -Yexplicit-nulls
    , которая позволяет использовать экспери­
    ментальную альтернативную обработку значения null
    , направленную на отслежи­
    вание переменных, которые могут и не могут быть нулевыми.

    17 .4 . Определение собственных классов значений 379
    ет. А зачем нужен тип без значений? Как говорилось в разделе 7.4,
    Nothing используется, в частности, для того, чтобы сигнализировать об аварийном завершении операции.
    Например, в объекте sys стандартной библиотеки Scala есть метод error
    , имеющий такое определение:
    def error(message: String): Nothing =
    throw new RuntimeException(message)
    Возвращаемым типом метода error является
    Nothing
    , что говорит пользо­
    вателю о ненормальном возвращении из метода (вместо этого метод сгене­
    рировал исключение). Поскольку
    Nothing
    — подтип любого другого типа, то методы, подобные error
    , допускают весьма гибкое использование, например:
    def divide(x: Int, y: Int): Int =
    if y != 0 then x / y else sys.error("деление на ноль невозможно")
    Ветка then данного условия, представленная выражением x
    /
    y
    , имеет тип
    Int
    , а ветка else
    , то есть вызов error
    , имеет тип
    Nothing
    . Поскольку
    Nothing
    — подтип
    Int
    , то типом всего условного выражения, как и требова­
    лось, является
    Int
    17 .4 . Определение собственных классов значений
    В разделе 17.1 говорилось, что в дополнение к встроенным классам значений можно определять собственные. Как и экземпляры встроенных, экземпляры ваших классов значений будут, как правило, компилироваться в байт­код
    Java, который не задействует класс­оболочку. В том контексте, где нужна оболочка, например, при использовании обобщенного кода, значения будут упаковываться и распаковываться автоматически
    1
    Классами значений можно сделать только вполне определенные классы.
    Чтобы класс стал классом значений, он должен иметь только один параметр и не должен иметь внутри ничего, кроме def
    ­определений. Более того, класс значений не может расширяться никакими другими классами и в нем не могут переопределяться методы equals или hashCode
    1
    Scala 3 также предлагает непрозрачные типы, что является некоторым ограниче­
    нием, но гарантирует, что значение никогда не будет упаковано.

    380 Глава 17 • Иерархия Scala
    Чтобы определить класс значений, его нужно сделать подклассом класса
    AnyVal и поставить перед его единственным параметром префикс val
    . При­
    мер класса значений выглядит так:
    class Dollars(val amount: Int) extends AnyVal:
    override def toString = "$" + amount
    В соответствии с описанием, приведенным в разделе 10.6, префикс val по­
    зволяет иметь доступ к параметру amount как к полю. Например, следующий код создает экземпляр класса значений, а затем извлекает из него amount
    :
    val money = new Dollars(1_000_000)
    money.amount // 1000000
    В данном примере money ссылается на экземпляр класса значений. Эта пере­
    менная в исходном коде Scala имеет тип
    Dollars
    , но скомпилированный байт­код Java будет напрямую использовать тип
    Int
    В этом примере определяется метод toString
    , и компилятор понимает, ко­
    гда его использовать. Именно поэтому вывод значения money дает результат
    $1000000
    со знаком доллара, а вывод money.amount дает результат
    1000000
    Можно даже определить несколько типов значений, и все они будут опи­
    раться на одно и то же
    Int
    ­значение, например:
    class SwissFrancs(val amount: Int) extends AnyVal:
    override def toString = s"$amount CHF"
    Несмотря на то что
    Dollars и
    SwissFrancs во время выполнения представ­
    лены в виде целых чисел, в процессе компиляции они становятся разными типами:
    scala> val dollars: Dollars = new SwissFrancs(1000)
    1 |val dollars: Dollars = new SwissFrancs(1000)
    | ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
    | Found: SwissFrancs
    | Required: Dollars
    Уход от монокультурности типов
    Чтобы получить наибольшие преимущества от использования иерархии классов Scala, старайтесь для каждого понятия предметной области опре­
    делять новый класс, несмотря на то что будет возможность неоднократно применять его для различных целей. Даже если он относится к так называе­
    мому крошечному (tiny) типу, не имеющему методов или полей, определение дополнительного класса поможет компилятору принести вам больше пользы.

    17 .4 . Определение собственных классов значений 381
    Предположим, вы написали некий код для генерации HTML. В HTML название стиля представлено в виде строки. То же самое касается и иденти­
    фикаторов привязки. Сам код HTML также является строкой, поэтому при желании представить все здесь перечисленное можно с помощью определе­
    ния вспомогательного кода, используя строки наподобие этих:
    def title(text: String, anchor: String, style: String): String =
    s"
    1   ...   35   36   37   38   39   40   41   42   ...   64


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