Наборы наряду с массивами являются частью важнейшего механизма, позволяющего работать с множеством однотипных значений как с одним.
Набор (Set) — это неупорядоченная коллекция уникальных элементов. В отличие от массивов, у элементов набора нет четкого порядка следования, важен лишь факт наличия некоторого значения в наборе. Определенное значение элемента может существовать в нем лишь единожды, то есть каждое значение в пределах одного набора должно быть уникальным. Возможно, в русскоязычной документации по языку Swift вы встречали другое название наборов — множества.
Исходя из определения набора, ясно, что он позволяет собрать множество уникальных значений в пределах одного.
Представьте, что вы предложили друзьям совместный выезд на природу. Каждый из них должен взять с собой одно-два блюда. Вы получаете сообщение от первого товарища, что он возьмет хлеб и овощи. Второй друг готов привезти тушенку и воду. Все поступившие значения вы помещаете в отдельный набор, чтобы избежать дублирования блюд. В вашем наборе уже 4 элемента: «хлеб», «овощи», «тушенка» и «вода». Третий друг чуть позже сообщает, что готов взять мясо и овощи. Однако при попытке поместить в набор элемент «овощи» возникнет исключительная ситуация, поскольку данное значение уже присутствует в наборе. И правильно, зачем вам нужен второй набор овощей!
Набор создается с помощью литерала набора. В плане синтаксиса он идентичен литералу массива, но при этом не должен содержать дублирующихся значений.
Синтаксис
[значение_1, значение_2, ..., значение_N]
• значение: Hashable — значение очередного элемента набора, должно иметь хешируемый тип данных.
Литерал набора возвращает набор, состоящий из N элементов, значения которых имеют один и тот же тип данных. Литерал указывается в квадратных скобках, а значения отдельных элементов в нем разделяются запятыми. Литерал может содержать произвольное количество уникальных элементов одного типа.
ПРИМЕЧАНИЕ Несмотря на то что элементы набора являются неупорядоченными, Swift все же проводит внутреннюю работу по их сортировке и выстраиванию в последовательность. В ином случае у разработчика не было бы возможности последовательного перебора элементов набора.
Для упорядочивания у каждого значения высчитывается цифровой хеш (число, вычисляемое по специальному алгоритму). Для каждого возможного значения хеш всегда будет уникальным, так как он выступает своеобразным индексом, но при этом недоступным разработчику. Хеш используется только для внутренней сортировки значений.
В связи с этим не каждый тип данных может использоваться для элементов набора. Для того чтобы тип данных имел такую возможность, он должен соответствовать требованиям протокола Hashable. Все фундаментальные типы поддерживают этот протокол.
При создании набора необходимо явно указать, что создается именно набор. Если переменной передать литерал набора, то Swift распознает в нем литерал массива и вместо набора будет создан массив. По этой причине для создания набора необходимо использовать один из следующих способов.
• Явно указать тип данных набора с использованием конструкции Set<Т>, где Т указывает на тип значений элементов создаваемого набора, и передать литерал набора в качестве инициализируемого значения.
Пример
var mySet: Set<Int> = [1,5,0]
• Неявно задать тип данных с помощью конструкции Set и передать литерал набора с элементами в качестве инициализируемого значения.
Пример
var mySet: Set = [1,5,0]
• Использовать функцию Set<Т>(arrayLiteral:), явно указывающую на тип данных элементов массива, где Т указывает на тип значений элементов создаваемого набора, а входной аргумент arrayLiteral содержит список элементов.
Пример
var mySet = Set<Int>(arrayLiteral: 5,66,12)
• Использовать функцию Set(arrayLiteral:), где входной аргумент arrayLiteral содержит список элементов.
Пример
var mySet = Set(arrayLiteral: 5,66,12)
Тип данных набора — Set<Т>, где Т определяет тип данных элементов набора и должен быть хешируемым типом (Hashable).
ПРИМЕЧАНИЕ Для создания неизменяемого набора используйте оператор let, в ином случае — оператор var.
В листинге 10.1 продемонстрированы все доступные способы создания наборов.
Листинг 10.1
var dishes: Set<String> = ["хлеб", "овощи", "тушенка", "вода"]
var dishesTwo: Set = ["хлеб", "овощи", "тушенка", "вода"]
var members = Set<String>(arrayLiteral: "Энакин", "Оби Ван", "Йода")
var membersTwo = Set(arrayLiteral: "Энакин", "Оби Ван", "Йода")
В переменных members, membersTwo, dishes, dishesTwo хранятся наборы уникальных значений. При этом в области вывода порядок значений может не совпадать с определенным в литерале. Это связано с тем, что наборы — неупорядоченные коллекции элементов.
Пустой набор, то есть набор, значение которого не имеет элементов (по аналогии с пустым массивом), создается с помощью пустого литерала набора [] либо вызова функции Set<T>() без входных параметров, где T определяет тип данных элементов массива. Вы также можете передать данный литерал с целью уничтожения всех элементов изменяемого набора (то есть в качестве хранилища используется переменная, а не константа). Пример приведен в листинге 10.2.
Листинг 10.2
// создание пустого набора
var emptySet = Set<String>()
// набор со значениями
var setWithValues: Set<String> = ["хлеб", "овощи"]
// удаление всех элементов набора
setWithValues = []
setWithValues // Set([])
Набор — это неупорядоченная коллекция, элементы которой не имеют индексов. Для взаимодействия с его элементами используются специальные методы.
Так, для создания нового элемента в наборе применяется метод insert(_:), которому передается создаваемое значение. Обратите внимание, что оно должно соответствовать типу набора (листинг 10.3).
Листинг 10.3
// создаем пустой набор
var musicStyleSet: Set<String> = []
// добавляем к нему новый элемент
musicStyleSet.insert("Jazz") // (inserted true, memberAfterInsert
// "Jazz")
musicStyleSet //{"Jazz"}
В результате выполнения метода insert(_:) возвращается кортеж, первый элемент которого содержит значение типа Bool, характеризующее успешность проведенной операции. Если возвращен true — элемент успешно добавлен, если false — он уже существует в наборе.
Для удаления элемента из набора используется метод remove(_:), который уничтожает элемент с указанным значением и возвращает его значение или ключевое слово nil, если такого элемента не существует. Также вы можете задействовать метод removeAll() для удаления всех элементов набора (листинг 10.4).
Листинг 10.4
// создание набора со значениями
musicStyleSet = ["Jazz", "Hip-Hop", "Rock"]
// удаляем один из элементов
var removeStyleResult = musicStyleSet.remove("Hip-Hop")
removeStyleResult //"Hip-Hop"
musicStyleSet //{"Jazz", "Rock"}
// удаляем несуществующий элемент
musicStyleSet.remove("Classic") // nil
// удаляем все элементы набора
musicStyleSet.removeAll()
musicStyleSet // Set([])
ПРИМЕЧАНИЕ В предыдущем примере вы впервые встретились с ключевым словом nil. С его помощью определяется полное отсутствие какого-либо значения. Его подробному изучению будет посвящена глава 14.
У вас мог возникнуть вопрос, почему в случае, если элемент не был удален, возвращается nil, а не false. Дело в том, что false само по себе является значением хешируемого типа Bool. Это значит, что набор вполне может иметь тип Set<Bool>. Если бы данный метод вернул false, то было бы логично утверждать, что из набора был удален элемент с именно таким значением.
В свою очередь, получив nil, можно однозначно сказать, что искомый элемент отсутствует в наборе.
Проверка факта наличия значения в наборе осуществляется методом contains(_:), который возвращает значение типа Bool в зависимости от результата проверки (листинг 10.5).
Листинг 10.5
musicStyleSet = ["Jazz", "Hip-Hop", "Rock", "Funk"]
// проверка существования значения в наборе
musicStyleSet.contains("Funk") // true
musicStyleSet.contains("Pop") // false
Для определения количества элементов в наборе вы можете использовать свойство count, возвращающее целое число (листинг 10.5а).
Листинг 10.5а
musicStyleSet.count //4
Наборы подобны математическим множествам. Два или более набора могут содержать пересекающиеся и непересекающиеся между собой значения. Swift позволяет получать эти группы значений.
В листинге 10.6 создаются три различных целочисленных набора (рис. 10.1). Один из наборов содержит четные числа, второй — нечетные, третий — те и другие.
Листинг 10.6
// набор с четными цифрами
let evenDigits: Set = [0, 2, 4, 6, 8]
// набор с нечетными цифрами
let oddDigits: Set = [1, 3, 5, 7, 9]
// набор со смешанными цифрами
var differentDigits: Set = [3, 4, 7, 8]
Рис. 10.1. Три набора целочисленных значений
В наборах evenDigits, oddDigits и differentDigits существуют как уникальные для каждого из них, так и общие элементы.
Для каждой пары наборов можно произвести следующие операции (рис. 10.2):
• получить все общие элементы (intersection);
• получить все непересекающиеся (не общие) элементы (symmetricDifference);
• получить все элементы обоих наборов (union);
• получить разницу элементов, то есть элементы, которые входят в первый набор, но не входят во второй (subtracting).
При использовании метода intersection(_:) возвращается набор, содержащий значения, общие для двух наборов (листинг 10.7).
Листинг 10.7
var inter = differentDigits.intersection(oddDigits)
inter // {3, 7}
Для получения всех непересекающихся значений служит метод symmetricDifference(_:), представленный в листинге 10.8.
Листинг 10.8
var exclusive = differentDigits.symmetricDifference(oddDigits)
exclusive // {4, 8, 1, 5, 9}
Рис. 10.2. Операции, проводимые с наборами
Для получения всех элементов из обоих наборов (их объединения) применяется объединяющий метод union(_:), как показано в листинге 10.9.
Листинг 10.9
var union = evenDigits.union(oddDigits)
union // {8, 4, 2, 7, 3, 0, 6, 5, 9, 1}
Метод subtracting(_:) возвращает все элементы первого набора, которые не входят во второй набор (листинг 10.10).
Листинг 10.10
var subtract = differentDigits.subtracting(evenDigits)
subtract // {3, 7}
В листинге 10.10a созданы три набора: aSet, bSet и cSet. В них присутствуют как уникальные, так и общие элементы. Их графическое представление показано на рис. 10.3.
Листинг 10.10a
var aSet: Set = [1, 2, 3, 4, 5]
var bSet: Set = [1, 3]
var cSet: Set = [6, 7, 8, 9]
Рис. 10.3. Три набора значений с различными отношениями друг с другом
Набор aSet — это супернабор для набора bSet, так как включает в себя все элементы из bSet. В то же время набор bSet — это субнабор (или поднабор) для aSet, так как все элементы bSet существуют в aSet. Наборы cSet и bSet являются непересекающимися, так как у них нет общих элементов, а наборы aSet и cSet — пересекающиеся, так как имеют общие элементы.
Два набора считаются эквивалентными, если у них один и тот же набор элементов. Эквивалентность наборов проверяется с помощью оператора эквивалентности (==), как показано в листинге 10.11.
Листинг 10.11
// создаем копию набора
var copyOfBSet = bSet
/* в наборах bSet и copyOfBSet одинаковый состав
элементов. Проверим их на эквивалентность */
bSet == copyOfBSet //true
Метод isSubset(of:) определяет, является ли один набор субнабором другого, как bSet для aSet (листинг 10.12).
Листинг 10.12
var aSet: Set = [1, 2, 3, 4, 5]
var bSet: Set = [1, 3]
bSet.isSubset(of: aSet) //true
Метод isSuperset(of:) вычисляет, является ли набор супернабором для другого набора (листинг 10.13).
Листинг 10.13
var aSet: Set = [1, 2, 3, 4, 5]
var bSet: Set = [1, 3]
aSet.isSuperset(of: bSet) // true
Метод isDisjoint(with:) определяет, существуют ли в двух наборах общие элементы, и в случае их отсутствия возвращает true (листинг 10.14).
Листинг 10.14
bSet.isDisjoint(with: cSet) // true
Методы isStrictSubset(of:) и isStrictSuperset(of:) определяют, является набор субнабором или супернабором, не равным указанному множеству (листинг 10.15).
Листинг 10.15
bSet.isStrictSubset(of: aSet) // true
aSet.isStrictSuperset(of: bSet) // true var aSet: Set = [1, 2, 3, 4, 5]
С помощью уже знакомого метода sorted() вы можете отсортировать набор. При этом будет возвращен массив, в котором все элементы расположены по возрастанию (листинг 10.16).
Листинг 10.16
var setOfNums: Set = [1,10,2,5,12,23]
var sortedArray = setOfNums.sorted()
sortedArray // [1, 2, 5, 10, 12, 23]
type(of: sortedArray) // Array<Int>.Type