web-dev-qa-db-fra.com

Comment implémenter le protocole hashable dans Swift pour un tableau Int (une structure de chaîne personnalisée)

Je crée une structure qui agit comme un String, sauf qu'elle ne traite que des valeurs scalaires Unicode UTF-32. Ainsi, c'est un tableau de UInt32. (Voir cette question pour plus d'informations.)

Ce que je veux faire

Je veux pouvoir utiliser ma structure ScalarString personnalisée comme clé dans un dictionnaire. Par exemple:

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
}

Problème

Pour ce faire, ScalarString doit implémenter Hashable Protocol . Je pensais pouvoir faire quelque chose comme ça:

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
}

mais j'ai découvert que tableaux Swift n'ont pas de hashValue.

Ce que j'ai lu

L'article Stratégies pour implémenter le protocole hashable dans Swift avait beaucoup de bonnes idées, mais je n'en ai vu aucune qui semblait fonctionner correctement dans ce cas. Plus précisément,

  • Propriété d'objet (le tableau ne contient pas hashValue)
  • ID propriété (je ne sais pas comment cela pourrait être bien mis en œuvre)
  • Formula (il semble que toute formule pour une chaîne d'entiers de 32 bits soit lourde pour le processeur et ait beaucoup de débordements d'entiers)
  • ObjectIdentifier (J'utilise une structure, pas une classe)
  • Héritant de NSObject (J'utilise une structure, pas une classe)

Voici quelques autres choses que j'ai lues:

Question

Les chaînes Swift ont une propriété hashValue , donc je sais que c'est possible.

Comment créer un hashValue pour ma structure personnalisée?

Mises à jour

Mise à jour 1: Je voudrais faire quelque chose qui n'implique pas la conversion en String puis l'utilisation de String's hashValue. Mon but pour créer ma propre structure était de pouvoir éviter de faire beaucoup de conversions String. String obtient son hashValue de quelque part. Il semble que je pourrais l'obtenir en utilisant la même méthode.

Mise à jour 2: J'ai étudié l'implémentation d'algorithmes de codes de hachage de chaîne dans d'autres contextes. J'ai un peu de difficulté à savoir ce qui est le mieux et à les exprimer dans Swift, cependant.

Mise à jour 3

Je préférerais ne pas importer de frameworks externes à moins que ce soit la voie recommandée pour ces choses.

J'ai soumis une solution possible en utilisant la fonction de hachage DJB.

40
Suragch

Mise à jour

Martin R écrit :

À partir de Swift 4.1 , le compilateur peut synthétiser Equatable et Hashable pour la conformité des types automatiquement, si tous les membres se conforment à Equatable/Hashable (SE0185). Et à partir de Swift 4.2 , un combineur de hachage de haute qualité est intégré dans la bibliothèque standard Swift (SE -0206).

Il n'est donc plus nécessaire de définir votre propre fonction de hachage, il suffit de déclarer la conformité:

struct ScalarString: Hashable, ... {

    private var scalarArray: [UInt32] = []

    // ... }

Ainsi, la réponse ci-dessous doit être réécrite (encore une fois). En attendant, reportez-vous à la réponse de Martin R à partir du lien ci-dessus.


Ancienne réponse:

Cette réponse a été complètement réécrite après avoir soumis ma réponse originale à la révision du code .

Comment mettre en œuvre le protocole Hashable

Le protocole hashable vous permet d'utiliser votre classe ou structure personnalisée comme clé de dictionnaire. Afin de mettre en œuvre ce protocole, vous devez

  1. Implémenter Equatable protocol (Hashable hérite d'Equatable)
  2. Renvoie un hashValue calculé

Ces points découlent de l'axiome donné dans la documentation:

x == y Implique x.hashValue == y.hashValue

x et y sont des valeurs de certains types.

Mettre en œuvre le protocole Equatable

Afin d'implémenter le protocole Equatable, vous définissez comment votre type utilise l'opérateur == (Équivalence). Dans votre exemple, l'équivalence peut être déterminée comme ceci:

func ==(left: ScalarString, right: ScalarString) -> Bool {
    return left.scalarArray == right.scalarArray
}

La fonction == Est globale, elle sort donc de votre classe ou structure.

Renvoie un hashValue calculé

Votre classe ou structure personnalisée doit également avoir une variable hashValue calculée. Un bon algorithme de hachage fournira une large gamme de valeurs de hachage. Cependant, il convient de noter que vous n'avez pas besoin de garantir que les valeurs de hachage sont toutes uniques. Lorsque deux valeurs différentes ont des valeurs de hachage identiques, cela s'appelle une collision de hachage. Cela nécessite un travail supplémentaire en cas de collision (c'est pourquoi une bonne distribution est souhaitable), mais certaines collisions sont à prévoir. Si je comprends bien, la fonction == Fait ce travail supplémentaire. ( Mise à jour : Il semble que == Puisse faire tout le travail. )

Il existe plusieurs façons de calculer la valeur de hachage. Par exemple, vous pouvez faire quelque chose d'aussi simple que de renvoyer le nombre d'éléments dans le tableau.

var hashValue: Int {
    return self.scalarArray.count
} 

Cela donnerait une collision de hachage chaque fois que deux tableaux avaient le même nombre d'éléments mais des valeurs différentes. NSArray utilise apparemment cette approche.

Fonction de hachage DJB

Une fonction de hachage commune qui fonctionne avec des chaînes est la fonction de hachage DJB. C'est celui que j'utiliserai, mais jetez un œil à d'autres ici .

A Swift fournie par @MartinR suit:

var hashValue: Int {
    return self.scalarArray.reduce(5381) {
        ($0 << 5) &+ $0 &+ Int($1)
    }
}

Il s'agit d'une version améliorée de mon implémentation d'origine, mais permettez-moi également d'inclure l'ancien formulaire développé, qui peut être plus lisible pour les personnes qui ne connaissent pas reduce . C'est équivalent, je crois:

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
} 

L'opérateur &+ Permet à Int de déborder et de recommencer pour les longues chaînes.

Grande image

Nous avons regardé les morceaux, mais permettez-moi maintenant de montrer tout l'exemple de code en ce qui concerne le protocole Hashable. ScalarString est le type personnalisé de la question. Ce sera différent pour différentes personnes, bien sûr.

// 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
}

Autre lecture utile

Crédits

Un grand merci à Martin R dans Code Review. Ma réécriture est largement basée sur sa réponse . Si vous avez trouvé cela utile, veuillez lui donner une note positive.

Mise à jour

Swift est maintenant open source, il est donc possible de voir comment hashValue est implémenté pour String à partir de code source . Elle semble plus complexe que la réponse que j'ai donnée ici et je n'ai pas pris le temps de l'analyser complètement. N'hésitez pas à le faire vous-même.

46
Suragch

Ce n'est pas une solution très élégante mais cela fonctionne bien:

"\(scalarArray)".hashValue

ou

scalarArray.description.hashValue

Qui utilise simplement la représentation textuelle comme source de hachage

4
Kametrixom

Edit (31 mai '17): Veuillez vous référer à la réponse acceptée. Cette réponse est à peu près juste une démonstration sur la façon d'utiliser le CommonCrypto Framework

D'accord, j'ai pris de l'avance et étendu tous les tableaux avec le protocole Hashable en utilisant l'algorithme de hachage SHA-256 du framework CommonCrypto. Vous devez mettre

#import <CommonCrypto/CommonDigest.h>

dans votre en-tête de pontage pour que cela fonctionne. C'est dommage que les pointeurs doivent être utilisés cependant:

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 mai '17): Ne faites pas cela, même si SHA256 n'a pratiquement pas de collisions de hachage, c'est la mauvaise idée de définir l'égalité par l'égalité de hachage

public func ==<T>(lhs: [T], rhs: [T]) -> Bool {
    return lhs.hashValue == rhs.hashValue
}

C'est aussi bon qu'avec CommonCrypto. C'est moche, mais rapide et pas beaucoupà peu près pas de collisions de hachage à coup sûr

Edit (15 juillet '15): Je viens de faire quelques tests de vitesse:

Int tableaux de taille n remplis aléatoirement ont pris en moyenne plus de 1 000 exécutions

n      -> time
1000   -> 0.000037 s
10000  -> 0.000379 s
100000 -> 0.003402 s

Alors qu'avec la méthode de hachage de chaîne:

n      -> time
1000   -> 0.001359 s
10000  -> 0.011036 s
100000 -> 0.122177 s

La méthode SHA-256 est donc environ 33 fois plus rapide que la chaîne. Je ne dis pas que l'utilisation d'une chaîne est une très bonne solution, mais c'est la seule à laquelle nous pouvons la comparer en ce moment

4
Kametrixom

Une suggestion - puisque vous modélisez un String, est-ce que cela fonctionnerait pour convertir votre [UInt32] tableau à un String et utilisez le StringhashValue? Comme ça:

var hashValue : Int {
    get {
        return String(self.scalarArray.map { UnicodeScalar($0) }).hashValue
    }
}

Cela pourrait vous permettre de comparer facilement votre struct personnalisé avec Strings, bien que cela soit une bonne idée ou non dépend de ce que vous essayez de faire ...

Notez également qu'en utilisant cette approche, les instances de ScalarString auraient la même hashValue si leurs représentations String étaient canoniquement équivalentes, ce qui peut ou non être ce que vous désirez.

Je suppose donc que si vous voulez que le hashValue représente un String unique, mon approche serait bonne. Si vous souhaitez que hashValue représente une séquence unique de UInt32 valeurs, la réponse de @ Kametrixom est la voie à suivre ...

3
Aaron Rasmussen