Зачем нужна ещё одна коллекция
Представьте список гостей на вечеринке. Порядок, в котором они пришли, никого не волнует, а вот повторять одного и того же человека в списке дважды бессмысленно — он либо в списке, либо нет. Для массива это неудобная задача: чтобы проверить, есть ли уже такой гость, придётся пройти по всем именам one by one, а чтобы не допустить повтора — проверять перед каждым добавлением. Чем длиннее список, тем дольше проверка.
Для таких случаев в Swift есть отдельная коллекция — набор уникальных значений без определённого порядка. Официально она называется Set. Внутри неё элементы хранятся не подряд, а по вычисленному «отпечатку» значения, поэтому проверка «есть ли элемент» происходит практически мгновенно, а не перебором. Обратная сторона — вы никогда не знаете заранее, в каком порядке Set отдаст элементы при обходе.
Объявление и создание
Set — универсальный (generic) тип, поэтому при объявлении переменной указывают, элементы какого типа он хранит: Set<String>, Set<Int> и так далее.
var fruits: Set<String> = ["яблоко", "банан", "груша"]
print(fruits.count)
Здесь важна одна деталь, которая ловит почти всех новичков. Литерал в квадратных скобках сам по себе не говорит компилятору, что вы хотите множество, — так же выглядит и литерал массива. Без явной аннотации типа Set<String> компилятор выберет тип по умолчанию для такого литерала, а это Array:
let numbers = [1, 2, 2, 3] // это Array<Int>, а не множество
print(numbers.count) // 4, повтор никуда не делся
Убрать повторы из уже существующего массива можно, передав его в инициализатор Set:
let numbersArray = [1, 2, 2, 3, 3, 3]
let numbersSet = Set(numbersArray)
print(numbersSet.count) // 3 — повторы схлопнулись
Здесь аннотация типа не нужна: тип виден из вызова Set(...) явно, ловушка возникает только у литерала без контекста.
Требование Hashable
Элементом множества может быть не любой тип, а только тот, что умеет вычислять свой «отпечаток» — хеш. В официальной терминологии такой тип соответствует протоколу Hashable. Не пугайтесь термина: все встроенные типы, с которыми вы уже работали — Int, String, Double, Bool, а также кортежи и массивы из таких типов, — уже умеют это делать «из коробки». Вам не нужно ничего настраивать, чтобы положить строки или числа в Set. Написать свой тип, который тоже это умеет, вы сможете позже, когда доберётесь до структур и классов.
Добавление и удаление
insert добавляет элемент. Если такого значения ещё не было, оно попадёт в множество; если было — ничего не изменится, ведь повторы не допускаются. Метод возвращает не просто факт успеха, а пару значений: было ли значение действительно вставлено, и какое значение сейчас лежит в множестве под этим «именем».
let result = fruits.insert("апельсин")
print(result.inserted) // true — добавили новый фрукт
let result2 = fruits.insert("банан")
print(result2.inserted) // false — банан уже был, множество не изменилось
remove удаляет значение, если оно есть; contains проверяет присутствие; count и isEmpty работают так же, как у массива.
fruits.remove("груша")
print(fruits.contains("груша")) // false
print(fruits.isEmpty) // false, другие фрукты остались
Операции над множествами
Настоящая сила Set — в операциях, которые в математике называются операциями над множествами. Они принимают на вход другое множество и возвращают новое.
| Операция | Что делает |
|---|---|
union | объединение — все элементы из обоих множеств |
intersection | пересечение — только то, что есть в обоих сразу |
subtracting | разность — элементы первого, которых нет во втором |
symmetricDifference | элементы, которые есть только в одном из двух множеств, но не в обоих |
let a: Set<Int> = [1, 2, 3, 4]
let b: Set<Int> = [3, 4, 5, 6]
print(a.union(b).sorted()) // [1, 2, 3, 4, 5, 6]
print(a.intersection(b).sorted()) // [3, 4]
print(a.subtracting(b).sorted()) // [1, 2]
print(b.subtracting(a).sorted()) // [5, 6] — порядок аргументов важен
print(a.symmetricDifference(b).sorted()) // [1, 2, 5, 6]
Обратите внимание на subtracting: a.subtracting(b) и b.subtracting(a) — это разные результаты, операция не симметрична, в отличие от union и intersection.
Есть ещё три метода, которые не создают новое множество, а отвечают на вопрос да/нет о взаимном расположении двух множеств:
isSubset(of:)— все элементы этого множества входят в другоеisSuperset(of:)— это множество содержит все элементы другогоisDisjoint(with:)— общих элементов нет вообще
print(a.isSubset(of: [1, 2, 3, 4, 5])) // true
print(a.isSuperset(of: [1, 2])) // true
print(a.isDisjoint(with: [10, 20])) // true
Массив или множество
Выбор зависит от того, что вам важнее. Если нужен порядок элементов, повторы допустимы и коллекция обычно короткая — берите Array. Если важна уникальность и частая проверка «есть ли такой элемент» на большой коллекции — Set справится быстрее массива, потому что не перебирает элементы один за другим, а сразу вычисляет, где искать.
Порядок обхода
Set не хранит элементы в порядке добавления и не гарантирует вообще никакого стабильного порядка — при следующем запуске программы порядок обхода того же множества может оказаться другим. Если для вывода на экран или для проверки в тестах вам нужен предсказуемый порядок, превратите множество в отсортированный массив методом sorted() — он вернёт Array с элементами по возрастанию. Подробно про сортировку своих правил поговорим отдельно позже в этом модуле.
Частые ошибки
- Написать литерал
let set = [1, 2, 3]и ожидать множество — без аннотации типа это массив, повторы никуда не денутся. - Сравнивать или выводить множество в тестах напрямую, забыв, что порядок обхода не гарантирован — код может проходить проверку локально и падать на сервере просто из-за другого порядка. Спасает
sorted(). - Пытаться положить в множество тип, который не поддерживает
Hashable— компилятор откажется компилировать код, а не тихо всё сломает. - Путать
subtractingместами:a.subtracting(b)иb.subtracting(a)— разные множества.
Резюме
Setхранит только уникальные значения без гарантированного порядка.- Литералу обязательно нужна аннотация типа
Set<T>, иначе получитсяArray. - Из массива множество делают через
Set(массив)— повторы схлопываются. insertвозвращает пару: вставлено ли значение и что сейчас лежит в множестве; есть такжеremove,contains,count,isEmpty.- Операции
union,intersection,subtracting,symmetricDifferenceстроят новые множества;isSubset(of:),isSuperset(of:),isDisjoint(with:)отвечают да/нет. - Элементы множества обязаны соответствовать
Hashable— у встроенных типов это уже есть. - Для предсказуемого порядка при выводе или в тестах превращайте множество в массив через
sorted().
Проверьте себя
3 вопроса
Сколько разных значений
Напишите функцию uniqueCount, которая принимает массив целых чисел и возвращает количество уникальных значений в нём — то есть сколько разных чисел встречается, если не считать повторы.
Например, для [1, 2, 2, 3] ответ — 3, а для пустого массива — 0.
Общие теги
У двух постов есть списки тегов в виде массивов строк. Напишите функцию commonTags, которая принимает два массива тегов и возвращает массив тегов, встречающихся в обоих списках, без повторов и отсортированный по алфавиту.
Например, для ["swift", "ios", "backend"] и ["ios", "web", "swift"] результат — ["ios", "swift"].
Порядок тегов на входе может быть любым, а внутри одного списка теги могут повторяться — это не должно влиять на результат.