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

  • Листинг 6.1.

  • Листинг 6.3.

  • Листинг 6.4.

  • 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
    страница15 из 64
    1   ...   11   12   13   14   15   16   17   18   ...   64
    129
    Модификатор override перед определением метода показывает, что преды­
    дущее определение метода переопределяется (более подробно этот вопрос рассматривается в главе 10). Поскольку отныне числа типа
    Rational будут выводиться совершенно отчетливо, мы удаляем отладочную инструкцию println
    , помещенную в тело предыдущей версии класса
    Rational
    . Теперь новое поведение
    Rational можно протестировать в REPL:
    scala> val x = Rational(1, 3)
    x: Rational = 1/3
    scala> val y = Rational(5, 7)
    y: Rational = 5/7 6 .4 . Проверка соблюдения предварительных условий
    В качестве следующего шага переключим внимание на проблему, связанную с текущим поведением первичного конструктора. Как упоминалось в на­
    чале главы, рациональные числа не должны содержать ноль в знаменателе.
    Но пока первичный конструктор может принимать ноль, передаваемый в качестве параметра d
    :
    scala> new Rational(5, 0) // 5/0
    val res1: Rational = 5/0
    Одно из преимуществ объектно­ориентированного программирования — возможность инкапсуляции данных внутри объектов, чтобы можно было гарантировать, что данные корректны в течение всей жизни объекта. В дан­
    ном случае для такого неизменяемого объекта, как
    Rational
    , это значит, что вы должны гарантировать корректность данных на этапе конструирования объекта при условии, что нулевой знаменатель — недопустимое состояние числа типа
    Rational и такое число не должно создаваться, если в качестве параметра d
    передается ноль.
    Лучше всего решить эту проблему, определив для первичного конструктора
    предусловие, согласно которому d
    должен иметь ненулевое значение. Предус­
    ловие — ограничение, накладываемое на значения, передаваемые в метод или конструктор, то есть требование, которое должно выполняться вызыва ющим кодом. Один из способов решить задачу — использовать метод require
    1
    :
    1
    Метод require определен в самостоятельном объекте
    Predef
    . Как упоминалось в разделе 4.5, элементы класса
    Predef автоматически импортируются в каждый исходный файл Scala.

    130 Глава 6 • Функциональные объекты class Rational(n: Int, d: Int):
    require(d != 0)
    override def toString = s"$n/$d"
    Метод require получает один булев параметр. Если переданное значение приведет к вычислению в true
    , то из метода require произойдет нормальный выход. В противном случае объект не создастся и будет выдано исключение
    IllegalArgumentException
    6 .5 . Добавление полей
    Теперь, когда первичный конструктор выдвигает нужные предусловия, мы переключимся на поддержку сложения. Для этого определим в классе
    Rational публичный метод add
    , получающий в качестве параметра еще одно значение типа
    Rational
    . Чтобы сохранить неизменяемость класса
    Rational
    , метод add не должен прибавлять переданное рациональное число к объекту, в отношении которого он вызван. Ему нужно создать и вернуть новый объ­
    ект
    Rational
    , содержащий сумму. Можно подумать, что метод add создается следующим образом:
    class Rational(n: Int, d: Int): // Этот код не будет скомпилирован require(d != 0)
    override def toString = s"$n/$d def add(that: Rational): Rational =
    Rational(n * that.d + that.n * d, d * that.d)
    Но, получив этот код, компилятор выдаст свои возражения:
    5 | Rational(n * that.d + that.n * d, d * that.d)
    | ˆˆˆˆˆˆ
    |value n in class Rational cannot be accessed as a member
    | of (that : Rational) from class Rational.
    5 | Rational(n * that.d + that.n * d, d * that.d)
    | ˆˆˆˆˆˆ
    |value d in class Rational cannot be accessed as a member
    | of (that : Rational) from class Rational.
    5 | Rational(n * that.d + that.n * d, d * that.d)
    | ˆˆˆˆˆˆ
    |value d in class Rational cannot be accessed as a member
    | of (that : Rational) from class Rational.
    Хотя параметры n
    и d
    класса находятся в области видимости кода вашего метода add
    , получить доступ к их значениям можно только в объекте, в от­
    ношении которого вызван данный метод. Следовательно, когда в реализации последнего указывается n
    или d
    , компилятор рад предоставить вам значения

    6 .5 . Добавление полей 131
    для этих параметров класса. Но он не может позволить указать that.n или that.d
    , поскольку они не ссылаются на объект
    Rational
    , в отношении кото­
    рого был вызван метод add
    1
    . Чтобы получить доступ к числителю и знамена­
    телю, вам нужно превратить их в поля. В листинге 6.1 показано, как можно добавить эти поля в класс
    Rational
    2
    Листинг 6.1. Класс Rational с полями class Rational(n: Int, d: Int):
    require(d != 0)
    val numer: Int = n val denom: Int = d override def toString = s"$numer/$denom"
    def add(that: Rational): Rational =
    Rational(
    numer * that.denom + that.numer * denom,
    denom * that.denom
    )
    В версии
    Rational
    , показанной в листинге, добавлены два поля с именами numer и denom
    , которые были проинициализированы значениями параме­
    тров n
    и d
    данного класса
    3
    . Вдобавок были внесены изменения в реализацию методов toString и add
    , позволяющие им использовать поля, а не параметры класса. Эта версия класса
    Rational проходит компиляцию. Ее можно про­
    тестировать путем сложения рациональных чисел:
    val oneHalf = Rational(1, 2) // 1/2
    val twoThirds = Rational(2, 3) // 2/3 oneHalf.add(twoThirds) // 7/6
    Теперь вы уже можете сделать то, чего не могли сделать раньше, а именно получить доступ к значениям числителя и знаменателя из­за пределов объекта. Для этого нужно просто обратиться к публичным полям numer и denom
    :
    1
    Фактически объект
    Rational можно сложить с самим собой, тогда ссылка будет на тот же объект, в отношении которого был вызван метод add
    . Но поскольку данному методу можно передать любой объект
    Rational
    , то компилятор все же не позволит вам воспользоваться кодом that.n
    2
    В разделе 10.6 вы узнаете о параметрических полях, которые позволяют сделать тот же самый код короче.
    3
    Несмотря на то что n
    и d
    используются в теле класса и учитывая, что они применя­
    ются только внутри конструкторов, компилятор Scala не станет выделять под них поля. Таким образом, получив этот код, компилятор Scala создаст класс с двумя полями типа
    Int
    : одним для numer
    , другим для denom

    132 Глава 6 • Функциональные объекты val r = Rational(1, 2) // 1/2
    r.numer // 1
    r.denom // 2 6 .6 . Собственные ссылки
    Ключевое слово this позволяет сослаться на экземпляр объекта, в отноше­
    нии которого был вызван выполняемый в данный момент метод, или, если оно использовалось в конструкторе, — на создаваемый экземпляр объекта.
    Рассмотрим в качестве примера добавление метода lessThan
    . Он проверяет, не имеет ли объект
    Rational
    , в отношении которого он вызван, значение меньше значения параметра:
    def lessThan(that: Rational) =
    this.numer * that.denom < that.numer * this.denom
    Здесь выражение this.numer ссылается на числительное объекта, в отноше­
    нии которого вызван метод lessThan
    . Можно также не указывать префикс this и написать просто numer
    , обе записи будут равнозначны.
    В качестве примера случаев, когда вам не обойтись без this
    , рассмотрим до­
    бавление к классу
    Rational метода max
    , возвращающего наибольшее число из заданного рационального числа и переданного аргумента:
    def max(that: Rational) =
    if this.lessThan(that) then that else this
    Здесь первое ключевое слово this избыточно. Можно его не указывать и на­
    писать lessThan(that)
    . Но второе ключевое слово this представляет резуль­
    тат метода в том случае, если тест вернет false
    , и если вы его не укажете, то возвращать будет просто нечего!
    6 .7 . Вспомогательные конструкторы
    Иногда нужно, чтобы в классе было несколько конструкторов. В Scala все конструкторы, кроме первичного, называются вспомогательными. Например, рациональное число со знаменателем 1 можно кратко записать просто в виде числителя. Вместо 5/1, например, можно просто указать 5. Поэтому было бы неплохо, чтобы вместо записи
    Rational(5,
    1)
    программисты могли напи­
    сать просто
    Rational(5)
    . Для этого потребуется добавить к классу
    Rational вспомогательный конструктор, который получает только один аргумент — числитель, а в качестве знаменателя имеет предопределенное значение
    1
    . Как может выглядеть соответствующий код, показано в листинге 6.2.

    6 .7 . Вспомогательные конструкторы 133
    Листинг 6.2. Класс Rational со вспомогательным конструктором class Rational(n: Int, d: Int):
    require(d != 0)
    val numer: Int = n val denom: Int = d def this(n: Int) = this(n, 1) // вспомогательный конструктор override def toString = s"$numer/$denom"
    def add(that: Rational): Rational =
    Rational(
    numer * that.denom + that.numer * denom,
    denom * that.denom
    )
    Определения вспомогательных конструкторов в Scala начинаются с def this(...)
    . Тело вспомогательного конструктора класса
    Rational просто вызывает первичный конструктор, передавая дальше свой единственный аргумент n
    в качестве числителя и
    1
    — в качестве знаменателя. Увидеть вспо­
    могательный конструктор в действии можно, набрав в REPL следующий код:
    val y = Rational(3) // 3/1
    В Scala каждый вспомогательный конструктор в качестве первого действия должен вызывать еще один конструктор того же класса. Иными словами, первой инструкции в каждом вспомогательном конструкторе каждого класса
    Scala следует иметь вид this(...)
    . Вызываемым должен быть либо первич­
    ный конструктор (как в примере с классом
    Rational
    ), либо другой вспо­
    могательный конструктор, который появляется в тексте программы перед вызывающим его конструктором. Конечный результат применения данного правила заключается в том, что каждый вызов конструктора в Scala должен в конце концов завершаться вызовом первичного конструктора класса. Пер­
    вичный конструктор, таким образом, — единственная точка входа в класс.
    ПРИМЕЧАНИЕ
    Знатоков Java может удивить то, что в Scala правила в отношении конструк- торов более строгие, чем в Java . Ведь в Java первым действием конструктора должен быть либо вызов другого конструктора того же класса, либо вызов конструктора суперкласса напрямую . В классе Scala конструктор суперклас- са может быть вызван только первичным конструктором . Более сильные ограничения в Scala фактически являются компромиссом дизайна — платой за большую лаконичность и простоту конструкторов Scala по сравнению с конструкторами Java . Суперклассы и подробности вызова конструкторов и наследования будут рассмотрены в главе 10 .

    134 Глава 6 • Функциональные объекты
    6 .8 . Приватные поля и методы
    В предыдущей версии класса
    Rational мы просто инициализировали numer значением n
    , а denom
    — значением d
    . Из­за этого числитель и знаменатель
    Rational могут превышать необходимые значения. Например, дробь 66/42 можно сократить и привести к виду 11/7, но первичный конструктор класса
    Rational пока этого не делает:
    Rational(66, 42) // 66/42
    Чтобы выполнить такое сокращение, нужно разделить числитель и знаме­
    натель на их наибольший общий делитель. Например, таковым для 66 и 42 будет число 6. (Иными словами, 6 — наибольшее целое число, на которое без остатка делится как 66, так и 42.) Деление и числителя, и знаменателя числа 66/42 на 6 приводит к получению сокращенной формы 11/7. Один из способов решения данной задачи показан в листинге 6.3.
    В данной версии класса
    Rational было добавлено приватное поле g
    и из­
    менены инициализаторы для полей numer и denom
    . (Инициализатором на­
    зывается код, инициализирующий переменную, например n
    /
    g
    , который инициализирует поле numer
    .) Поле g
    является приватным, поэтому доступ к нему может быть выполнен изнутри, но не снаружи тела класса. Кроме того, был добавлен приватный метод по имени gcd
    , вычисляющий наиболь­
    ший общий делитель двух переданных ему значений
    Int
    . Например, вызов gcd(12,
    8)
    дает результат
    4
    . Как было показано в разделе 4.1, чтобы сделать поле или метод приватным, следует просто поставить перед его определе­
    нием ключевое слово private
    . Назначение приватного «вспомогательного метода» gcd
    — обособление кода, необходимого для остальных частей класса, в данном случае для первичного конструктора. Чтобы обеспечить постоянное положительное значение поля g
    , методу передаются абсолютные значения параметров n
    и d
    , которые вызов получает в отношении этих параметров метода abs
    . Последний может вызываться в отношении любого
    Int
    ­объекта в целях получения его абсолютного значения.
    Листинг 6.3. Класс Rational с приватным полем и методом class Rational(n: Int, d: Int):
    require(d != 0)
    private val g = gcd(n.abs, d.abs)
    val numer = n / g val denom = d / g def this(n: Int) = this(n, 1)

    6 .9 . Определение операторов 135
    def add(that: Rational): Rational =
    Rational(
    numer * that.denom + that.numer * denom,
    denom * that.denom
    )
    override def toString = s"$numer/$denom"
    private def gcd(a: Int, b: Int): Int =
    if b == 0 then a else gcd(b, a % b)
    Компилятор Scala поместит коды инициализаторов трех полей класса
    Rational в первичный конструктор в порядке их следования в исходном коде. Таким образом, инициализатор поля g
    , имеющий код gcd(n.abs,
    d.abs)
    , будет выполнен до выполнения двух других инициализаторов, поскольку в исходном коде появляется первым. Поле g
    будет инициализировано резуль­
    татом — наибольшим общим делителем абсолютных значений параметров n
    и d
    класса. Затем поле g
    будет использовано в инициализаторах полей numer и denom
    . Разделив n
    и d
    на их наибольший общий делитель g
    , каждый объект
    Rational можно сконструировать в нормализованной форме:
    Rational(66, 42) // 11/7 6 .9 . Определение операторов
    Текущая реализация класса
    Rational нас вполне устраивает, но ее можно сделать гораздо более удобной в использовании. Вы можете спросить, по­
    чему допустима запись x + y если x
    и y
    — целые числа или числа с плавающей точкой. Но когда это рацио­
    нальные числа, приходится пользоваться записью x.add(y)
    или в крайнем случае x add y
    Такое положение дел ничем не оправдано. Рациональные числа совершенно неотличимы от остальных чисел. В математическом смысле они гораздо бо­
    лее естественны, чем, скажем, числа с плавающей точкой. Так почему бы не воспользоваться во время работы с ними естественными математическими

    136 Глава 6 • Функциональные объекты операторами? И в Scala есть такая возможность. Она будет показана в остав­
    шейся части главы.
    Сначала нужно заменить add обычным математическим символом. Сделать это нетрудно, поскольку знак
    +
    является в Scala вполне допустимым иден­
    тификатором. Можно просто определить метод с именем
    +
    . Если уж на то пошло, то можно определить и метод
    *
    , выполняющий умножение. Результат показан в листинге 6.4.
    Листинг 6.4. Класс Rational с методами-операторами class Rational(n: Int, d: Int):
    require(d != 0)
    private val g = gcd(n.abs, d.abs)
    val numer = n / g val denom = d / g def this(n: Int) = this(n, 1)
    def + (that: Rational): Rational =
    Rational(
    numer * that.denom + that.numer * denom,
    denom * that.denom
    )
    def * (that: Rational): Rational =
    Rational(numer * that.numer, denom * that.denom)
    override def toString = s"$numer/$denom"
    private def gcd(a: Int, b: Int): Int =
    if b == 0 then a else gcd(b, a % b)
    После такого определения класса
    Rational можно будет воспользоваться следующим кодом:
    val x = Rational(1, 2) // 1/2
    val y = Rational(2, 3) // 2/3
    x + y // 7/6
    Как всегда, синтаксис оператора в последней строке ввода — эквивалент вы­
    зова метода. Можно также использовать следующий код:
    x.+(y) // 7/6
    но читать его будет намного труднее.

    6 .10 . Идентификаторы в Scala 137
    Следует также заметить, что из­за действующих в Scala правил приоритета операторов, рассмотренных в разделе 5.9, метод
    *
    будет привязан к объ­
    ектам
    Rational сильнее метода
    +
    . Иными словами, выражения, в которых к объектам
    Rational применяются операции
    +
    и
    *
    , будут вести себя вполне ожидаемым образом. Например, x
    +
    x
    *
    y будет выполняться как x
    +
    (x
    *
    y)
    , а не как
    (x
    +
    x)
    *
    y
    :
    x + x * y // 5/6
    (x + x) * y // 2/3
    x + (x * y) // 5/6 6 .10 . Идентификаторы в Scala
    Вам уже встречались два наиболее важных способа составления идентифи­
    каторов в Scala — из буквенно­цифровых символов и из операторов. В Scala используются весьма гибкие правила формирования идентификаторов.
    Кроме двух уже встречавшихся форм, существует еще две. Все четыре формы составления идентификаторов рассматриваются в этом разделе.
    Буквенно-цифровые идентификаторы начинаются с буквы или знака под­
    черкивания, за которыми могут следовать другие буквы, цифры или знаки подчеркивания. Символ
    $
    также считается буквой, но зарезервирован для идентификаторов, создаваемых компилятором Scala. Идентификаторы в пользовательских программах не должны содержать символы
    $
    , несмотря на возможность успешно пройти компиляцию: если это произойдет, то мо­
    гут возникнуть конфликты имен с теми идентификаторами, которые будут созданы компилятором Scala.
    В Scala соблюдается соглашение, принятое в Java относительно применения идентификаторов в смешанном регистре
    1
    , таких как toString и
    HashSet
    . Хотя использование знаков подчеркивания в идентификаторах вполне допустимо, в программах на Scala они встречаются довольно редко — отчасти в целях со­
    блюдения совместимости с Java, а также из­за того, что знаки подчеркивания в коде Scala активно применяются не только для идентификаторов. Поэтому лучше избегать таких идентификаторов, как, например, to_string
    ,
    __init__
    или name_
    . Имена полей, параметры методов, имена локальных перемен­
    ных и имена функций в смешанном регистре должны начинаться с буквы в нижнем регистре, например: length
    , flatMap и s
    . Имена классов и трейтов
    1
    Этот стиль именования идентификаторов называется верблюжьим, поскольку у идентификаторов ИмеютсяГорбы, состоящие из символов в верхнем регистре.

    1   ...   11   12   13   14   15   16   17   18   ...   64


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