Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 763
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
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
1 ... 32 33 34 35 36 37 38 39 ... 64
Таблица 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.