Как реализовать протокол Hashable в Swift для массива Int (настраиваемая строковая структура)
Я создаю структуру, которая действует как String
, за исключением того, что она использует только скалярные значения Unicode UTF-32. Таким образом, это массив из UInt32
. (См. этот вопрос для получения дополнительной информации.)
Что я хочу сделать
Я хочу, чтобы использовать пользовательскую структуру ScalarString
как ключ в словаре. Например:
var suffixDictionary = [ScalarString: ScalarString]() // Unicode key, rendered glyph value
// populate dictionary
suffixDictionary[keyScalarString] = valueScalarString
// ...
// check if dictionary contains Unicode scalar string key
if let renderedSuffix = suffixDictionary[unicodeScalarString] {
// do something with value
}
Проблема
Чтобы сделать это, ScalarString
необходимо реализовать Hashable Protocol. Я думал, что смогу сделать что-то вроде этого:
struct ScalarString: Hashable {
private var scalarArray: [UInt32] = []
var hashValue : Int {
get {
return self.scalarArray.hashValue // error
}
}
}
func ==(left: ScalarString, right: ScalarString) -> Bool {
return left.hashValue == right.hashValue
}
но затем я обнаружил, что массивы Swift не имеют hashValue
.
Что я читал
В статье "Стратегии реализации протокола Hashable в Swift" было много замечательных идей, но я не видел таких, которые казались бы в этом случае они будут хорошо работать. В частности,
- Свойство объекта (массив не имеет
hashValue
)
- Свойство ID (не уверен, как это можно было бы реализовать хорошо)
- Формула (похоже, любая формула для строки из 32-битных целых чисел будет тяжелой для процессора и имеет много целых переполнений)
- ObjectIdentifier (я использую структуру, а не класс)
- Наследование из NSObject (я использую структуру, а не класс)
Вот некоторые другие вещи, которые я читал:
Вопрос
Swift Strings имеют свойство hashValue
, поэтому я знаю, что это возможно.
Как создать hashValue
для моей пользовательской структуры?
Обновления
Обновление 1: Я хотел бы сделать что-то, что не связано с преобразованием в String
, а затем с помощью String
hashValue
. Весь смысл создания моей собственной структуры состоял в том, чтобы я мог избежать большого количества преобразований String
. String
получает его hashValue
откуда-то. Кажется, я мог получить его, используя тот же метод.
Обновление 2: Я изучаю реализацию алгоритмов хеш-кодов строк из других контекстов. Мне трудно понять, что лучше всего и выражать их в Свифте.
Обновление 3
Я бы предпочел не импортировать какие-либо внешние фреймворки, если это не рекомендуется для этих целей.
Я представил возможное решение, используя функцию хеша DJB.
Ответы
Ответ 1
Этот ответ был полностью переписан после отправки моего исходного ответа на обзор кода.
Как реализовать протокол Hashable
Протокол Hashable позволяет использовать ваш собственный класс или структуру в качестве словарного ключа. Для реализации этого протокола вам необходимо
- Внедрить Equatable protocol (Hashable наследует от Equatable)
- Возвращает вычисленный
hashValue
Эти точки следуют из аксиомы, приведенной в документации:
x == y
означает x.hashValue == y.hashValue
где x
и y
- значения некоторого типа.
Внедрить Equableable
Чтобы реализовать Equatable protocol, вы определяете, как ваш тип использует оператор ==
(эквивалентность). В вашем примере эквивалентность может быть определена следующим образом:
func ==(left: ScalarString, right: ScalarString) -> Bool {
return left.scalarArray == right.scalarArray
}
Функция ==
является глобальной, поэтому она выходит за пределы вашего класса или структуры.
Возвращает вычисленный hashValue
Ваш собственный класс или структура также должен иметь вычисленную переменную hashValue
. Хороший алгоритм хеширования обеспечит широкий диапазон значений хеширования. Однако следует отметить, что вам не нужно гарантировать, что значения хэша уникальны. Когда два разных значения имеют одинаковые хэш-значения, это называется хеш-коллизией. Это требует некоторой дополнительной работы при столкновении (поэтому желательно хорошее распределение), но следует ожидать некоторых столкновений. Как я понимаю, функция ==
выполняет эту дополнительную работу. (Обновить: Похоже, что ==
может выполнять всю работу.)
Существует несколько способов вычисления значения хэш-функции. Например, вы можете сделать что-то так же просто, как вернуть количество элементов в массиве.
var hashValue: Int {
return self.scalarArray.count
}
Это даст хеш-столкновение каждый раз, когда два массива имеют одинаковое количество элементов, но разные значения. NSArray
, очевидно, использует этот подход.
Функция хэша DJB
Общей хэш-функцией, которая работает со строками, является хеш-функция DJB. Это тот, который я буду использовать, но ознакомьтесь с некоторыми другими здесь.
Ниже приведена реализация Swift предоставляемая @MartinR:
var hashValue: Int {
return self.scalarArray.reduce(5381) {
($0 << 5) &+ $0 &+ Int($1)
}
}
Это улучшенная версия моей первоначальной реализации, но позвольте мне также включить более старую расширенную форму, которая может быть более читаемой для людей, не знакомых с reduce
. Это эквивалентно, я считаю:
var hashValue: Int {
// DJB Hash Function
var hash = 5381
for(var i = 0; i < self.scalarArray.count; i++)
{
hash = ((hash << 5) &+ hash) &+ Int(self.scalarArray[i])
}
return hash
}
Оператор &+
позволяет Int
переполняться и начинать заново для длинных строк.
Большое изображение
Мы просмотрели фрагменты, но позвольте мне теперь показать весь примерный код, относящийся к протоколу Hashable. ScalarString
- это настраиваемый тип вопроса. Разумеется, это разные для разных людей.
// Include the Hashable keyword after the class/struct name
struct ScalarString: Hashable {
private var scalarArray: [UInt32] = []
// required var for the Hashable protocol
var hashValue: Int {
// DJB hash function
return self.scalarArray.reduce(5381) {
($0 << 5) &+ $0 &+ Int($1)
}
}
}
// required function for the Equatable protocol, which Hashable inheirits from
func ==(left: ScalarString, right: ScalarString) -> Bool {
return left.scalarArray == right.scalarArray
}
Прочее полезное чтение
Кредиты
Большое спасибо Мартину Р в обзоре кода. Моя переписка в основном основана на его ответе. Если вы сочтете это полезным, то, пожалуйста, дайте ему возвышение.
Update
Swift теперь открыт с открытым исходным кодом, поэтому можно увидеть, как hashValue
реализован для String
из исходного кода. Это кажется более сложным, чем ответ, который я дал здесь, и я не нашел времени, чтобы проанализировать его полностью. Не стесняйтесь делать это самостоятельно.
Ответ 2
Изменить (31 мая '17): см. принятый ответ. Этот ответ в значительной степени просто демонстрирует, как использовать CommonCrypto
Framework
Хорошо, я продвинулся вперед и расширил все массивы протоколом Hashable
, используя алгоритм хэширования SHA-256 из среды CommonCrypto. Вы должны поставить
#import <CommonCrypto/CommonDigest.h>
в ваш заголовок для соединения, чтобы это работало. Жаль, что указатели должны быть использованы:
extension Array : Hashable, Equatable {
public var hashValue : Int {
var hash = [Int](count: Int(CC_SHA256_DIGEST_LENGTH) / sizeof(Int), repeatedValue: 0)
withUnsafeBufferPointer { ptr in
hash.withUnsafeMutableBufferPointer { (inout hPtr: UnsafeMutableBufferPointer<Int>) -> Void in
CC_SHA256(UnsafePointer<Void>(ptr.baseAddress), CC_LONG(count * sizeof(Element)), UnsafeMutablePointer<UInt8>(hPtr.baseAddress))
}
}
return hash[0]
}
}
Edit (31 мая '17): Не делайте этого, даже если SHA256 не имеет довольно никаких хэш-коллизий, это неправильная идея определить равенство по хэш-равенству
public func ==<T>(lhs: [T], rhs: [T]) -> Bool {
return lhs.hashValue == rhs.hashValue
}
Это так же хорошо, как и с CommonCrypto
. Это некрасиво, но быстро и не много в значительной степени не содержит хеш-коллизий.
Изменить (15 июля '15): Я только что сделал несколько тестов скорости:
Случайно заполненные массивы Int
размером n занимали в среднем более 1000 прогонов
n -> time
1000 -> 0.000037 s
10000 -> 0.000379 s
100000 -> 0.003402 s
Принимая во внимание метод хеширования строк:
n -> time
1000 -> 0.001359 s
10000 -> 0.011036 s
100000 -> 0.122177 s
Таким образом, способ SHA-256 примерно в 33 раза быстрее, чем строковый. Я не говорю, что использование строки - очень хорошее решение, но единственное, с чем мы можем сравнить прямо сейчас
Ответ 3
Одно предложение - так как вы моделируете String
, будет ли он работать, чтобы преобразовать массив [UInt32]
в String
и использовать String
hashValue
? Вот так:
var hashValue : Int {
get {
return String(self.scalarArray.map { UnicodeScalar($0) }).hashValue
}
}
Это может удобно сравнить с вашим пользовательским struct
и String
, хотя, будь то хорошая идея, зависит от того, что вы пытаетесь сделать...
Заметим также, что, используя этот подход, экземпляры ScalarString
имели бы тот же hashValue
, если бы их представления String
были канонически эквивалентны, что может быть или не быть тем, что вы хотите.
Итак, я полагаю, что если вы хотите, чтобы hashValue
представлял уникальный String
, мой подход был бы хорошим. Если вы хотите, чтобы hashValue
представлял уникальную последовательность значений UInt32
, ответ @Kametrixom - это способ пойти...
Ответ 4
Это не очень элегантное решение, но оно прекрасно работает:
"\(scalarArray)".hashValue
или
scalarArray.description.hashValue
Который использует текстовое представление как хэш-источник