Чи безпечно видаляти вибрані ключі з карти в циклі діапазону?


135

Як можна видалити вибрані ключі з карти? Чи безпечно поєднувати delete()з дальністю, як у наведеному нижче коді?

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

Відповіді:


174

Це безпечно! Ви також можете знайти подібний зразок у програмі Effective Go :

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

І мовна специфікація :

Порядок ітерації карт не вказаний і не гарантується, що він буде однаковим від однієї ітерації до іншої. Якщо під час ітерації буде видалено записи, які ще не були досягнуті , відповідні значення ітерації не виробляться. Якщо записи карт створені під час ітерації , цей запис може бути зроблений під час ітерації або може бути пропущений. Вибір може відрізнятися для кожної створеної записи та від однієї ітерації до наступної. Якщо карта нульова, кількість ітерацій дорівнює 0.


не вказано key.expired (рядок типу не має поля чи методу минув)

4
@kristen - в описаному вище прикладі ключ повинен бути не рядком, а певним типом, що реалізує func (a T) expired() boolінтерфейс. Для цього прикладу ви можете спробувати: 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); } }
абанана

Дуже заплутано.
g10guang

150

Відповідь Себастьяна точна, але я хотів дізнатися, чому це безпечно, тому я трохи перекопався у вихідному коді карти . Це виглядає як під час виклику до delete(k, v), він в основному просто встановлює прапор (а також змінює значення підрахунку), а не фактично видаляє значення:

b->tophash[i] = Empty;

(Порожня - константа для значення 0)

Насправді карта, що насправді робить, - це виділення заданої кількості відра залежно від розміру карти, яка зростає під час виконання вставок зі швидкістю 2^Bцього вихідного коду ):

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

Тому майже завжди виділяється більше відра, ніж ви використовуєте, і коли ви робите rangeнад картою, вона перевіряє це tophashзначення кожного відра, 2^Bщоб побачити, чи може він пропустити його.

Підводячи підсумок, deleteінтервал a rangeє безпечним, оскільки дані технічно все ще є, але коли він перевіряє, tophashвін бачить, що він може просто пропустити його і не включати його в будь-яку rangeоперацію, яку ви виконуєте. Вихідний код навіть включає TODO:

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

Це пояснює, чому використання delete(k,v)функції насправді не звільняє пам’ять, а просто видаляє її зі списку відер, до яких ви можете отримати доступ. Якщо ви хочете звільнити фактичну пам'ять, вам потрібно зробити всю карту недоступною, щоб збирати сміття. Ви можете це зробити, використовуючи такий рядок, як

map = nil

2
Отже, це здається, що ви говорите, що безпечно видалити з карти будь-яке довільне значення, а не лише "поточне", правда? І коли настане час оцінити хеш, який я раніше довільно видалив, він сміливо пропустить його?
Flimzy

@Flimzy Це правильно, як ви бачите з цього майданчика play.golang.org/p/FwbsghzrsO . Зауважте, що якщо індекс, який ви видаляєте, є першим у діапазоні, він все одно буде показувати, що той, який уже написаний на k, v, але якщо встановити індекс будь-який, крім першого, який знаходить діапазон, він відображатиме лише два клавіші / Значення пар замість трьох і не панікувати.
Верран

1
Чи "фактично не звільняє пам'ять" все ще актуально? Я намагався знайти у джерелі цей коментар, але не можу його знайти.
Тоні

11
Важлива примітка: пам’ятайте, що це лише поточна реалізація , і вона може змінитися в майбутньому, тому ви не повинні покладатися на будь-які додаткові властивості, які, можливо, можуть “підтримувати”. У Єдині гарантії у вас є це ті , які передбачені в специфікації, як описано у відповідь Себастьяна . (Ось сказано, вивчення та пояснення
інтернатів

4

Мені було цікаво, чи може статися витік пам'яті. Тому я написав тестову програму:

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

Схоже, GC дійсно звільняє пам'ять. Так це гаразд.


0

Словом, так. Дивіться попередні відповіді.

А також це, звідси :

ianlancetaylor прокоментував 18 лютого 2015 р.
Я думаю, що ключовим для розуміння цього є усвідомлення того, що під час виконання тілом оператора для / діапазону не існує поточної ітерації. Є набір значень, які були помічені, і набір значень, які не були помічені. Виконуючи тіло, одній із розглянутих пар ключів / значень - останню пару - присвоювали змінну (и) оператора діапазону. У цій парі ключ / значення немає нічого особливого, це лише одна з тих, які вже були помічені під час ітерації.

Питання, на яке він відповідає, стосується зміни елементів карти на місці під час rangeоперації, саме тому він згадує про "поточну ітерацію". Але це також актуально і тут: ви можете видаляти ключі під час діапазону, а це просто означає, що ви їх більше не побачите в діапазоні (і якщо ви вже бачили їх, це нормально).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.