Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
Скачать 6.24 Mb.
|
Проблема во включении в строку пробелов перед второй строкой текста! Чтобы справиться с этой весьма часто возникающей ситуацией, вы можете вызывать в отношении строк метод stripMargin . Чтобы им воспользоваться, поставьте символ вертикальной черты ( | ) перед каждой строкой текста, а за тем в отношении всей строки вызовите метод stripMargin : println("""|Welcome to Ultamix 3000. |Type "HELP" for help.""".stripMargin) Вот теперь код ведет себя подобающим образом: Welcome to Ultamix 3000. Type "HELP" for help. Булевы литералы У типа Boolean имеется два литерала, true и false : val bool = true // true: Boolean val fool = false // false: Boolean Вот, собственно, и все. Теперь вы буквально (или литерально) стали боль шим специалистом по Scala. 5 .3 . Интерполяция строк В Scala включен довольно гибкий механизм для интерполяции строк, позволяющий вставлять выражения в строковые литералы. В самом рас пространенном случае использования этот механизм предоставляет лако ничную и удобочитаемую альтернативу конкатенации строк. Рассмотрим пример: val name = "reader" println(s"Hello, $name!") Выражение s"Hello, $name!" — обрабатываемый строковый литерал. По скольку за буквой s стоят открывающие кавычки, то Scala для обработки литерала воспользуется интерполятором строк s . Он станет вычислять каждое встроенное выражение, вызывая в отношении каждого результата метод toString и заменяя встроенные выражения в литерале этими резуль татами. Таким образом, из s"Hello, $name!" получится "Hello, reader!" , 5 .3 . Интерполяция строк 111 то есть точно такой же результат, как при использовании кода "Hello, " + name + "!" После знака доллара ( $ ) в обрабатываемом строковом литерале можно ука зать любое выражение. Для выражений с одной переменной зачастую можно просто поместить после знака доллара имя этой переменной. Все символы, вплоть до первого символа, не относящегося к идентификатору, Scala будет интерпретировать как выражение. Если в него включены символы, не явля ющиеся идентификаторами, то это выражение следует заключить в фигур ные скобки, а открывающая фигурная скобка должна ставиться сразу же после знака доллара, например: scala> s"The answer is ${6 * 7}." val res0: String = The answer is 42. Scala содержит еще два интерполятора строк: raw и f . Интерполятор строк raw ведет себя практически так же, как и s , за исключением того, что не рас познает управляющие последовательности символьных литералов (те самые, которые показаны в табл. 5.2). Например, следующая инструкция выводит четыре, а не два обратных слеша: println(raw"No\\\\escape!") // выводит: No\\\\escape! Интерполятор строк f позволяет прикреплять к встроенным выражениям инструкции форматирования в стиле функции printf . Инструкции ставятся после выражения и начинаются со знака процента ( % ), при этом используется синтаксис, заданный классом java.util.Formatter . Например, вот как можно было бы отформатировать число π: scala> f"${math.Pi}%.5f" val res1: String = 3.14159 Если для встроенного выражения не указать никаких инструкций форма тирования, то интерполятор строк f по умолчанию превратится в %s , что означает подстановку значения, полученного в результате выполнения ме тода toString , точно так же, как это делает интерполятор строк s , например: scala> val pi = "Pi" val pi: String = Pi scala> f"$pi is approximately ${math.Pi}%.8f." val res2: String = Pi is approximately 3.14159265. В Scala интерполяция строк реализуется перезаписью кода в ходе ком пиляции. Компилятор в качестве выражения интерполятора строк будет 112 Глава 5 • Основные типы и операции рассматривать любое выражение, состоящее из идентификатора, за которым сразу же стоят открывающие двойные кавычки строкового литерала. Интер поляторы строк s , f и raw реализуются с помощью этого общего механизма. Библиотеки и пользователи могут определять другие интерполяторы строк, применяемые в иных целях. 5 .4 . Все операторы являются методами Для основных типов Scala предоставляет весьма богатый набор операторов. Как упоминалось в предыдущих главах, эти операторы — всего лишь прият ный синтаксис для обычных вызовов методов. Например, 1 + 2 означает то же самое, что и 1.+(2) . Иными словами, в классе Int имеется метод по имени + , который получает Int значение и возвращает Int результат. Он вызывается при сложении двух Int значений: val sum = 1 + 2 // Scala вызывает 1.+(2) Чтобы убедиться в этом, можете набрать выражение, в точности соответ ствующее вызову метода: scala> val sumMore = 1.+(2) val sumMore: Int = 3 Фактически в классе Int содержится несколько перегруженных методов + , получающих различные типы параметров 1 . Например, у Int есть еще один метод, тоже по имени + , который получает и возвращает значения типа Long При сложении Long и Int будет вызван именно этот альтернативный метод: scala> val longSum = 1 + 2L // Scala вызывает 1.+(2L) val longSum: Long = 3 Символ + — оператор, точнее, инфиксный оператор. Форма записи опера торов не ограничивается методами, подобными + , которые в других языках выглядят как операторы. Любой метод может использоваться в нотации операторов, если он принимает только один параметр 2 . Например, в классе String есть метод indexOf , получающий один параметр типа Char . Метод indexOf ведет поиск первого появления в строке указанного символа и воз 1 Перегруженные методы имеют точно такие же имена, но используют другие типы аргументов. Более подробно перегрузка методов рассматривается в разделе 6.11. 2 В будущих версиях Scala методы с несимвольными именами будут разрешены в качестве операторов только в том случае, если они объявлены с модификато ром infix 5 .4 . Все операторы являются методами 113 вращает его индекс или –1 , если символ найден не будет. Метод indexOf можно использовать как оператор: scala> val s = "Hello, world!" val s: String = Hello, world! scala> s indexOf 'o' // Scala вызывает s.indexOf('o') val res0: Int = 4 Любой однопараметрический метод может быть оператором В Scala операторы не относятся к специальному синтаксису языка. Любой метод, который содержит один параметр, может быть операто ром. Однопараметрический метод становится оператором в зависимо сти от того, как вы его используете. Если вы напишете s.indexOf('o') , то indexOf не будет являться оператором, но станет им, если запись будет иметь вид формы оператора — s indexOf 'o' До сих пор рассматривались только примеры инфиксной формы записи операторов, означающей, что вызываемый метод находится между объектом и параметром или параметрами, которые нужно передать методу, как в вы ражении 7 + 2 . В Scala также имеются две другие формы записи операторов: префиксная и постфиксная. В префиксной форме записи имя метода ста вится перед объектом, в отношении которого вызывается этот метод (напри мер, – в выражении –7 ). В постфиксной форме имя метода ставится после объекта (например, toLong в выражении 7 toLong ). В отличие от инфиксной формы записи, в которой операторы получают два операнда (один слева, другой справа), префиксные и постфиксные операторы являются унарными — получают только один операнд. В префиксной форме записи операнд размещается справа от оператора. В качестве примеров мож но привести выражения –2.0 , !found и 0xFF . Как и в случае использования инфиксных операторов, эти префиксные операторы являются сокращенной формой вызова методов. Но в данном случае перед символом оператора в име ни метода ставится приставка unary_ . Например, Scala превратит выражение –2.0 в вызов метода (2.0).unary_– . Вы можете убедиться в этом, набрав вызов метода как с использованием формы записи операторов, так и в явном виде: scala> -2.0 // Scala вызывает (2.0).unary_- val res2: Double = -2.0 scala> (2.0).unary_- val res3: Double = -2.0 114 Глава 5 • Основные типы и операции Идентификаторами, которые могут служить в качестве префиксных операто ров, являются только + , – , ! и . Следовательно, если вы определите метод по имени unary_! , то сможете вызвать его в отношении значения или переменной подходящего типа, прибегнув к префиксной форме записи операторов, напри мер !p . Но, определив метод по имени unary_* , вы не сможете использовать префиксную форму записи операторов, поскольку * не входит в число четы рех идентификаторов, которые могут использоваться в качестве префиксных операторов. Метод можно вызвать обычным способом как p.unary_* , но при попытке вызвать его в виде *p Scala воспримет код так, словно он записан в виде *.p , что, вероятно, совершенно не совпадает с задуманным 1 ! Постфиксные операторы, будучи вызванными без точки или круглых ско бок, являются методами, не получающими аргументов. В Scala при вызове метода пустые круглые скобки можно не ставить. Соглашение гласит, что круглые скобки ставятся, если метод имеет побочные эффекты, как в случае с методом println() . Но их можно не ставить, если метод не имеет побочных эффектов, как в случае с методом toLowerCase , вызываемым в отношении значения типа String : scala> val s = "Hello, world!" val s: String = Hello, world! scala> s.toLowerCase val res4: String = hello, world! В последнем случае, где методу не требуются аргументы, можно при желании не ставить точку и воспользоваться постфиксной формой записи операто ров. Однако компилятор потребует, чтобы вы импортировали scala.lan- guage.postfixOps , прежде чем вызывать метод: scala> import scala.language.postfixOps scala> s toLowerCase val res5: String = hello, world! Здесь метод toLowerCase используется в качестве постфиксного оператора в отношении операнда s Чтобы понять, какие операторы можно использовать с основными типами Scala, нужно посмотреть на методы, объявленные в классах типов, в доку ментации по Scala API. Но данная книга — пособие по языку Scala, поэтому в нескольких следующих разделах будет представлен краткий обзор боль шинства этих методов. 1 Однако не обязательно все будет потеряно. Есть весьма незначительная вероят ность того, что программа с кодом *p может скомпилироваться как код C++. 5 .5 . Арифметические операции 115 УСКОРЕННЫЙ РЕЖИМ ЧТЕНИЯ ДЛЯ JAVA-ПРОГРАММИСТОВ Многие аспекты Scala, рассматриваемые в оставшейся части главы, со- впадают с аналогичными Java-аспектами . Если вы хорошо разбираетесь в Java и у вас мало времени, то можете спокойно перейти к разделу 5 .8, в котором рассматриваются отличия Scala от Java в области равенства объектов . 5 .5 . Арифметические операции Арифметические методы при работе с любыми числовыми типами можно вызвать в инфиксной форме для сложения ( + ), вычитания ( – ), умноже ния ( * ), деления ( / ) и получения остатка от деления ( % ). Вот несколько примеров: 1.2 + 2.3 // 3.5: Double 3 — 1 // 2: Int 'b' — 'a' // 1: Int 2L * 3L // 6: Long 11 / 4 // 2: Int 11 % 4 // 3: Int 11.0f / 4.0f // 2.75: Float 11.0 % 4.0 // 3.0: Double Когда целочисленными типами являются как правый, так и левый операнды ( Int , Long , Byte , Short или Char ), оператор / выведет всю числовую часть результата деления, исключая остаток. Оператор % показывает остаток от предполагаемого целочисленного деления. Остаток от деления числа с плавающей точкой, полученный с помощью метода % , не определен в стандарте IEEE 754. Что касается операции вы числения остатка, в этом стандарте используется деление с округлением, а не деление с отбрасыванием остатка. Поэтому данная операция сильно от личается от операции вычисления остатка от целочисленного деления. Если всетаки нужно получить остаток по стандарту IEEE 754, то можно вызвать метод IEEEremainder из scala.math : math.IEEEremainder(11.0, 4.0) // -1.0: Double Числовые типы также предлагают прибегнуть к унарным префиксным операторам + (метод unary_+ ) и – (метод unary_– ), позволяющим показать положительное или отрицательное значение числового литерала, как в –3 или +4.0 . Если не указать унарный + или – , то литерал интерпретируется как положительный. Унарный + существует исключительно для симметрии с унарным – , однако не производит никаких действий. Унарный – может 116 Глава 5 • Основные типы и операции также использоваться для смены знака переменной. Вот несколько при меров: val neg = 1 + -3 // -2 : Neg val y = +3 // 3: Int -neg // 2: Int 5 .6 . Отношения и логические операции Числовые типы можно сравнивать с помощью методов отношений «боль ше» ( > ), «меньше» ( < ), «больше или равно» ( >= ) и «меньше или равно» ( <= ), которые выдают в качестве результата булево значение. Дополнительно, чтобы инвертировать булево значение, можно использовать унарный опе ратор ! (метод unary_! ). Вот несколько примеров: 1 > 2 // false: Boolean 1 < 2 // true: Boolean 1.0 <= 1.0 // true: Boolean 3.5f >= 3.6f // false: Boolean 'a' >= 'A' // true: Boolean val untrue = !true // false: Boolean Методы «логическое И» ( && и & ) и «логическое ИЛИ» ( || и | ) получают операнды типа Boolean в инфиксной нотации и выдают результат в виде Boolean значения, например: val toBe = true // true: Boolean val question = toBe || !toBe // true: Boolean val paradox = toBe && !toBe // false: Boolean Операции && и || , как и в Java, — сокращенно вычисляемые: выражения, по строенные с помощью этих операторов, вычисляются, только когда это нуж но для определения результата. Иными словами, правая часть выражений с использованием && и || не будет вычисляться, если результат уже опреде лился при вычислении левой части. Например, если левая часть выражения с методом && вычисляется в false , то результатом выражения, несомненно, будет false , поэтому правая часть не вычисляется. Аналогично этому если левая часть выражения с методом || вычисляется в true , то результатом выражения конечно же будет true , поэтому правая часть не вычисляется: scala> def salt() = { println("salt"); false } def salt(): Boolean scala> def pepper() = { println("pepper"); true } def pepper(): Boolean 5 .7 . Поразрядные операции 117 scala> pepper() && salt() pepper salt val res21: Boolean = false scala> salt() && pepper() salt val res22: Boolean = false В первом выражении вызываются pepper и salt , но во втором вызывается только salt . Поскольку salt возвращает false , то необходимость в вызове pepper отпадает. Если правую часть нужно вычислить при любых условиях, то вместо пока занных выше методов следует обратиться к методам & и | . Первый выполняет логическую операцию И, а второй — операцию ИЛИ, но при этом они не прибегают к сокращенному вычислению, как это делают методы && и || . Вот как выглядит пример их использования: scala> salt() & pepper() salt pepper val res23: Boolean = false ПРИМЕЧАНИЕ Возникает вопрос: как сокращенное вычисление может работать, если операторы — всего лишь методы? Обычно все аргументы вычисляются перед входом в метод, тогда каким же образом метод может избежать вы- числения своего второго аргумента? Дело в том, что у всех методов Scala есть средство для задержки вычисления его аргументов или даже полной его отмены . Оно называется «параметр, передаваемый по имени» и будет рассмотрено в разделе 9 .5 . 5 .7 . Поразрядные операции Scala позволяет выполнять операции над отдельными разрядами целочис ленных типов, используя несколько поразрядных методов. К таким методам относятся поразрядное И ( & ), поразрядное ИЛИ ( | ) и поразрядное исклю чающее ИЛИ ( ^ ) 1 . Унарный поразрядный оператор дополнения ( , метод unary_ ) инвертирует каждый разряд в своем операнде, например: 1 Метод поразрядного исключающего ИЛИ выполняет соответствующую опера цию в отношении своих операндов. Из одинаковых разрядов получается 0 , а из разных -1 . Следовательно, выражение 0011 ^ 0101 вычисляется в 0110 118 Глава 5 • Основные типы и операции 1 & 2 // 0: Int 1 | 2 // 3: Int 1 ^ 3 // 2: Int 1 // -2: Int В первом выражении, 1 & 2 , выполняется поразрядное И над каждым раз рядом чисел 1 ( 0001 ) и 2 ( 0010 ) и выдается результат 0 ( 0000 ). Во втором выражении, 1 | 2 , выполняется поразрядное ИЛИ над теми же операндами и выдается результат 3 ( 0011 ). В третьем выражении, 1 ^ 3 , выполняется поразрядное исключающее ИЛИ над каждым разрядом 1 ( 0001 ) и 3 ( 0011 ) и выдается результат 2 ( 0010 ). В последнем выражении, 1 , инвертируется каждый разряд в 1 ( 0001 ) и выдается результат –2 , который в двоичной форме выглядит как 11111111111111111111111111111110 Целочисленные типы Scala также предлагают три метода сдвига: влево ( << ), вправо ( >> ) и беззнаковый сдвиг вправо ( >>> ). Методы сдвига, примененные в инфиксной форме записи операторов, сдвигают разряды целочисленного значения, указанные слева от оператора, на количество разрядов, указанное в целочисленном значении справа от оператора. При сдвиге влево и беззнако вом сдвиге вправо разряды по мере сдвига заполняются нулями. При сдвиге вправо разряды указанного слева значения по мере сдвига заполняются зна чением самого старшего разряда (разряда знака). Вот несколько примеров: -1 >> 31 // -1: Int -1 >>> 31 // 1: Int 1 << 2 // 4: Int Число –1 в двоичном виде выглядит как 11111111111111111111111111111111 В первом примере, –1 >> 31 , в числе –1 происходит сдвиг вправо на 31 разряд ную позицию. В значении типа Int содержатся 32 разряда, поэтому данная операция, по сути, перемещает самый левый разряд до тех пор, пока тот не станет самым правым 1 . Поскольку метод >> выполняет заполнение по мере сдвига единицами ввиду того, что самый левый разряд числа –1 — это 1 , результат получается идентичным исходному левому операнду и состоит из 32 единичных разрядов, или равняется –1 . Во втором примере, –1 >>> 31 , самый левый разряд опять сдвигается вправо в самую правую позицию, од нако на сей раз освобождающиеся разряды заполняются нулями. Поэтому результат в двоичном виде получается 00000000000000000000000000000001 , или 1 . В последнем примере, 1 << 2 , левый операнд, 1 , сдвигается влево на две позиции (освобождающиеся позиции заполняются нулями), в результате 1 Самый левый разряд в целочисленном типе является знаковым. Если самый левый разряд установлен в 1, значит, число отрицательное, если в 0, то положительное. |