Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
Скачать 6.24 Mb.
|
Таблица 15.2 (окончание) 15 .2 . Множества и отображения 341 Таблица 15.3. Реализации используемых по умолчанию неизменяемых множеств Количество элементов Реализация 0 scala.collection.immutable.EmptySet 1 scala.collection.immutable.Set1 2 scala.collection.immutable.Set2 3 scala.collection.immutable.Set3 4 scala.collection.immutable.Set4 5 или более scala.collection.immutable.HashSet В целях обеспечения максимальной производительности используемые по умолчанию реализации неизменяемых классов, показанные в табл. 15.3 и 15.4, работают совместно. Например, если добавляется элемент к EmptySet , то возвращается Set1 . Если добавляется элемент к этому Set1 , то возвраща ется Set2 . Если затем удалить элемент из Set2 , то будет опять получен Set1 Таблица 15.4. Реализации используемых по умолчанию неизменяемых отображений Количество элементов Реализация 0 scala.collection.immutable.EmptyMap 1 scala.collection.immutable.Map1 2 scala.collection.immutable.Map2 3 scala.collection.immutable.Map3 4 scala.collection.immutable.Map4 5 или более scala.collection.immutable.HashMap Отсортированные множества и отображения Иногда может понадобиться множество или отображение, итератор которо го возвращает элементы в определенном порядке. Для этого в библиотеке коллекций Scala имеются трейты SortedSet и SortedMap . Они реализованы с помощью классов TreeSet и TreeMap , которые в целях хранения элементов в определенном порядке применяют красночерное дерево (в случае TreeSet ) или ключи (в случае с TreeMap ). Порядок определяется трейтом Ordered , неявный экземпляр которого должен быть определен для типа элементов и множества или типа ключей отображения. Эти классы поставляются в изменяемых и неизменяемых вариантах. Рассмотрим ряд примеров ис пользования TreeSet : 342 Глава 15 • Работа с другими коллекциями import scala.collection.immutable.TreeSet val ts = TreeSet(9, 3, 1, 8, 0, 2, 7, 4, 6, 5) // TreeSet(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) val cs = TreeSet('f', 'u', 'n') // TreeSet(f, n, u) А это ряд примеров использования TreeMap : import scala.collection.immutable.TreeMap var tm = TreeMap(3 –> 'x', 1 –> 'x', 4 –> 'x') // TreeMap(1 –> x, 3 –> x, 4 –> x) tm += (2 –> 'x') tm // TreeMap(1 –> x, 2 –> x, 3 –> x, 4 –> x) 15 .3 . Выбор между изменяемыми или неизменяемыми коллекциями При решении одних задач лучше работают изменяемые коллекции, а при решении других — неизменяемые. В случае сомнений лучше начать с не изменяемой коллекции, а позже при необходимости перейти к изменяе мой, поскольку разобраться в работе неизменяемых коллекций гораздо проще. Иногда может оказаться полезно двигаться в обратном направлении. Если код, использующий изменяемые коллекции, становится сложным и в нем трудно разобраться, то следует подумать, стоит ли заменить некоторые кол лекции их неизменяемыми альтернативами. В частности, если вас волнует вопрос создания копий изменяемых коллекций только в нужных местах либо вы слишком много думаете над тем, кто владеет изменяемой коллекцией или что именно она содержит, то имеет смысл заменить некоторые коллекции их неизменяемыми аналогами. Помимо того что в неизменяемых коллекциях потенциально легче разобрать ся, они, как правило, могут храниться более компактно, чем изменяемые, если количество хранящихся в них элементов невелико. Например, пустое изменяемое отображение в его представлении по умолчанию в виде HashMap занимает примерно 80 байт, и около 16 дополнительных байт требуется для добавления к нему каждой записи. А пустое неизменяемое отображение Map — это один объект, который совместно используется всеми ссылками, и потому ссылаться на него можно, по сути, одним полем указателя. Более того, в настоящее время библиотека коллекций Scala хранит до че тырех записей неизменяемых отображений и множеств в одном объекте, который в зависимости от количества хранящихся в коллекции записей 15 .3 . Выбор между изменяемыми или неизменяемыми коллекциями 343 обычно занимает от 16 до 40 байт 1 . Следовательно, для небольших множеств и отображений неизменяемые версии занимают намного меньше места, чем изменяемые. С учетом того, что многие коллекции весьма невелики, переход на их неизменяемый вариант может существенно сэкономить пространство памяти и обеспечить преимущество в производительности. Чтобы облегчить переход с неизменяемых на изменяемые коллекции и на оборот, Scala предоставляет немного синтаксического сахара. Неизменяемые множества и отображения не поддерживают настоящий метод += , однако в Scala дается полезная альтернативная интерпретация += . Когда исполь зуется запись a += b и a не поддерживает метод по имени += , Scala пытается интерпретировать эту запись как a = a + b Например, неизменяемые множества не поддерживают оператор += : scala> val people = Set("Nancy", "Jane") val people: Set[String] = Set(Nancy, Jane) scala> people += "Bob" 1 |people += "Bob" |ˆˆˆˆˆˆˆˆˆ |value += is not a member of Set[String] Но если объявить people в качестве var , а не val переменной, то коллекцию можно обновить с помощью операции += даже притом, что она неизменяемая. Сначала создается новая коллекция, а затем переменной people присваива ется новое значение для ссылки на новую коллекцию: var people = Set("Nancy", "Jane") people += "Bob" people // Set(Nancy, Jane, Bob) После этой серии инструкций переменная people ссылается на новое неиз меняемое множество, содержащее добавленную строку "Bob" . Та же идея применима не только к методу += , но и к любому другому методу, заканчи вающемуся знаком = . Вот как тот же самый синтаксис используется с опе ратором -= , который удаляет элемент из множества, и с оператором ++= , добавляющим в множество коллекцию элементов: people -= "Jane" people ++= List("Tom", "Harry") people // Set(Nancy, Bob, Tom, Harry) 1 Под одним объектом, как следует из табл. 15.3 и 15.4, понимается экземпляр одного из классов: от Set1 до Set4 или от Map1 до Map4 344 Глава 15 • Работа с другими коллекциями Чтобы понять, насколько это полезно, рассмотрим еще раз пример отобра жения Map из раздела 1.1: var capital = Map("US" –> "Washington", "France" –> "Paris") capital += ("Japan" –> "Tokyo") println(capital("France")) В этом коде используются неизменяемые коллекции. Если захочется попро бовать задействовать вместо них изменяемые коллекции, то нужно будет всего лишь импортировать изменяемую версию Map , переопределив таким образом выполненный по умолчанию импорт неизменяемой версии Map : import scala.collection.mutable.Map // единственное требуемое изменение! var capital = Map("US" –> "Washington", "France" –> "Paris") capital += ("Japan" –> "Tokyo") println(capital("France")) Так легко преобразовать удастся не все примеры, но особая трактовка мето дов, заканчивающихся знаком равенства, зачастую сокращает объем кода, который нуждается в изменениях. Кстати, эта трактовка синтаксиса работает не только с коллекциями, но и с любыми разновидностями значений. Например, здесь она была исполь зована в отношении чисел с плавающей точкой: var roughlyPi = 3.0 roughlyPi += 0.1 roughlyPi += 0.04 roughlyPi // 3.14 Эффект от такого расширяющего преобразования похож на эффект, получа емый от операторов присваивания, использующихся в Java ( += , -= , *= и т. п.), однако носит более общий характер, поскольку преобразован может быть каждый оператор, заканчивающийся на = 15 .4 . Инициализация коллекций Как уже было показано, наиболее широко востребованный способ создания и инициализации коллекции — передача исходных элементов фабричному методу, определенному в объектекомпаньоне класса выбранной вами кол лекции. Элементы просто помещаются в круглые скобки после имени объ ектакомпаньона, и компилятор Scala преобразует это в вызов метода apply в отношении объектакомпаньона: 15 .4 . Инициализация коллекций 345 List(1, 2, 3) Set('a', 'b', 'c') import scala.collection.mutable mutable.Map("hi" –> 2, "there" –> 5) Array(1.0, 2.0, 3.0) Чаще всего можно позволить компилятору Scala вывести тип элемента кол лекции из элементов, переданных ее фабричному методу. Но иногда может понадобиться создать коллекцию, указав притом тип, отличающийся от того, который выберет компилятор. Это особенно касается изменяемых коллек ций. Рассмотрим пример: scala> import scala.collection.mutable scala> val stuff = mutable.Set(42) val stuff: scala.collection.mutable.Set[Int] = HashSet(42) scala> stuff += "abracadabra" 1 |stuff += "abracadabra" | ˆˆˆˆˆˆˆˆˆˆˆˆˆ | Found: ("abracadabra" : String) | Required: Int Проблема здесь заключается в том, что переменной stuff был задан тип элемента Int . Если вы хотите, чтобы типом элемента был Any , то это нужно указать явно, поместив тип элемента в квадратные скобки: scala> val stuff = mutable.Set[Any](42) val stuff: scala.collection.mutable.Set[Any] = HashSet(42) Еще одна особая ситуация возникает при желании инициализировать кол лекцию с помощью другой коллекции. Допустим, есть список, но нужно получить коллекцию TreeSet , содержащую элементы, которые находятся в нем. Список выглядит так: val colors = List("blue", "yellow", "red", "green") Передать список названий цветов фабричному методу для TreeSet невоз можно: scala> import scala.collection.immutable.TreeSet scala> val treeSet = TreeSet(colors) 1 |val treeSet = TreeSet(colors) | ˆ |No implicit Ordering defined for List[String].. Вместо этого вам нужно будет преобразовать список в TreeSet с помощью метода to : 346 Глава 15 • Работа с другими коллекциями val treeSet = colors to TreeSet // TreeSet(blue, green, red, yellow) Метод to принимает в качестве параметра объекткомпаньон коллекции. С его помощью вы можете преобразовать любую коллекцию в другую. Преобразование в массив или список Помимо универсального метода to для преобразования коллекции в другую произвольную коллекцию, вы также можете использовать более конкретные методы для преобразования в наиболее распространенные типы коллекций Scala. Как было показано ранее, чтобы инициализировать новый список с помощью другой коллекции, следует просто вызвать в отношении этой коллекции метод toList : treeSet.toList // List(blue, green, red, yellow) Или же, если нужен массив, вызвать метод toArray : treeSet.toArray // Array(blue, green, red, yellow) Обратите внимание: несмотря на неотсортированность исходного списка co- lors , элементы в списке, создаваемом вызовом toList в отношении TreeSet , стоят в алфавитном порядке. Когда в отношении коллекции вызывается toList или toArray , порядок следования элементов в списке, получающемся в результате, будет таким же, как и порядок следования элементов, создавае мый итератором на этой коллекции. Поскольку итератор, принадлежащий типу TreeSet[String] , будет выдавать строки в алфавитном порядке, то они в том же порядке появятся и в списке, который создается в результате вызова toList в отношении объекта TreeSet Разница между xs to List и xs.toList в том, что реализация toList может быть переопределена конкретным типом коллекции xs . Это делает преоб разование ее элементов в список более эффективным по сравнению с реа лизацией по умолчанию, копирующей все элементы коллекции. Например, коллекция ListBuffer переопределяет метод toList с помощью реализации, которая имеет постоянные время выполнения и объем памяти. Но следует иметь в виду, что преобразование в списки или массивы, как правило, требует копирования всех элементов коллекции и потому для больших коллекций может выполняться довольно медленно. Но иногда это все же приходится делать изза уже существующего API. Кроме того, многие коллекции содержат всего несколько элементов, а при этом потери в скорости незначительны. 15 .5 . Кортежи 347 Преобразования между изменяемыми и неизменяемыми множествами и отображениями Иногда возникает еще одна ситуация, требующая преобразования изменя емого множества либо отображения в неизменяемый аналог или наоборот. Выполнить эти задачи следует с помощью метода, показанного чуть ранее. Преобразование неизменяемого множества TreeSet из предыдущего примера в изменяемый и обратно в неизменяемый выполняется так: import scala.collection.mutable treeSet // TreeSet(blue, green, red, yellow) val mutaSet = treeSet to mutable.Set // mutable.HashSet(red, blue, green, yellow) val immutaSet = mutaSet to Set // // Set(red, blue, green, yellow) Ту же технику можно применить для преобразований между изменяемыми и неизменяемыми отображениями: val muta = mutable.Map("i" –> 1, "ii" –> 2) muta // mutable.HashMap(i –> 1, ii –> 2) val immu = muta to Map // Map(ii –> 2, i –> 1) 15 .5 . Кортежи Согласно описанию, которое дано в шаге 9 в главе 3, кортеж объединяет фиксированное количество элементов, позволяя выполнять их передачу в виде единого целого. В отличие от массива или списка кортеж может со держать объекты различных типов. Вот как, к примеру, выглядит кортеж, содержащий целое число, строку и консоль: (1, "hello", Console) Кортежи избавляют вас от скуки, возникающей при определении упрощен ных, насыщенных данными классов. Даже притом что определить класс не составляет особого труда, все же требуется приложить определенное количество порой напрасных усилий. Кортежи избавляют вас от необхо димости выбирать имя класса, область видимости, в которой определяется класс, и имена для членов класса. Если класс просто хранит целое число и строку, то добавление класса по имени AnIntegerAndAString особой яс ности не внесет. Поскольку кортежи могут сочетать объекты различных типов, они не яв ляются наследниками класса Iterable . Если потребуется сгруппировать 348 Глава 15 • Работа с другими коллекциями ровно одно целое число и ровно одну строку, то понадобится кортеж, а не List или Array Довольно часто кортежи применяются для возвращения из метода несколь ких значений. Рассмотрим, к примеру, метод, который выполняет поиск самого длинного слова в коллекции и возвращает наряду с ним его индекс: def longestWord(words: Array[String]): (String, Int) = var word = words(0) var idx = 0 for i <- 1 until words.length do if words(i).length > word.length then word = words(i) idx = i (word, idx) А вот пример использования этого метода: val longest = longestWord("The quick brown fox".split(" ")) // (quick,1) Функция longestWord выполняет здесь два вычисления, получая при этом слово word , являющееся в массиве самым длинным, и его индекс idx . Во из бежание усложнений в функции предполагается, что список имеет хотя бы одно слово, и она отдает предпочтение тому из одинаковых по длине слов, которое стоит в списке первым. Как только функция выберет, какое слово и какой индекс возвращать, она возвращает их вместе, используя синтаксис кортежа (word, idx) Доступ к элементам кортежа можно получить с помощью круглых скобок и индекса, основанного на нуле. Результат будет иметь соответствующий тип. Например: scala> longest(0) val res0: String = quick scala> longest(1) val res1: Int = 1 Кроме того, значение каждого элемента кортежа можно присвоить собствен ной переменной 1 : scala> val (word, idx) = longest val word: String = quick 1 Этот синтаксис является, по сути, особым случаем сопоставления с образцом, по дробно рассмотренным в разделе 13.7. Резюме 349 val idx: Int = 1 scala> word val res55: String = quick Кстати, если не поставить круглые скобки, то будет получен совершенно иной результат: scala> val word, idx = longest val word: (String, Int) = (quick,1) val idx: (String, Int) = (quick,1) Представленный синтаксис дает множественное определение одного и того же выражения. Каждая переменная инициализируется собственным вы числением выражения в правой части. В данном случае неважно, что это выражение вычисляется в кортеж. Обе переменные инициализируются всем кортежем целиком. Ряд примеров, в которых удобно применять множествен ные определения, можно увидеть в главе 16. Следует заметить, что кортежи довольно просты в использовании. Они очень хорошо подходят для объединения данных, не имеющих никакого другого смысла, кроме «А и Б». Но когда объединение имеет какоелибо значение или нужно добавить к объединению некие методы, то лучше пойти дальше и создать класс. Например, не стоит использовать кортеж из трех значений, чтобы объединить месяц, день и год, — нужно создать класс Date . Так вы явно обозначите свои намерения, благодаря чему код станет более понятным для читателей, и это позволит компилятору и средствам самого языка помочь вам отловить возможные ошибки. Резюме В данной главе мы дали обзор библиотеки коллекций Scala и рассмотрели наиболее важные ее классы и трейты. Опираясь на эти знания, вы сможете эффективно работать с коллекциями Scala и будете знать, что именно нуж но искать в Scaladoc, когда возникнет необходимость в дополнительных сведениях. Более подробную информацию о коллекциях Scala можно найти в главах 3 и 24. А в следующей главе мы переключим внимание с библиотеки Scala на сам язык и рассмотрим имеющуюся в Scala поддержку изменяемых объектов. 16 Изменяемые объекты В предыдущих главах в центре внимания были функциональные (неизме няемые) объекты. Дело в том, что идея использования объектов без какого либо изменяемого состояния заслуживала более пристального рассмотре ния. Но в Scala также вполне возможно определять объекты с изменяемым состоя нием. Подобные изменяемые объекты зачастую появляются есте ственным образом, когда нужно смоделировать объекты из реального мира, которые со временем подвергаются изменениям. В этой главе мы раскроем суть изменяемых объектов и рассмотрим синта ксические средства для их выражения, предлагаемые Scala. Кроме того, рас смотрим большой пример моделирования дискретных событий, в котором используются изменяемые объекты, а также описан внутренний предметно ориентированный язык (domainspecific language, DSL), предназначенный для определения моделируемых цифровых электронных схем. 16 .1 . Что делает объект изменяемым Принципиальную разницу между чисто функциональным и изменяемым объектами можно проследить, даже не изучая реализацию объектов. При вызове метода или получении значения поля по указателю в отношении функционального объекта вы всегда будете получать один и тот же результат. Например, если есть следующий список символов: val cs = List('a', 'b', 'c') то применение cs.head всегда будет возвращать 'a' . То же самое произойдет, даже если между местом определения cs и местом, где будет применено об |