web-dev-qa-db-fra.com

Est-il prudent de supprimer les clés sélectionnées de la carte dans une boucle?

Comment peut-on supprimer les touches sélectionnées d'une carte? Est-il prudent de combiner delete() avec une plage, 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

90
Everton

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

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

Et la spécification de langue :

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

123
Sebastian

La réponse de Sebastian est exacte, mais je voulais savoir pourquoi il était sûr, alors j'ai fouillé dans le code source de la carte . Cela ressemble à un appel à delete(k, v), en fait, il ne fait que positionner un indicateur (ainsi que changer la valeur de comptage) au lieu de supprimer la valeur:

b->tophash[i] = Empty;

(Vide est une constante pour la valeur 0)

En réalité, la carte semble allouer un nombre défini de compartiments en fonction de la taille de la carte, qui augmente à mesure que vous effectuez des insertions au taux de 2^B (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 compartiments alloués que vous n'en utilisez, et lorsque vous effectuez une range sur la carte, il vérifie que la valeur tophash de chaque compartiment de ce 2^B permet de voir si elle peut être ignorée.

Pour résumer, la delete dans une range est sûre, car les données sont toujours là, mais quand elle vérifie la tophash, elle voit qu'elle peut simplement les ignorer et ne pas l'inclure dans l'opération range que vous effectuez. Le code source inclut même une 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 fonction delete(k,v) ne libère pas réellement de la mémoire, mais la supprime simplement de la liste des compartiments auxquels vous êtes autorisé à accéder. Si vous souhaitez libérer la mémoire réelle, vous devez rendre toute la carte inaccessible pour que la récupération de place intervienne. Vous pouvez le faire en utilisant une ligne telle que 

map = nil
121
Verran

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 c'est bon.

3
vitvlkv

En bref, oui. Voir les réponses précédentes.

Et aussi cela, de ici :

ianlancetaylor a commenté le février 18, 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 en cours. Il y a 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 ont été vues - 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 de carte en place lors d'une opération range, raison pour laquelle il mentionne "l'itération en cours". Mais c'est aussi 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, ce n'est pas grave).

0
Larry Clapp