Казалось бы, элементарный тип OptionSet в Swift кроет в себе много интересного. Чтобы разобраться в деталях, давайте вспомним примеры использования OptionSet в Apple SDK например при вызове метода animateWithDuration:
у UIView есть аргумент типа AnimationOptions
который предзаполнен пустым массивом.
func animate(
withDuration duration: TimeInterval,
delay: TimeInterval,
options: UIView.AnimationOptions = [],
animations: @escaping () -> Void,
completion: ((Bool) -> Void)? = nil
)
UIView.animate(
withDuration: 0,
delay: 0,
options: [.allowUserInteraction, .autoreverse],
animations: {}
)
Почему же набор опций представлен массивом а не сетом (Set) как может следовать из названия? Давайте разберемся в этом!
OptionSet
в Swift невероятно легковесный тип данных, его проще всего визуализировать через последовательность Bool переменных. Давайте представим что нам нужно создать OptionSet
опций добавок к пицце:
Двойная порция сыра
Сырный бортик
Белый соус
Соус барбекю
Наиболее эффективно в памяти можно все комбинации разместить в 4 битах: _ _ _ _
каждый из которых будет принимать значение 1 в случае выбранной дополнительной опции, а 0 без опции. Набор бит 0 0 0 0
будет соответствовать пицце без добавок а 1 1 1 1
пицце со всеми добавками.
Таким образом, зная двоичную систему счисления и степени двойки мы можем ассоциировать добавки со следующими числами
Двойная порция сыра // 0001 == 1
Сырный бортик. // 0010 == 2
Белый соус // 0100 == 4
Соус барбекю // 1000 == 8
В Swift это будет выглядеть следующим образом:
struct PizzaOptions: OptionSet {
static let extraCheese = PizzaOptions(rawValue: 1) // 0001
static let cheeseSide = PizzaOptions(rawValue: 2) // 0010
static let whiteSause = PizzaOptions(rawValue: 4) // 0100
static let bbqSause = PizzaOptions(rawValue: 8) // 1000
let rawValue: Int8
}
Но так как вспоминать следующую степень двойки не удобно, особенно после 12-й степени, можно использовать всегда 1 и “сдвигать” биты оператором <<
.
struct PizzaOptions: OptionSet {
static let extraCheese = PizzaOptions(rawValue: 1) // 0001
static let cheeseSide = PizzaOptions(rawValue: 1 << 1) // 0010
static let whiteSause = PizzaOptions(rawValue: 1 << 2) // 0100
static let bbqSause = PizzaOptions(rawValue: 1 << 3) // 1000
let rawValue: Int8
}
Можно проверить соответствие битового представлению десятичному следующим образом:
let one = 0b00000001
print(one == 1) // true
print(one << 1 == 2) // true
print(one << 2 == 4) // true
print(one << 2 == 0b00000100) // true
Но пока мы не ответили почему тип данных называется OptionSet, а дело кроется в магии алгебры множеств. Что это значит с практической точки зрения? Мы можем понять какие добавки есть в 2 пиццах при помощи union
:
let first: PizzaOptions = .whiteSause
let second: PizzaOptions = [.bbqSause, .extraCheese]
print(first) // 4
print(second) // 9
print(first.union(second)) // 13
print(first.intersection(second)) // 0
Если пока понятнее не стало, это нормально. Мы обсуждали уже откуда 4, но откуда взялось 9 и тем более 13?
Суть в битовом представлении:
PizzaOptions.whiteSouse
это 4 или в нашем наборе добавок заполнена одна ячейка 0 1 0 0
в битовом представлении.
Во второй пицце заказали .bbqSause
и .extraCheese
или первый и последний ингридиенты. Заполняем: 1 0 0 1
, если перевести в десятичную систему счисления, это 9.
Дайте теперь объединим первый 0 1 0 0
и второй 1 0 0 1
набор ингредиентов, получается 1 1 0 1
и это как раз 13!
Вывод
Подводя итоги, хочется сказать что OptionSet
один из самых редко используемых типов данных и надеюсь после того как мы в нем разобрались, вы понимаете все его преимущества. Он отлично подходит для сжатия полей в Json а так же для архитектуры ваших API методов и библиотек, т.к. может значительно упростить работу с множествами. Методы union
, intersection
, symmetricDifference
, contains
, insert
, remove
, все доступны при работе с OptionSet
так же как при работе с обычными Set
.