Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
Скачать 6.24 Mb.
|
72 Глава 3 • Дальнейшие шаги в Scala Следовательно, код greetStrings(i) преобразуется в код greetStrings. apply(i) . Получается, что элемент массива в Scala является просто вызовом обычного метода, ничем не отличающегося от любого своего собрата. Этот принцип не ограничивается массивами: любое использование объекта в от ношении какихлибо аргументов в круглых скобках будет преобразовано в вызов метода apply . Разумеется, данный код будет скомпилирован, только если в этом типе объекта определен метод apply . То есть это не особый случай, а общее правило. Рис. 3.1. Все операции в Scala являются вызовами методов По аналогии с этим, когда присваивание выполняется в отношении пере менной, к которой применены круглые скобки с одним или несколькими аргументами внутри, компилятор выполнит преобразование в вызов метода update , получающего не только аргументы в круглых скобках, но и объект, расположенный справа от знака равенства. Например, код greetStrings(0) = "Hello" будет преобразован в код greetStrings.update(0, "Hello") Таким образом, следующий код семантически эквивалентен коду листин га 3.1: val greetStrings = new Array[String](3) greetStrings.update(0, "Hello") greetStrings.update(1, ", ") greetStrings.update(2, "world!\n") for i <- 0.to(2) do print(greetStrings.apply(i)) Шаг 8 . Используем списки 73 Концептуальная простота в Scala достигается за счет того, что все — от массивов до выражений — рассматривается как объекты с методами. Вам не нужно запоминать особые случаи, например такие, как существующее в Java различие между примитивными типами и соответствующими им типамиоболочками или между массивами и обычными объектами. Бо лее того, подобное единообразие не вызывает больших потерь произво дительности. Компилятор Scala везде, где только возможно, использует в скомпилированном коде массивы Java, элементарные типы и чистые арифметические операции. Рассмотренные до сих пор в этом шаге примеры компилируются и выпол няются весьма неплохо, однако в Scala имеется более лаконичный способ создания и инициализации массивов, который, как правило, вы и будете ис пользовать (см. листинг 3.2). Данный код создает новый массив длиной три элемента, инициализируемый переданными строками "zero" , "one" и "two" Компилятор выводит тип массива как Array[String] , поскольку ему пере даются строки. Листинг 3.2. Создание и инициализация массива val numNames = Array("zero", "one", "two") Фактически в листинге 3.2 вызывается фабричный метод по имени apply , создающий и возвращающий новый массив. Метод apply получает пере менное количество аргументов 1 и определяется в объекте-компаньоне Array Подробнее объектыкомпаньоны будут рассматриваться в разделе 4.3. Если вам приходилось программировать на Java, то можете воспринимать это как вызов статического метода по имени apply в отношении класса Array . Менее лаконичный способ вызова того же метода apply выглядит следующим об разом: val numNames2 = Array.apply("zero", "one", "two") Шаг 8 . Используем списки Одна из превосходных отличительных черт функционального стиля про граммирования — полное отсутствие у методов побочных эффектов. Един ственным действием метода должно быть вычисление и возвращение зна чения. Получаемые в результате применения такого подхода преимущества 1 Списки аргументов переменной длины или повторяемые параметры рассматрива ются в разделе 8.8. 74 Глава 3 • Дальнейшие шаги в Scala заключаются в том, что методы становятся менее запутанными, и это упро щает их чтение и повторное использование. Есть и еще одно преимущество (в статически типизированных языках): все попадающее в метод и выходя щее за его пределы проходит проверку на принадлежность к определенному типу, поэтому логические ошибки, скорее всего, проявятся сами по себе в виде ошибок типов. Применять данную функциональную философию к миру объектов означает превратить эти объекты в неизменяемые. Как вы уже видели, массив Scala — неизменяемая последовательность объ ектов с общим типом. Тип Array[String] , к примеру, содержит только строки. Изменить длину массива после создания его экземпляра невозможно, но вы можете изменять значения его элементов. Таким образом, массивы относятся к изменяемым объектам. Для неизменяемой последовательности объектов с общим типом можно воспользоваться списком, определяемым Scalaклассом List . Как и в случае применения массивов, в типе List[String] содержатся только строки. Спи сок Scala List отличается от Javaтипа java.util.List тем, что списки Scala всегда неизменямые, а списки Java могут изменяться. В более общем смысле список Scala разработан с прицелом на использование функционального стиля программирования. Список создается очень просто, и листинг 3.3 как раз показывает это. Листинг 3.3. Создание и инициализация списка val oneTwoThree = List(1, 2, 3) Код в листинге 3.3 создает новую val переменную по имени oneTwoThree , инициализируемую новым списком List[Int] с целочисленными элемента ми 1 , 2 и 3 1 . Изза своей неизменяемости списки ведут себя подобно строкам в Java: при вызове метода в отношении списка изза имени данного метода может создаваться впечатление, что обрабатываемый список будет изменен, но вместо этого создается и возвращается новый список с новым значением. Например, в List для объединения списков имеется метод, обозначаемый как ::: . Используется он следующим образом: val oneTwo = List(1, 2) val threeFour = List(3, 4) val oneTwoThreeFour = oneTwo ::: threeFour 1 Использовать запись new List не нужно, поскольку List.apply() определен в объ ектекомпаньоне scala.List как фабричный метод. Более подробно объектыком паньоны рассматриваются в разделе 4.3. Шаг 8 . Используем списки 75 После выполнения этого кода oneTwoThreeFour будет ссылаться на List(1, 2, 3, 4) , но oneTwo попрежнему будет ссылаться на List(1, 2) , а threeFour — на List(3, 4) . Ни один из списков операндов не изменяется оператором кон катенации ::: , который возвращает новый список со значением List(1, 2, 3, 4) . Возможно, работая со списками, вы чаще всего будете пользоваться оператором :: , который называется cons cons добавляет новый элемент в на чало существующего списка и возвращает полученный список. Например, если вы запустите этот код: val twoThree = List(2, 3) val oneTwoThree = 1 :: twoThree значение oneTwoThree будет List(1, 2, 3) ПРИМЕЧАНИЕ В выражении 1 :: twoThree метод :: относится к правому операнду — списку twoThree . Можно заподозрить, будто с ассоциативностью метода :: что-то не то, но есть простое мнемоническое правило: если метод используется в виде оператора, например a * b, то вызывается в отношении левого операнда, как в выражении a .*(b), если только имя метода не закан- чивается двоеточием . А если оно заканчивается двоеточием, то метод вызывается в отношении правого операнда . Поэтому в выражении 1 :: twoThree метод :: вызывается в отношении twoThree с передачей ему 1, то есть twoThree .::(1) . Ассоциативность операторов более подробно будет рассматриваться в разделе 5 .9 . Исходя из того, что короче всего указать пустой список с помощью Nil , один из способов инициализировать новые списки — связать элементы с помощью cons оператора с Nil в качестве последнего элемента 1 . Например, использо вание следующего способа инициализации переменной OneTwoThree даст ей то же значение, что и в предыдущем подходе List(1, 2, 3) : val oneTwoThree = 1 :: 2 :: 3 :: Nil Имеющийся в Scala класс List укомплектован весьма полезными методами, многие из которых показаны в табл. 3.1. Вся эффективность списков будет раскрыта в главе 14. 1 Причина, по которой в конце списка нужен Nil , заключается в том, что метод :: определен в классе List . Если попытаться просто воспользоваться кодом 1 :: 2 :: 3 , то он не пройдет компиляцию, поскольку 3 относится к типу Int , у которого нет метода ::. 76 Глава 3 • Дальнейшие шаги в Scala Тонкости добавления в списки Класс List реализует операцию добавления в список с помощью команды :+ . Подробнее об этом — в главе 24. Однако эта операция используется редко, поскольку время, необходимое для добавления элемента в список, увеличивается в соответствии с размером списка, а время при добавлении методом :: фиксированное и не зависит от размера списка. Если вы хотите эффективно работать со списками, то добавляйте элементы в начало, а в конце вызовите reverse . В против ном случае вы можете использовать ListBuffer — изменяемый список, который реализует операцию добавления, а после ее окончания вы зовите toList ListBuffer будет описан в разделе 15.1. Таблица 3.1. Некоторые методы класса List и их использование Что используется Что этот метод делает List.empty или Nil Создает пустой список List List("Cool", "tools", "rule") Создает новый список типа List[String] с тремя значениями: "Cool" , "tools" и "rule" val thrill = "Will" :: "fill" :: "until" :: Nil Создает новый список типа List[String] с тремя значениями: "Will" , "fill" и "until" List("a", "b") ::: List("c", "d") Объединяет два списка (возвращает новый список типа List[String] со значениями "a" , "b" , "c" и "d" ) thrill(2) Возвращает элемент с индексом 2 (при начале отсчета с нуля) списка thrill (воз вращает "until" ) thrill.count(s => s.length == 4) Подсчитывает количество строковых элементов в thrill , имеющих длину 4 (воз вращает 2) thrill.drop(2) Возвращает список thrill без его первых двух элементов (возвращает List("until") ) thrill.dropRight(2) Возвращает список thrill без двух крайних справа элементов (возвращает List("Will") ) thrill.exists(s => s == "until") Определяет наличие в списке thrill строкового элемента, имеющего значение "until" (возвращает true ) Шаг 8 . Используем списки 77 Что используется Что этот метод делает thrill.filter(s => s.length == 4) Возвращает список всех элементов списка thrill , имеющих длину 4, соблюдая по рядок их следования в списке (возвращает List("Will" , "fill") ) thrill.forall(s => s.endsWith("l")) Показывает, заканчиваются ли все элемен ты в списке thrill буквой "l" (возвращает true ) thrill.foreach(s => print(s)) Выполняет инструкцию print в отноше нии каждой строки в списке thrill (выво дит "Willfilluntil" ) thrill.foreach(print) Делает то же самое, что и предыдущий код, но с использованием более лаконич ной формы записи (также выводит "Willfilluntil" ) thrill.head Возвращает первый элемент в списке thrill (возвращает "Will" ) thrill.init Возвращает список всех элементов списка thrill , кроме последнего (возвращает List("Will", "fill") ) thrill.isEmpty Показывает, не пуст ли список thrill (воз вращает false ) thrill.last Возвращает последний элемент в списке thrill (возвращает "until" ) thrill.length Возвращает количество элементов в спи ске thrill (возвращает 3) thrill.map(s => s + "y") Возвращает список, который получается в результате добавления "y" к каждому строковому элементу в списке thrill (возвращает List("Willy", "filly", "untily") ) thrill.mkString(", ") Создает строку с элементами списка (воз вращает "Will, fill, until" ) thrill.filterNot(s => s.length == 4) Возвращает список всех элементов в по рядке их следования в списке thrill , за ис ключением имеющих длину 4 (возвращает List("until") ) thrill.reverse Возвращает список, содержащий все элементы списка thrill , следующие в об ратном порядке (возвращает List("until", "fill", "Will") ) 78 Глава 3 • Дальнейшие шаги в Scala Что используется Что этот метод делает thrill.sortWith((s, t) => s.charAt(0).toLower < t.charAt(0).toLower) Возвращает список, содержащий все эле менты списка thrill в алфавитном поряд ке с первым символом, преобразованным в символ нижнего регистра (возвращает List("fill", "until", "will") ) thrill.tail Возвращает список thrill за исключе нием его первого элемента (возвращает List("fill", "until") ) Шаг 9 . Используем кортежи Еще один полезный объектконтейнер — кортеж. Как и списки, кортежи не могут быть изменены, но, в отличие от списков, могут содержать различные типы элементов. Список может быть типа List[Int] или List[String] , а кор теж может содержать одновременно как целые числа, так и строки. Кортежи находят широкое применение, например, при возвращении из метода сразу нескольких объектов. Там, где на Java для хранения нескольких возвраща емых значений зачастую приходится создавать JavaBeanподобный класс, в Scala можно просто вернуть кортеж. Все делается просто: чтобы создать экземпляр нового кортежа, содержащего объекты, нужно лишь заключить объекты в круглые скобки, отделив их друг от друга запятыми. Создав экземпляр кортежа, вы можете получить доступ к его элементам по отдель ности с помощью нулевого индекса в круглых скобках. Пример показан в листинге 3.4. Листинг 3.4. Создание и использование кортежа val pair = (99, "Luftballons") val num = pair(0) // тип Int, значение 99 val what = pair(1) // тип String, значение "Luftballons" В первой строке листинга 3.4 создается новый кортеж, содержащий в ка честве первого элемента целочисленное значение 99 , а в качестве второ го — строку "Luftballons" . Scala выводит тип кортежа в виде Tuple2[Int, String] , а также присваивает этот тип паре переменных 1 . Во второй строке 1 Компилятор Scala использует синтаксический сахар для типов кортежей, который выглядит как кортеж типов. Например, Tuple2 [Int, String] представлен как (Int, String) Таблица 3.1 (окончание) Шаг 10 . Используем множества и отображения 79 вы получаете доступ к первому элементу 99 по его индексу 0 1 . Результатом типа pair(0) является Int . В третьей строке вы получаете доступ ко второму элементу Luftballons по его индексу 1. Результатом типа pair(1) является String . Это говорит о том, что кортежи отслеживают индивидуальные типы каждого из своих элементов. Реальный тип кортежа зависит от количества содержащихся в нем эле ментов и от типов этих элементов. Следовательно, типом кортежа (99, "Luftballons") является Tuple2[Int, String] . А типом кортежа ('u', 'r', "the", 1, 4, "me") — Tuple6[Char, Char, String, Int, Int, String] 2 Шаг 10 . Используем множества и отображения Scala призван помочь вам использовать преимущества как функциональ ного, так и объектноориентированного стиля, поэтому в библиотеках его коллекций особое внимание обращают на разницу между изменяемыми и не изменяемыми коллекциями. Например, массивы всегда изменяемы, а списки всегда неизменяемы. Scala также предоставляет изменяемые и неизменяемые альтернативы для множеств и отображений, но использует для обеих версий одни и те же простые имена. Для множеств и отображений Scala моделирует изменяемость в иерархии классов. Например, в API Scala содержится основной трейт для множеств, где этот трейт аналогичен Javaинтерфейсу (более подробно трейты рассматриваются в главе 11). Затем Scala предоставляет два трейтанаследника: один для из меняемых, а второй для неизменяемых множеств. На рис. 3.2 показано, что для всех трех трейтов используется одно и то же простое имя Set . Но их полные имена отличаются друг от друга, поскольку все трейты размещаются в разных пакетах. Классы для конкретных множеств в Scala API, например HashSet (см. рис. 3.2), являются расширениями либо изменяемого, либо неизменяемого трейта Set . (В то время как в Java вы реализуете интерфейсы, в Scala расширяете (иначе говоря, подмешиваете) трейты.) Следовательно, если нужно воспользоваться HashSet , то в зависимо сти от потребностей можно выбирать между его изменяемой и неизменяемой 1 Обратите внимание, что до Scala 3 обращение к элементам кортежа осуществлялось с помощью имен полей, начинающихся с единицы, например _1 или _2 2 Как и в Scala 3, вы можете создавать кортежи любой длины. 80 Глава 3 • Дальнейшие шаги в Scala разновидностями. Способ создания множества по умолчанию показан в ли стинге 3.5. Листинг 3.5. Создание, инициализация и использование неизменяемого множества var jetSet = Set("Boeing", "Airbus") jetSet += "Lear" val query = jetSet.contains("Cessna") // false В первой строке кода листинга 3.5 определяется новая var переменная по имени jetSet , которая инициализируется неизменяемым множеством, содер жащим две строки: "Boeing" и "Airbus" . В этом примере показано, что в Scala множества можно создавать точно так же, как списки и массивы: путем вызо ва фабричного метода по имени apply в отношении объектакомпаньона Set В листинге 3.5 метод apply вызывается в отношении объектакомпаньона для scala.collection.immutable.Set , возвращающего экземпляр исходного, не изменяемого класса Set . Компилятор Scala выводит тип переменной jetSet , определяя его как неизменяемый Set[String] Рис. 3.2. Иерархия классов для множеств Scala Чтобы добавить новый элемент в неизменяемое множество, в отношении последнего вызывается метод + , которому и передается этот элемент. Ме тод + создает и возвращает новое неизменяемое множество с добавленным элементом. Конкретный метод += предоставляется исключительно для из меняемых множеств. |