Казалось бы, элементарный тип 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 пиццах при помощи unioun:

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.

Комментарии (0)