Est-il sûr de supprimer les clés sélectionnées de la carte dans une boucle de portée?

135

Comment supprimer les clés sélectionnées d'une carte? Est-il sûr de combiner delete()avec range, comme dans le code ci-dessous?

package main

import "fmt"

type Info struct {
    value string
}

func main() {
    table := make(map[string]*Info)

    for i := 0; i < 10; i++ {
        str := fmt.Sprintf("%v", i)
        table[str] = &Info{str}
    }

    for key, value := range table {
        fmt.Printf("deleting %v=>%v\n", key, value.value)
        delete(table, key)
    }
}

https://play.golang.org/p/u1vufvEjSw

Everton
la source

Réponses:

174

C'est sûr! Vous pouvez également trouver un exemple similaire dans Effective Go :

for key := range m {
    if key.expired() {
        delete(m, key)
    }
}

Et la spécification de la langue :

L'ordre d'itération sur les cartes n'est pas spécifié et il n'est pas garanti qu'il soit le même d'une itération à l'autre. Si les entrées de mappe qui n'ont pas encore été atteintes sont supprimées pendant l'itération , les valeurs d'itération correspondantes ne seront pas produites. Si des entrées de mappe sont créées pendant l'itération , cette entrée peut être produite pendant l'itération ou peut être ignorée. Le choix peut varier pour chaque entrée créée et d'une itération à l'autre. Si la carte est nulle, le nombre d'itérations est égal à 0.

Sébastien
la source
key.expired undefined (la chaîne de type n'a pas de champ ou de méthode expirée)
4
@kristen - dans l'exemple décrit ci-dessus, la clé ne doit pas être une chaîne mais plutôt un type personnalisé qui implémente l' func (a T) expired() boolinterface. Pour les besoins de cet exemple, vous pouvez essayer: m := make(map[int]int) /* populate m here somehow */ for key := range (m) { if key % 2 == 0 { /* this is just some condition, such as calling expired */ delete(m, key); } }
abanana
Très perturbant.
g10guang le
150

La réponse de Sebastian est exacte, mais je voulais savoir pourquoi c'était sûr, alors j'ai fouillé dans le code source de Map . Cela ressemble à un appel à delete(k, v), il définit simplement un indicateur (ainsi que la modification de la valeur de comptage) au lieu de supprimer réellement la valeur:

b->tophash[i] = Empty;

(Vide est une constante pour la valeur 0)

Ce que la carte semble réellement faire est d'allouer un nombre défini de compartiments en fonction de la taille de la carte, qui augmente au fur et à mesure que vous effectuez des insertions au taux de 2^B(à partir de ce code source ):

byte    *buckets;     // array of 2^B Buckets. may be nil if count==0.

Il y a donc presque toujours plus de buckets alloués que vous n'en utilisez, et lorsque vous effectuez un rangeover the map, il vérifie la tophashvaleur de chaque bucket 2^Bpour voir s'il peut l'ignorer.

Pour résumer, deletedans un rangeest sûr parce que les données sont techniquement toujours là, mais quand il vérifie le, tophashil voit qu'il peut simplement l'ignorer et ne pas l'inclure dans l' rangeopération que vous effectuez. Le code source comprend même un TODO:

 // TODO: consolidate buckets if they are mostly empty
 // can only consolidate if there are no live iterators at this size.

Cela explique pourquoi l'utilisation de la delete(k,v)fonction ne libère pas réellement de mémoire, mais la supprime simplement de la liste des buckets auxquels vous êtes autorisé à accéder. Si vous souhaitez libérer de la mémoire réelle, vous devrez rendre la carte entière inaccessible afin que le garbage collection intervienne. Vous pouvez le faire en utilisant une ligne comme

map = nil
Verran
la source
2
On dirait donc que vous dites qu'il est prudent de supprimer toute valeur arbitraire de la carte, pas seulement celle «actuelle», n'est-ce pas? Et quand vient le temps d'évaluer un hachage que j'ai précédemment supprimé arbitrairement, il le sautera en toute sécurité?
Flimzy
@Flimzy C'est correct, comme vous pouvez le voir sur ce terrain de jeu play.golang.org/p/FwbsghzrsO . Notez que si l'index que vous supprimez est le premier de la plage, il affichera toujours celui-là car il est déjà écrit dans k, v mais si vous définissez l'index sur n'importe quel autre que le premier que la plage trouve, il n'affichera que deux clés / valeur paires au lieu de trois et pas de panique.
Verran
1
Le "ne libère pas réellement de mémoire" est-il toujours pertinent? J'ai essayé de trouver dans la source ce commentaire mais je ne le trouve pas.
Tony
11
Remarque importante: rappelez-vous qu'il ne s'agit que de l' implémentation actuelle , et qu'elle peut changer à l'avenir, vous ne devez donc pas vous fier à des propriétés supplémentaires qu'elle peut sembler "prendre en charge". Les seules garanties dont vous disposez sont celles fournies par la spécification, comme décrit dans la réponse de Sebastian . (Cela dit, explorer et expliquer les éléments internes de Go est certainement intéressant, instructif et généralement génial!)
Akavel
4

Je me demandais si une fuite de mémoire pouvait se produire. J'ai donc écrit un programme de test:

package main

import (
    log "github.com/Sirupsen/logrus"
    "os/signal"
    "os"
    "math/rand"
    "time"
)

func main() {
    log.Info("=== START ===")
    defer func() { log.Info("=== DONE ===") }()

    go func() {
        m := make(map[string]string)
        for {
            k := GenerateRandStr(1024)
            m[k] = GenerateRandStr(1024*1024)

            for k2, _ := range m {
                delete(m, k2)
                break
            }
        }
    }()

    osSignals := make(chan os.Signal, 1)
    signal.Notify(osSignals, os.Interrupt)
    for {
        select {
        case <-osSignals:
            log.Info("Recieved ^C command. Exit")
            return
        }
    }
}

func GenerateRandStr(n int) string {
    rand.Seed(time.Now().UnixNano())
    const letterBytes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    b := make([]byte, n)
    for i := range b {
        b[i] = letterBytes[rand.Int63() % int64(len(letterBytes))]
    }
    return string(b)
}

On dirait que GC libère la mémoire. Alors ça va.

vitvlkv
la source
0

Bref oui. Voir les réponses précédentes.

Et aussi ceci, d' ici :

ianlancetaylor a commenté le 18 février 2015
Je pense que la clé pour comprendre cela est de réaliser que lors de l'exécution du corps d'une instruction for / range, il n'y a pas d'itération actuelle. Il existe un ensemble de valeurs qui ont été vues et un ensemble de valeurs qui n'ont pas été vues. Lors de l'exécution du corps, l'une des paires clé / valeur qui a été vue - la paire la plus récente - a été affectée à la ou aux variables de l'instruction range. Il n'y a rien de spécial à propos de cette paire clé / valeur, c'est juste l'une de celles qui ont déjà été vues lors de l'itération.

La question à laquelle il répond concerne la modification des éléments cartographiques en place lors d'une rangeopération, c'est pourquoi il mentionne «l'itération actuelle». Mais c'est également pertinent ici: vous pouvez supprimer des clés pendant une plage, ce qui signifie simplement que vous ne les verrez pas plus tard dans la plage (et si vous les avez déjà vues, c'est pas grave).

Larry Clapp
la source