Как реализовать протокол 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

Который использует текстовое представление как хэш-источник