Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
Скачать 6.24 Mb.
|
100 Глава 4 • Классы и объекты С добавлением модификатора case компилятор сгенерирует для вас несколь ко полезных методов. Вопервых, компилятор создаст объекткомпаньон и поместит в него фабричный метод с именем apply . Таким образом, вы можете создать новый объект Person следующим образом: val p = Person("Sally", 39) Компилятор перепишет эту строку кода в вызов сгенерированного фабрич ного метода: Person.apply("Sally", 39) Вовторых, компилятор будет хранить все параметры класса в полях и ге нерировать методы доступа с тем же именем, что и у заданного параметра 1 Например, вы можете получить доступ к заданным в Person значениям име ни и возраста следующим образом: p.name // Sally p.age // 39 Втретьих, компилятор предоставит вам реализацию toString : p.toString // Person(Sally,39) Вчетвертых, компилятор сгенерирует реализацию hashCode и equals для вашего класса. Эти методы будут основывать свой результат на параметрах, переданных конструктору. Например, объект Person будет учитывать и имя, и возраст при сравнении: p == Person("Sally", 21) // false p.hashCode == Person("Sally", 21).hashCode // false p == Person("James", 39) // false p.hashCode == Person("James", 39).hashCode // false p == Person("Sally", 39) // true p.hashCode == Person("Sally", 39).hashCode // true Компилятор не будет генерировать метод, который вы реализуете самостоя тельно. Он будет использовать вашу реализацию. Вы также можете добавить другие поля и методы к классу и его компаньону. Вот пример, в котором вы определяете метод apply в сопутствующем объекте Person (компилятор не будет его генерировать) и добавляете метод appendToName в класс: case class Person(name: String, age: Int): def appendToName(suffix: String): Person = Person(s"$name$suffix", age) 1 Они называются параметрическими полями, которые будут описаны в разделе 10.6. 4 .5 . Приложение на языке Scala 101 object Person: // Убедитесь, что непустое имя написано с заглавной буквы def apply(name: String, age: Int): Person = val capitalizedName = if !name.isEmpty then val firstChar = name.charAt(0).toUpper val restOfName = name.substring(1) s"$firstChar$restOfName" else throw new IllegalArgumentException("Empty name") new Person(capitalizedName, age) Этот apply метод гарантирует, что первый символ имени будет начинаться с заглавной буквы: val q = Person("sally", 39) // Person(Sally,39) Вы также можете вызвать метод appendToName , который вы определили в классе: q.appendToName(" Smith") // Person(Sally Smith,39) Наконец, компилятор добавляет метод copy в ваш класс и метод unapply к компаньону. Они будут описаны в главе 13. Все эти условности облегчают работу, но с небольшой оговоркой: вам всего лишь понадобится написать модификатор case , а ваши классы и объекты при этом станут немного больше. Они вырастают, потому что генерируются дополнительные методы и для каждого параметра конструктора добавляется неявное поле. 4 .5 . Приложение на языке Scala Чтобы запустить программу на Scala, нужно предоставить имя автономного объектаодиночки с методом main , который получает один параметр с типом Array[String] и имеет результирующий тип Unit . Точкой входа в приложе ние может стать любой самостоятельный объект с методом main , имеющим надлежащую сигнатуру 1 . Пример показан в листинге 4.3. Листинг 4.3. Приложение Summer // Код находится в файле Summer.scala import ChecksumAccumulator.calculate 1 Вы можете обозначить методы другими именами в качестве основных функций с помощью @main . Этот метод будет описан в разделе 23.3. 102 Глава 4 • Классы и объекты object Summer: def main(args: Array[String]): Unit = for arg <- args do println(arg + ": " + calculate(arg)) Объектодиночка, показанный в данном листинге, называется Summer . Его метод main имеет надлежащую сигнатуру, поэтому его можно задействовать в качестве приложения. Первая инструкция в файле импортирует метод calculate , который определен в объекте ChecksumAccumulator из предыду щего примера. Инструкция import позволяет далее использовать в файле простое имя метода 1 . Тело метода main всего лишь выводит на стандартное устройство каждый аргумент и контрольную сумму для аргумента, разделяя их двоеточием. ПРИМЕЧАНИЕ Подразумевается, что в каждый свой исходный файл Scala импортирует элементы пакетов java .lang и scala, а также элементы объекта-одиночки по имени Predef . В Predef, который находится в пакете scala, содержится множество полезных методов . Например, когда в исходном файле Scala встречается println, фактически вызывается println из Predef . (А метод Predef .println, в свою очередь, вызывает метод Console .println, который фактически и выполняет всю работу .) Когда же встречается assert, вы- зывается метод Predef .assert . Чтобы запустить приложение Summer, поместите код из листинга 4.3 в файл Summer.scala . В Summer используется ChecksumAccumulator , поэтому поме стите код для ChecksumAccumulator как для класса, показанного в листин ге 4.1, так и для его объектакомпаньона, показанного в листинге 4.2, в файл ChecksumAccumulator.scala Одним из отличий Scala от Java является то, что в Java от вас требуется поме стить публичный класс в файл, названный по имени класса, например, класс SpeedRacer — в файл SpeedRacer.java . А в Scala файл с расширением .scala можно называть как угодно независимо от того, какие классы Scala или код в них помещаются. Но обычно, когда речь идет не о скриптах, рекоменду ется придерживаться стиля, при котором файлы называются по именам включенных в них классов, как это делается в Java, чтобы программистам было легче искать классы по именам их файлов. Именно этим подходом мы 1 Наличие опыта программирования на Java позволяет сопоставить такой импорт с объявлением статического импорта, введенным в Java 5. Единственное отли чие — в Scala импортировать элементы можно из любого объекта, а не только из объектоводиночек. Резюме 103 и воспользовались в отношении двух файлов в данном примере. Имеются в виду файлы Summer.scala и ChecksumAccumulator.scala Ни ChecksumAccumulator.scala , ни Summer.scala не являются скриптами, поскольку заканчиваются определением. В отличие от этого скрипт должен заканчиваться выражением, выдающим результат. Поэтому при попытке запустить Summer.scala в качестве скрипта интерпретатор Scala пожалуется на то, что Summer.scala не заканчивается выражением, выдающим результат. (Конечно, если предположить, что вы самостоятельно не добавили какое либо выражение после определения объекта Summer .) Вместо этого нужно будет скомпилировать данные файлы с помощью компилятора Scala, а затем запустить получившиеся в результате файлы классов. Для этого можно вос пользоваться основным компилятором Scala по имени scalac : $ scalac ChecksumAccumulator.scala Summer.scala Эта команда скомпилирует ваши исходные файлы и приведет к созданию файлов классов Java, которые затем можно будет запускать через команду scala — ту же самую, с помощью которой вы вызывали интерпретатор в пре дыдущих примерах. Однако вместо того, чтобы указывать ему имя файла с расширением .scala , содержащим код Scala для интерпретации (как вы делали в каждом предыдущем примере) 1 , вы дадите ему имя отдельного объекта, содержащего метод main с соответствующей сигнатурой. Следова тельно, приложение Summer можно запустить, набрав команду: $ scala Summer of love Вы сможете увидеть контрольные суммы, выведенные для двух аргументов командной строки: of: -213 love: -182 Резюме В этой главе мы рассмотрели основы классов и объектов в Scala и показали приемы компиляции и запуска приложений. В следующей главе рассмотрим основные типы данных и варианты их использования. 1 Фактический механизм, который программа Scala использует для «интерпрета ции» исходного файла Scala, заключается в том, что она компилирует исходный код Scala в байткоды Java, немедленно загружает их через загрузчик классов и выполняет их. 5 Основные типы и операции После того как были рассмотрены в действии классы и объекты, самое время поглубже изучить имеющиеся в Scala основные типы и операции. Если вы хорошо знакомы с Java, то вас может обрадовать тот факт, что в Scala и в Java основные типы и операторы имеют тот же смысл. И все же есть интересные различия, ради которых с этой главой стоит ознакомиться даже тем, кто считает себя опытным разработчиком Javaприложений. Не которые аспекты Scala, рассматриваемые в данной главе, в основном такие же, как и в Java, поэтому мы указываем, какие разделы Javaразработчики могут пропустить. В текущей главе мы представим обзор основных типов Scala, включая стро ки типа String и типы значений Int , Long , Short , Byte , Float , Double , Char и Boolean . Кроме того, рассмотрим операции, которые могут выполняться с этими типами, и вопросы соблюдения приоритета операторов в выражени ях Scala. Поговорим мы и о том, как Scala «обогащает» варианты основных типов, позволяя выполнять дополнительные операции вдобавок к тем, что поддерживаются в Java. 5 .1 . Некоторые основные типы В табл. 5.1 показан ряд основных типов, используемых в Scala, а также диа пазоны значений, которые могут принимать их экземпляры. В совокупности типы Byte , Short , Int , Long и Char называются целочисленными. Целочислен ные типы плюс Float и Double называются числовыми. 5 .2 . Литералы 105 Таблица 5.1. Некоторые основные типы Основной тип Диапазон Byte 8битовое знаковое целое число в дополнительном коде (от –2 7 до 2 7 – 1 включительно) Short 16битовое знаковое целое число в дополнительном коде (от –2 15 до 2 15 – 1 включительно) Int 32битовое знаковое целое число в дополнительном коде (от –2 31 до 2 31 – 1 включительно) Long 64битовое знаковое целое число в дополнительном коде (от –2 63 до 2 63 – 1 включительно) Char 16битовый беззнаковый Unicodeсимвол (от 0 до 2 16 – 1 включительно) String Последовательность из Char Float 32битовое число с плавающей точкой одинарной точности, которое соответствует стандарту IEEE 754 Double 64битовое число с плавающей точкой двойной точности, которое соответствует стандарту IEEE 754 Boolean true или false За исключением типа String , который находится в пакете java.lang , все типы, показанные в данной таблице, входят в пакет scala 1 . Например, полное имя типа Int обозначается scala.Int . Но, учитывая, что все элементы пакета scala и java.lang автоматически импортируются в каждый исходный файл Scala, можно повсеместно использовать только простые имена, то есть имена вида Boolean , Char или String Опытные Javaразработчики заметят, что основные типы Scala имеют в точно сти такие же диапазоны, как и соответствующие им типы в Java. Это позволяет компилятору Scala в создаваемом им байткоде преобразовывать экземпляры типов значений Scala, например Int или Double , в примитивные типы Java. 5 .2 . Литералы Все основные типы, перечисленные в табл. 5.1, можно записать с помощью литералов. Литерал представляет собой способ записи постоянного значения непосредственно в коде. 1 Пакеты, кратко рассмотренные в шаге 1 главы 2, более подробно рассматриваются в главе 12. 106 Глава 5 • Основные типы и операции УСКОРЕННЫЙ РЕЖИМ ЧТЕНИЯ ДЛЯ JAVA-ПРОГРАММИСТОВ Синтаксис большинства литералов, показанных в данном разделе, совпадает с синтаксисом, применяемым в Java, поэтому знатоки Java могут спокойно пропустить практически весь раздел . Отдельные различия, о которых стоит прочитать, касаются используемых в Scala неформатированных строк (рас- сматриваются в подразделе «Строковые литералы»), а также интерполяции строк . Кроме того, в Scala не поддерживаются восьмеричные литералы, а целочисленные, начинающиеся с нуля, например 031, не проходят ком- пиляцию . Целочисленные литералы Целочисленные литералы для типов Int , Long , Short и Byte используются в двух видах: десятичном и шестнадцатеричном. Способ, применяемый для начала записи целочисленного литерала, показывает основание числа. Если число начинается с 0x или 0X , то оно шестнадцатеричное (по основанию 16) и может содержать цифры от 0 до 9, а также буквы от A до F в верхнем или нижнем регистре. Вы можете использовать символы подчеркивания (_), чтобы улучшить читаемость больших значений, например: val hex = 0x5 // 5: Int val hex2 = 0x00FF // 255: Int val magic = 0xcafebabe // -889275714: Int val billion = 1_000_000_000 // 1000000000: Int Обратите внимание на то, что оболочка Scala REPL всегда выводит целочис ленные значения в десятичном виде независимо от формы литерала, которую вы могли задействовать для инициализации этих значений. Таким образом, REPL показывает значение переменной hex2 , которая была инициализиро вана с помощью литерала 0x00FF , как десятичное число 255 . (Разумеется, не нужно все принимать на веру. Хорошим способом начать осваивать язык станет практическая работа с этими инструкциями в интерпретаторе по мере чтения данной главы.) Если цифра, с которой начинается число, не ноль и не имеет никаких других знаков отличия, значит, число десятичное (по основанию 10), например: val dec1 = 31 // 31: Int val dec2 = 255 // 255: Int val dec3 = 20 // 20: Int Если целочисленный литерал заканчивается на L или l , значит, показывает число типа Long , в противном случае это число относится к типу Int . По смотрите на примеры целочисленных литералов Long : 5 .2 . Литералы 107 val prog = 0XCAFEBABEL // 3405691582: Long val tower = 35L // 35: Long val of = 31l // 31: Long Если Int литерал присваивается переменной типа Short или Byte , то рас сматривается как принадлежащий к типу Short или Byte , если, конечно, его значение находится внутри диапазона, допустимого для данного типа, например: val little: Short = 367 // 367: Short val littler: Byte = 38 // 38: Byte Литералы чисел с плавающей точкой Литералы чисел с плавающей точкой состоят из десятичных цифр, которые также могут содержать необязательный символ десятичной точки, и после них может стоять необязательный символ E или e и экспонента. Посмотрите на примеры литералов чисел с плавающей точкой: val big = 1.2345 // 1.2345: Double val bigger = 1.2345e1 // 12.345: Double val biggerStill = 123E45 // 1.23E47: Double val trillion = 1_000_000_000e3 // 1.0E12: Double Обратите внимание: экспонента означает степень числа 10, на которую умножается остальная часть числа. Следовательно, 1.2345e1 равняется числу 1,2345, умноженному на 10, то есть получается число 12,345. Если литерал числа с плавающей точкой заканчивается на F или f , значит, число относится к типу Float , в противном случае оно относится к типу Double Дополнительно литералы чисел с плавающей точкой могут заканчиваться на D или d . Посмотрите на примеры литералов чисел с плавающей точкой: val little = 1.2345F // 1.2345: Float val littleBigger = 3e5f // 300000.0: Float Последнее значение, выраженное как тип Double , может также принимать иную форму: val anotherDouble = 3e5 // 300000.0: Double val yetAnother = 3e5D // 300000.0: Double Большие числовые литералы В Scala 3 добавлена экспериментальная функция, которая устраняет огра ничения на размер числовых литералов и позволяет использовать их для 108 Глава 5 • Основные типы и операции инициализации произвольных типов. Вы можете включить эту функцию с помощью импорта этого языка: import scala.language.experimental.genericNumberLiterals Вот два примера из стандартной библиотеки: val invoice: BigInt = 1_000_000_000_000_000_000_000 val pi: BigDecimal = 3.1415926535897932384626433833 Символьные литералы Символьные литералы состоят из любого Unicodeсимвола, заключенного в одинарные кавычки: scala> val a = 'A' val a: Char = A Помимо того что символ представляется в одинарных кавычках в явном виде, его можно указывать с помощью кода из таблицы символов Unicode. Для этого нужно записать \u , после чего указать четыре шестнадцатеричные цифры кода: scala> val d = '\u0041' val d: Char = A scala> val f = '\u0044' val f: Char = D Такие символы в кодировке Unicode могут появляться в любом месте про граммы на языке Scala. Например, вы можете набрать следующий иденти фикатор: scala> val B\u0041\u0044 = 1 val BAD: Int = 1 Он рассматривается точно так же, как идентификатор BAD , являющийся результатом раскрытия символов в кодировке Unicode в показанном ранее коде. По сути, в именовании идентификаторов подобным образом нет ничего хорошего, поскольку их трудно прочесть. Иногда с помощью этого синта ксиса исходные файлы Scala, которые содержат отсутствующие в таблице ASCII символы из таблицы Unicode, можно представить в кодировке ASCII. И наконец, нужно упомянуть о нескольких символьных литералах, пред ставленных специальными управляющими последовательностями (escape sequences), показанными в табл. 5.2, например: scala> val backslash = '\\' val backslash: Char = \ 5 .2 . Литералы 109 Таблица 5.2. Управляющие последовательности специальных символьных литералов Литерал Предназначение \n Перевод строки ( \u000A ) \b Возврат на одну позицию ( \u0008 ) \t Табуляция ( \u0009 ) \f Перевод страницы ( \u000C ) \r Возврат каретки ( \u000D ) \" Двойные кавычки ( \u0022 ) \' Одинарная кавычка ( \u0027 ) \\ Обратный слеш ( \u005C ) Строковые литералы Строковый литерал состоит из символов, заключенных в двойные кавычки: scala> val hello = "hello" val hello: String = hello Синтаксис символов внутри кавычек такой же, как и в символьных литера лах, например: scala> val escapes = "\\\"\'" val escapes: String = \"' Данный синтаксис неудобен для строк, в которых содержится множество управляющих последовательностей, или для строк, не умещающихся в одну строку текста, поэтому для неформатированных строк в Scala включен специальный синтаксис. Неформатированная строка начинается и закан чивается тремя идущими подряд двойными кавычками ( """ ). Внутри нее могут содержаться любые символы, включая символы новой строки, кавычки и специальные символы, за исключением, разумеется, трех кавычек подряд. Например, следующая программа выводит сообщение, используя неформа тированную строку: println("""Welcome to Ultamix 3000. Type "HELP" for help.""") Но при запуске этого кода получается не совсем то, что хотелось: Welcome to Ultamix 3000. Type "HELP" for help. |