OptionSetType и перечисления

У меня есть перечисление с именем ProgrammingLanguage:

enum ProgrammingLanguage {
  case Swift, Haskell, Scala
}

Теперь у меня есть класс с именем Programmer со следующим свойством:

let favouriteLanguages: ProgrammingLanguage = .Swift

Увидев, как программист может иметь несколько любимых языков, я подумал, что было бы неплохо написать что-то вроде этого:

let favouriteLanguages: ProgrammingLanguage = [.Swift, .Haskell]

После небольшого исследования я понял, что мне нужно соответствовать OptionSetType, но при этом я вызываю следующие 3 ошибки:

ProgrammingLanguage не соответствует

  • SetAlgebraType
  • OptionSetType
  • RawRepresentable

Когда я увидел ошибку Необработанное представление, я сразу же подумал о связанных типах для перечислений. Я хотел иметь возможность печатать значение перечисления в любом случае, поэтому я изменил свою подпись перечисления на следующее:

case ProgrammingLanguage: String, OptionSetType {
  case Swift, Haskell, Scala
}

Это застало 2 предупреждения. Но я все еще остаюсь с тем, что я не согласен с протоколом SetAlgebraType.

После нескольких проб и ошибок я обнаружил, что связанный тип перечисления как Int исправил его (что имеет смысл, так как протокол RawRepresentable требует, чтобы вы реализовали инициализатор подписи init(rawValue: Int)). Однако я не удовлетворен этим; Я хочу, чтобы можно было легко получить строковое представление перечисления.

Может ли кто-нибудь сообщить мне, как я могу сделать это легко, и почему OptionSetType требует связанный тип Int?

Edit:

Следующее объявление компилируется правильно, но ошибки во время выполнения:

enum ProgrammingLanguage: Int, OptionSetType {
  case Swift, Scala, Haskell
}

extension ProgrammingLanguage {
  init(rawValue: Int) {
    self.init(rawValue: rawValue)
  }
}

let programmingLanguages: ProgrammingLanguage = [.Swift, .Scala]

Ответ 1

Изменение: Я удивлен тем, что раньше я не говорил об этом заранее, но... вместо того, чтобы пытаться принудительно использовать другие типы значений в протоколе OptionSet (Swift 3 удалил Type из названия), он вероятно, лучше рассмотреть API, в котором вы используете эти типы, и, где это уместно, использовать коллекции Set.

OptionSet типы странные. Они оба являются коллекциями, а не коллекциями - вы можете создать один из нескольких флагов, но в результате вы получите одно значение. (Вы можете проделать некоторую работу, чтобы выяснить коллекцию одиночных флагов, эквивалентную такому значению, но в зависимости от возможных значений в типе оно может быть не уникальным.)

С другой стороны, возможность иметь что-то одно или несколько уникальных предметов может иметь важное значение для разработки API. Вы хотите, чтобы пользователи сказали, что у них более одного фаворита, или хотите, чтобы их было только один? Сколько "избранных" вы хотите разрешить? Если пользователь запрашивает несколько избранных, должны ли они быть ранжированы в определенном для него порядке? Это все вопросы, на которые трудно ответить с типом OptionSet -style, но гораздо проще, если вы используете тип Set или другую фактическую коллекцию.

Остальная часть этого ответа а) старая, с использованием имен Swift 2, и б) предполагает, что вы все равно пытаетесь реализовать OptionSet, даже если это плохой выбор для вашего API...


Смотрите документы для OptionSetType:

Обеспечивает удобное соответствие SetAlgebraType для любого типа, чей RawValue является BitwiseOperationsType.

Другими словами, вы можете объявить соответствие OptionSetType для любого типа, который также принимает RawRepresentable. Однако вы получаете поддержку синтаксиса магической алгебры множеств (через операторы и соответствие ArrayLiteralConvertible) тогда и только тогда, когда ваш связанный тип необработанных значений соответствует типу BitwiseOperationsType.

Таким образом, если ваш тип необработанного значения String, вам не повезло - вы не получаете набор алгебры, потому что String не поддерживает побитовые операции. (Самое интересное здесь, если вы можете так его назвать, это то, что вы можете расширить String для поддержки BitwiseOperationsType, и если ваша реализация удовлетворяет аксиомам axioms, вы можете использовать строки как необработанные значения для набора параметров.)

Ваши вторые синтаксические ошибки во время выполнения, потому что вы создали бесконечную рекурсию - вызов self.init(rawValue:) из init(rawValue:) удерживает гонг, пока он не унесет стек.

Возможно, это ошибка (, пожалуйста, подайте ее), которую вы можете попробовать даже без ошибки времени компиляции. Перечисления не должны быть в состоянии объявить соответствие OptionSetType, потому что:

  1. Семантический контракт перечисления состоит в том, что это закрытое множество. Объявляя ваше перечисление ProgrammingLanguage, вы говорите, что значение типа ProgrammingLanguage должно быть одним из Swift, Scala или Haskell, а не каким-либо другим. Значение "Swift и Scala" не входит в этот набор.

  2. Базовая реализация OptionSetType основана на целочисленных битовых полях. Значение "Swift и Haskell" ([.Swift, .Haskell]) действительно просто .Swift.rawValue | .Haskell.rawValue. Это вызывает проблемы, если ваш набор необработанных значений не выровнен по битам. То есть, если .Swift.rawValue == 1 == 0b01 и .Haskell.rawValue == 2 == 0b10, побитовый или из них равен 0b11 == 3, что совпадает с .Scala.rawValue.

TLDR: если вы хотите соответствия OptionSetType, объявите структуру.

И используйте static let для объявления членов вашего типа.

И выберите ваши исходные значения так, чтобы элементы, которые вы хотите отличить от возможных (побитовых или) комбинаций других элементов, фактически являются.

struct ProgrammingLanguage: OptionSetType {
    let rawValue: Int

    // this initializer is required, but it also automatically
    // synthesized if 'rawValue' is the only member, so writing it
    // here is optional:
    init(rawValue: Int) { self.rawValue = rawValue }

    static let Swift    = ProgrammingLanguage(rawValue: 0b001)
    static let Haskell  = ProgrammingLanguage(rawValue: 0b010)
    static let Scala    = ProgrammingLanguage(rawValue: 0b100)
}

Хорошие способы сохранить ваши значения различными: используйте двоично-литеральный синтаксис, как указано выше, или объявите значения с битовыми сдвигами, равными единице, как показано ниже:

    static let Swift    = ProgrammingLanguage(rawValue: 1 << 0)
    static let Haskell  = ProgrammingLanguage(rawValue: 1 << 1)
    static let Scala    = ProgrammingLanguage(rawValue: 1 << 2)

Ответ 2

Я думаю, вы могли бы просто достичь этого современным способом {^ _ ^}.

protocol Option: RawRepresentable, Hashable, CaseIterable {}

extension Set where Element: Option {
    var rawValue: Int {
        var rawValue = 0
        for (index, element) in Element.allCases.enumerated() where contains(element) {
            rawValue |= (1 << index)
        }
        return rawValue
    }
}

... тогда

enum ProgrammingLanguage: String, Option {
    case Swift, Haskell, Scala
}
typealias ProgrammingLanguages = Set<ProgrammingLanguage>

let programmingLanguages: ProgrammingLanguages = [.Swift, .Haskell]

Ссылка: https://nshipster.com/optionset/