Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
Скачать 6.24 Mb.
|
332 Глава 15 • Работа с другими коллекциями Краткий пример, показывающий способ инициализации списка и получения доступа к его голове и хвосту, выглядит так: val colors = List("red", "blue", "green") colors.head // red colors.tail // List(blue, green) Чтобы освежить в памяти сведения о списках, обратитесь к шагу 8 в главе 3. А подробности использования списков можно найти в главе 14. Массивы Массивы позволяют хранить последовательность элементов и оперативно обращаться к элементу, находящемуся в произвольной позиции, чтобы либо получить его, либо обновить; для этого используется индекс, отсчитываемый от нуля. Массив известной длины, для которого пока неизвестны значения элементов, создается следующим образом: val fiveInts = new Array[Int](5) // Array(0, 0, 0, 0, 0) А вот как инициализируется массив, когда значения элементов известны: val fiveToOne = Array(5, 4, 3, 2, 1) // Array(5, 4, 3, 2, 1) Как уже упоминалось, получить доступ к элементам массивов в Scala можно, указав индекс в круглых, а не в квадратных, как в Java, скобках. Рассмотрим пример доступа к элементу массива и обновления элемента: fiveInts(0) = fiveToOne(4) fiveInts // Array(1, 0, 0, 0, 0) Массивы в Scala представлены точно так же, как массивы в Java. Поэтому можно абсолютно свободно использовать имеющиеся в Java методы, воз вращающие массивы 1 В предыдущих главах действия с массивами встречались уже много раз. Основы этих действий были рассмотрены в шаге 7 главы 3. Ряд примеров поэлементного обхода массивов с помощью выражения for был показан в разделе 7.3. 1 Разница вариантности массивов в Scala и в Java — то есть является ли Array[String] подтипом Array[AnyRef] — будет рассмотрена в разделе 18.3. 15 .1 . Последовательности 333 Буферы списков Класс List предоставляет быстрый доступ к голове и хвосту списка, но не к его концу. Таким образом, при необходимости построить список с добав лением элементов в конец следует рассматривать возможность построить список в обратном порядке путем добавления элементов спереди. Затем, когда это будет сделано, нужно вызвать метод реверсирования reverse , чтобы получить элементы в требуемом порядке. Другой вариант, который позволяет избежать реверсирования, — исполь зовать объект ListBuffer . Это содержащийся в пакете scala.collecti- on.mutable изменяемый объект, который может помочь более эффективно строить списки, когда нужно добавлять элементы в их конец. Объект обе спечивает постоянное время выполнения операций добавления элементов как в конец, так и в начало списка. В конец списка элемент добавляется с помощью оператора += 1 , а в начало — с помощью оператора +=: . Когда по строение будет завершено, можно получить список типа List , вызвав в от ношении ListBuffer метод toList . Соответствующий пример выглядит так: import scala.collection.mutable.ListBuffer val buf = new ListBuffer[Int] buf += 1 // ListBuffer(1) buf += 2 // ListBuffer(1, 2) 3 +=: buf // ListBuffer(3, 1, 2) buf.toList // List(3, 1, 2) Еще один повод использовать ListBuffer вместо List — возможность предот вратить потенциальное переполнение стека. Если можно создать список в нужном порядке, добавив элементы в его начало, но рекурсивный алгоритм, который потребуется, не является алгоритмом с хвостовой рекурсией, то вме сто этого можно задействовать выражение for или цикл while и ListBuffer Буферы массивов Объект ArrayBuffer похож на массив, за исключением того, что в дополнение ко всему здесь предоставляет возможность добавлять и удалять элементы в начало и в конец последовательности. Доступны все те же операции, что и в классе Array , хотя выполняются они несколько медленнее, посколь ку в реализации есть уровеньоболочка. На новые операции добавления 1 Операторы += и +=: являются псевдонимами для append и prepend соответственно. 334 Глава 15 • Работа с другими коллекциями и удаления затрачивается в среднем одно и то же время, но иногда требуется время, пропорциональное размеру, изза реализации, требующей выделить новый массив для хранения содержимого буфера. Чтобы воспользоваться ArrayBuffer , нужно сначала импортировать его из пакета изменяемых коллекций: import scala.collection.mutable.ArrayBuffer При создании ArrayBuffer нужно указать параметр типа, а длину указывать не обязательно. По мере надобности ArrayBuffer автоматически установит выделяемое пространство памяти: val buf = new ArrayBuffer[Int]() Добавить элемент в ArrayBuffer можно с помощью метода += : buf += 12 // ArrayBuffer(12) buf += 15 // ArrayBuffer(12, 15) Доступны все обычные методы работы с массивами. Например, можно за просить у ArrayBuffer его длину или извлечь элемент по его индексу: buf.length // 2 buf(0) // 12 Строки (реализуемые через StringOps) Еще одной последовательностью, заслуживающей упоминания, является StringOps . В ней реализованы многие методы работы с последовательностя ми. Поскольку в Predef есть неявное преобразование из String в StringOps , то с любой строкой можно работать как с последовательностью. Вот пример: def hasUpperCase(s: String) = s.exists(_.isUpper) hasUpperCase("Robert Frost") // true hasUpperCase("e e cummings") // false В этом примере метод exists вызывается в отношении строки, которая в теле метода hasUpperCase называется s . В самом классе String не объявлено ника кого метода по имени exists , поэтому компилятор Scala выполнит неявное преобразование s в StringOps , где такой метод есть. Метод exists считает строку последовательностью символов и вернет значение true , если какой либо из них относится к верхнему регистру 1 1 Подобный пример представлен на с. 49. 15 .2 . Множества и отображения 335 15 .2 . Множества и отображения В предыдущих главах, начиная с шага 10 в главе 3, уже были показаны ос новы множеств и отображений. Прочитав этот раздел, вы получите более глубокое представление о способах их использования и увидите несколько дополнительных примеров. Ранее мы уже говорили, что библиотека коллекций Scala предлагает как изменяемые, так и неизменяемые версии множеств и отображений. Иерар хия множеств показана на рис. 3.2 (см. с. 80), а иерархия отображений — на рис. 3.3 (см. с. 82). Из этих схем следует, что простые имена Set и Map исполь зуются тремя трейтами и все они находятся в разных пакетах. По умолчанию, когда в коде используется Set или Map , вы получаете не изменяемый объект. Если нужен изменяемый вариант, то следует приме нить явно указанное импортирование. К неизменяемым вариантам Scala предоставляет самый простой доступ — в качестве небольшого поощрения за то, что предпочтение отдано им, а не их изменяемым аналогам. Доступ предоставляется через объект Predef , неявно импортируемый в каждый файл исходного кода на языке Scala. Соответствующие определения по казаны в листинге 15.1. Листинг 15.1. Исходные определения отображений map и множеств в set в Predef object Predef: type Map[A, +B] = collection.immutable.Map[A, B] type Set[A] = collection.immutable.Set[A] val Map = collection.immutable.Map val Set = collection.immutable.Set // ... end Predef Имена Set и Map в качестве псевдонимов для более длинных полных имен трейтов неизменяемых множеств и отображений в Predef определяются с помощью ключевого слова type 1 . Чтобы ссылаться на объектыодиночки для неизменяемых Set и Map , выполняется инициализация val переменных с именами Set и Map . Следовательно, Map является тем же, что и объект Predef.Map , который определен быть тем же самым, что и scala.collecti- on.im mutable.Map . Это справедливо как для типа Map , так и для объекта Map Если нужно воспользоваться как изменяемыми, так и неизменяемыми множествами или отображениями в одном и том же исходном файле, то 1 Более подробно ключевое слово type мы рассмотрим в разделе 20.6. 336 Глава 15 • Работа с другими коллекциями рекомендуемым подходом является импортирование имен пакетов, содер жащих изменяемые варианты: import scala.collection.mutable Можно продолжать ссылаться на неизменяемое множество, как и прежде Set , но теперь можно будет сослаться и на изменяемое множество, указав mutable.Set . Вот как выглядит соответствующий пример: val mutaSet = mutable.Set(1, 2, 3) Использование множеств Ключевой характеристикой множества является то, что оно гарантирует наличие каждого объекта не более чем в одном экземпляре, по определению оператора == . В качестве примера воспользуемся множеством, чтобы вы числить количество уникальных слов в строке. Если указать в качестве разделителей слов пробелы и знаки пунктуации, то метод split класса String может разбить строку на слова. Для этого вполне достаточно применить регулярное выражение [ !,.]+ : оно показывает, что строка должна быть разбита во всех местах, где есть один или несколько пробелов и/или знак пунктуации: val text = "See Spot run. Run, Spot. Run!" val wordsArray = text.split("[ !,.]+") // Array(See, Spot, run, Run, Spot, Run) Чтобы посчитать уникальные слова, их можно преобразовать, приведя их символы к единому регистру, а затем добавить в множество. Поскольку мно жества исключают дубликаты, то каждое уникальное слово будет появляться в множестве только раз. Сначала можно создать пустое множество, используя метод empty , предо ставляемый объектомкомпаньоном Set : val words = mutable.Set.empty[String] Далее, просто перебирая слова с помощью выражения for , можно преоб разовать каждое слово, приведя его символы к нижнему регистру, а затем добавить его в изменяемое множество, воспользовавшись оператором += : for word <- wordsArray do words += word.toLowerCase words // Set(see, run, spot) 15 .2 . Множества и отображения 337 Таким образом, в тексте содержится три уникальных слова: spot , run и see Наиболее часто используемые методы, применяемые равно к изменяемым и неизменяемым множествам, показаны в табл. 15.1. Таблица 15.1. Наиболее распространенные операторы для работы с множествами Что используется Что этот метод делает val nums = Set(1, 2, 3) Создает неизменяемое множество ( nums.toString возвращает Set(1, 2, 3) ) nums + 5 Добавляет элемент в неизменяемое множе ство (возвращает Set(1, 2, 3, 5) ) nums — 3 Удаляет элемент из неизменяемого множе ства (возвращает Set(1, 2) ) nums ++ List(5, 6) Добавляет несколько элементов (возвращает Set(1, 2, 3, 5, 6) ) nums –– List(1, 2) Удаляет несколько элементов из неизменяе мого множества (возвращает Set(3) ) nums & Set(1, 3, 5, 7) Выполняет пересечение двух множеств (возвращает Set(1, 3) ) nums.size Возвращает размер множества (возвраща ет 3 ) nums.contains(3) Проверка включения (возвращает true ) import scala.collection.mutable Упрощает доступ к изменяемым коллекциям val words = mutable.Set. empty[String] Создает пустое изменяемое множество ( words.toString возвращает Set() ) words += "the" Добавляет элемент ( words.toString возвра щает Set(the) ) words –= "the" Удаляет элемент, если он существует ( words.toString возвращает Set() ) words ++= List("do", "re", "mi") Добавляет несколько элементов ( words.toString возвращает Set(do, re, mi) ) words ––= List("do", "re") Удаляет несколько элементов ( words.toString возвращает Set(mi) ) words.clear Удаляет все элементы ( words.toString возвращает Set() ) 338 Глава 15 • Работа с другими коллекциями Применение отображений Отображения позволяют связать значение с каждым элементом мно жества. Отображение и массив используются похожим образом, за ис ключением того, что вместо индексирования с помощью целых чисел, начинающихся с нуля, можно применить ключи любого вида. Если им портировать пакет с именем mutable , то можно создать пустое изменяемое отображение: val map = mutable.Map.empty[String, Int] Учтите, что при создании отображения следует указать два типа. Первый тип предназначен для ключей отображения, а второй — для их значений. В данном случае ключами являются строки, а значениями — целые числа. Задание за писей в отображении похоже на задание записей в массиве: map("hello") = 1 map("there") = 2 map // Map(hello > 1, there > 2) По аналогии с этим чтение отображения похоже на чтение массива: map("hello") // 1 Чтобы связать все воедино, рассмотрим метод, подсчитывающий количество появлений каждого из слов в строке: def countWords(text: String) = val counts = mutable.Map.empty[String, Int] for rawWord <- text.split("[ ,!.]+") do val word = rawWord.toLowerCase val oldCount = if counts.contains(word) then counts(word) else 0 counts += (word –> (oldCount + 1)) counts countWords("See Spot run! Run, Spot. Run!") // Map(spot –> 2, see –> 1, run –> 3) Этот код работает благодаря тому, что используется изменяемое отображе ние по имени counts и каждое слово отображается на количество его появ лений в тексте. Для каждого слова в тексте выполняется поиск предыдущего количества появлений слова и его увеличение на единицу, а затем в counts сохраняется новое значение количества. Обратите внимание: проверка того, встречалось ли это слово раньше, выполняется с помощью метода contains 15 .2 . Множества и отображения 339 Если counts.contains(word) не возвращает true , значит, слово еще не встре чалось и за количество принимается ноль. Многие из наиболее часто используемых методов работы как с изменяемы ми, так и с неизменяемыми отображениями показаны в табл. 15.2. Таблица 15.2. Наиболее часто используемые операции для работы с отображениями Что используется Что этот метод делает val nums = Map("i" –> 1, "ii" –> 2) Создает неизменяемое отображение ( nums. toString возвращает Map(i –> 1, ii –> 2) ) nums + ("vi" –> 6) Добавляет запись в неизменяемое отображение (возвращает Map(i –> 1, ii –> 2, vi –> 6) ) nums — "ii" Удаляет запись из неизменяемого отображения (возвращает Map(i –> 1) ) nums ++ List("iii" –> 3, "v" –> 5) Добавляет несколько записей (возвращает Map(i –> 1, ii –> 2, iii –> 3, v –> 5) ) nums –– List("i", "ii") Удаляет несколько записей из неизменяемого отображения (возвращает Map() ) nums.size Возвращает размер отображения (возвращает 2 ) nums.contains("ii") Проверяет на включение (возвращает true ) nums("ii") Извлекает значение по указанному ключу (воз вращает 2 ) nums.keys Возвращает ключи (возвращает результат итера ции, выполненной над строками "i" и "ii" ) nums.keySet Возвращает ключи в виде множества (возвраща ет Set(i, ii) ) nums.values Возвращает значения (возвращает Iterable над целыми числами 1 и 2 ) nums.isEmpty Показывает, является ли отображение пустым (возвращает false ) import scala.collection. mutable Упрощает доступ к изменяемым коллекциям val words = mutable.Map. empty[String, Int] Создает пустое изменяемое отображение words += ("one" –> 1) Добавляет запись в отображение из ключа "one" и значения 1 ( words.toString возвращает Map(one –> 1) ) words –= "one" Удаляет запись из отображения, если она суще ствует ( words.toString возвращает Map() ) 340 Глава 15 • Работа с другими коллекциями Что используется Что этот метод делает words ++= List("one" –> 1, "two" –> 2, "three" –> 3) Добавляет записи в изменяемое отображение ( words.toString возвращает Map(one –> 1, two –> 2, three –> 3) ) words ––= List("one", "two") Удаляет несколько объектов ( words.toString воз вращает Map(three –> 3) ) Множества и отображения, используемые по умолчанию Для большинства случаев реализаций изменяемых и неизменяемых мно жеств и отображений, предоставляемых Set() , scala.collection.mutab- le.Map() и тому подобными фабриками, наверное, вполне достаточно. Реализации, предоставляемые этими фабриками, используют алгоритм ускоренного поиска, в котором обычно задействуется хештаблица, поэтому они могут быстро обнаружить наличие или отсутствие объекта в коллекции. Так, фабричный метод scala.collection.mutable.Set() возвращает sca- la.col lection.mutable.HashSet , внутри которого используется хештаблица. Аналогично этому фабричный метод scala.collection.mutable.Map() воз вращает scala.collection.mutable.HashMap История с неизменяемыми множествами и отображениями несколько сложнее. Как показано в табл. 15.3, класс, возвращаемый фабричным методом scala.collection.immutable.Set() , зависит, к примеру, от того, сколько элементов ему было передано. В целях достижения максималь ной производительности для множеств, состоящих не более чем из пяти элементов, применяется специальный класс. Но при запросе множества из пяти и более элементов фабричный метод вернет реализацию, исполь зующую хеш. По аналогии с этим, как следует из данной таблицы, в результате выполне ния фабричного метода scala.collection.immutable.Map() будет возвращен нужный класс в зависимости от того, сколько пар «ключ — значение» ему передано. Как и в случае с множествами, для того чтобы неизменяемые ото бражения с количеством элементов меньше пяти достигли максимальной производительности для отображения каждого конкретного размера, ис пользуется специальный класс. Но если отображение содержит пять и более пар «ключ — значение», то используется неизменяемый класс HashMap |