Приравниваемые, сопоставимые, идентифицируемые и хэшируемые решения

Протоколы, безусловно, не новы для iOS или ее родственной OS X; действительно, протокол делегата — это хлеб с маслом более чем для половины фреймворков, хотя с появлением нового async/await он может измениться в ближайшие годы. Тем не менее, протоколы, похоже, тем временем меняют свои цвета с тех пор, как SwiftUI вышел в 2019 году.

Я говорю это потому, что SwiftUI включает в себя несколько обязательных протоколов, связанных с самим языком. Базовые протоколы, такие как Equatable, Comparable, Identifying и Hashable, хотя не всегда очевидно, что происходит.

Идентифицируемый

Это первый протокол в качестве нового кодера SwiftUI, с которым вы, вероятно, столкнетесь, когда попытаетесь определить цикл ForEach, например, внутри List — при условии, что у нас есть массив кубиков, содержащий пользовательскую структуру.

struct ContentView: View {
  @State var dice = [Dice]()
  var body: some View {
    ForEach(dice) {
      Text(String($0.value))
    }
  }
}

Компилятору нужно средство уникальной идентификации каждой строки в цикле вашей структуры. Показанная здесь переменная dice должна соответствовать Identifiable. Соответствие, которое вы получаете с таким кодом.

struct Dice: Identifiable {
  //  var id = UUID()
  var id = Date().timeIntervalSince1970 // epoch [dies Jan 19, 2038]
  var value: Int!
}

Хэшируемый

Второй протокол, с которым вы, вероятно, столкнетесь, — это Hashable, который SwiftUI потребуется для цикла, подобного показанному здесь.

ForEach(dice, id: \.self) { die in
  Text("Die: \(die.value)")
}

Но будьте осторожны, потому что если вы впоследствии добавите третий протокол Equatable с показанным определением — это приведет к сбою вашего кода.

struct Dice: Equatable, Hashable {
  var id = UUID()
  var value: Int!
  static func ==(lhs: Dice, rhs: Dice) -> Bool {
    lhs.value == rhs.value
  }
}

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

Чтобы использовать как протокол Hashable, так и протокол Equatable, вам нужно сообщить протоколу Hashable, чтобы он сосредоточился на идентификаторе, поэтому да, это уникальное значение Identifiable.

extension Dice: Hashable {
  static func ==(lhs: Dice, rhs: Dice) -> Bool {
    lhs.id == rhs.id
  }
  func hash(into hasher: inout Hasher) {
    hasher.combine(id)
  }
}

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

.onAppear {
  var hash = Hasher()
  hash.combine(die.id)
  print("hash \(hash.finalize()) \(die.hashValue)")
}

Справочная страница здесь в Apple дает лучшее практическое применение этого протокола.

Сопоставимые

Следующим в моем списке протоколов является сопоставимый, который выглядит почти так же, как Equatable.

extension Dice: Comparable {
  static func < (lhs: Dice, rhs: Dice) -> Bool {
    lhs.value < rhs.value
  }
}

С этим кодом, добавленным в наш интерфейс SwiftUI, чтобы использовать новый протокол/свойство.

if dice.count == 2 {
  if dice.first! > dice.last! {
    Text("Winner 1st")
  } else {
    Text("Winner 2nd")
  }
}

Но здесь есть оговорка. Поскольку мне пришлось указать на id, чтобы соответствовать протоколу Hashable, я не могу использовать == таким же образом.

if dice.first! == dice.last! {
  Text("Unequal \(dice.hashValue)")
} else {
  Text("Equal \(dice.hashValue)")
}

Чтобы обойти/исправить это, мне нужен новый оператор. Исправление для приведенного выше требует, чтобы я создал новый инфиксный оператор, такой как ====.. Очевидно, мне нужно обновить фрагмент кода, показанный выше, чтобы использовать ==== вместо показанного ==.

infix operator ==== : DefaultPrecedence
extension Dice {
  static func ====(lhs: Dice, rhs: Dice) -> Bool {
    lhs.face == rhs.face
  }
}

Я уверен, что Apple хотела бы, чтобы вы использовали протоколы в своем повседневном коде, чтобы сделать более очевидным то, что вы пытаетесь сделать, расширение типов, которые вы можете использовать в своих пользовательских объектах.

равный

Хорошо, я признаю, что инфиксный оператор не для всех — в частности, для пуритан Swift. Итак, вот альтернатива, использующая более уравновешенный и без инфикса.

В нем я определяю представление как соответствующее протоколу уравновешивания, чтобы нацеливаться на лицевую сторону моего игрального кубика в нем.

Примечание. Я использовал .onAppear для первоначальной установки die1 и die2, а затем .onChange для управления всеми последующими перезагрузками костей каждый раз, когда я бросал новую пару.

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