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