Apakah aman untuk menghapus kunci yang dipilih dari peta dalam lingkaran jangkauan?

135

Bagaimana cara menghapus kunci yang dipilih dari peta? Apakah aman digabungkan delete()dengan rentang, seperti pada kode di bawah ini?

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
sumber

Jawaban:

174

Ini aman! Anda juga dapat menemukan sampel serupa di Efektif Go :

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

Dan spesifikasi bahasa :

Urutan iterasi atas peta tidak ditentukan dan tidak dijamin sama dari satu iterasi ke yang berikutnya. Jika entri peta yang belum tercapai dihapus selama iterasi , nilai iterasi yang sesuai tidak akan diproduksi. Jika entri peta dibuat selama iterasi , entri itu dapat diproduksi selama iterasi atau dapat dilewati. Pilihannya dapat bervariasi untuk setiap entri yang dibuat dan dari satu iterasi ke yang berikutnya. Jika peta nihil, jumlah iterasi adalah 0.

Sebastian
sumber
key.expired undefined (tipe string tidak memiliki bidang atau metode kedaluwarsa)
4
@ Kristen - dalam contoh yang dijelaskan di atas, kuncinya tidak boleh berupa string melainkan beberapa tipe khusus yang mengimplementasikan func (a T) expired() boolantarmuka. Untuk keperluan contoh ini, Anda dapat mencoba: 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
Sangat membingungkan.
g10guang
150

Jawaban Sebastian akurat, tetapi saya ingin tahu mengapa itu aman, jadi saya melakukan penggalian ke dalam kode sumber Peta . Sepertinya pada panggilan ke delete(k, v), itu pada dasarnya hanya menetapkan bendera (serta mengubah nilai hitungan) daripada benar-benar menghapus nilai:

b->tophash[i] = Empty;

(Kosong adalah konstanta untuk nilainya 0)

Apa yang sebenarnya dilakukan oleh peta adalah mengalokasikan sejumlah ember tergantung pada ukuran peta, yang tumbuh saat Anda melakukan sisipan dengan laju 2^B(dari kode sumber ini ):

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

Jadi hampir selalu ada lebih banyak ember yang dialokasikan daripada yang Anda gunakan, dan ketika Anda melakukan rangelebih dari peta, itu memeriksa tophashnilai setiap ember di 2^Bdalamnya untuk melihat apakah dapat melompati itu.

Untuk meringkas, deletedalam a rangeaman karena data secara teknis masih ada, tetapi ketika memeriksa tophashitu melihat bahwa itu bisa melewatinya dan tidak memasukkannya dalam rangeoperasi apa pun yang Anda lakukan. Kode sumber bahkan termasuk TODO:

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

Ini menjelaskan mengapa menggunakan delete(k,v)fungsi ini sebenarnya tidak membebaskan memori, hanya menghilangkannya dari daftar bucket yang diizinkan untuk Anda akses. Jika Anda ingin mengosongkan memori yang sebenarnya, Anda harus membuat seluruh peta tidak dapat dijangkau sehingga pengumpulan sampah akan masuk. Anda dapat melakukan ini menggunakan garis seperti

map = nil
Verran
sumber
2
Jadi sepertinya Anda mengatakan aman untuk menghapus nilai arbitrer apa pun dari peta, bukan hanya nilai 'saat ini', benar? Dan ketika tiba saatnya untuk mengevaluasi hash yang sebelumnya saya hapus secara sewenang-wenang, ia akan dengan aman melewatinya?
Flimzy
@Flimzy Itu benar, seperti yang Anda lihat dari taman bermain ini play.golang.org/p/FwbsghzrsO . Perhatikan bahwa jika indeks yang Anda hapus adalah yang pertama dalam rentang, itu akan tetap menunjukkan yang sejak itu sudah ditulis ke k, v tetapi jika Anda mengatur indeks ke yang lain selain yang pertama yang ditemukan itu hanya akan menampilkan dua kunci / nilai berpasangan alih-alih tiga dan tidak panik.
Verran
1
Apakah "tidak benar-benar membebaskan memori" masih relevan? Saya mencoba mencari di sumber yang berkomentar tetapi tidak dapat menemukannya.
Tony
11
Catatan penting: ingat bahwa ini hanya implementasi saat ini , dan itu mungkin berubah di masa depan, jadi Anda tidak harus bergantung pada properti tambahan apa pun yang tampaknya "mendukung". Satu- satunya jaminan yang Anda miliki adalah yang disediakan oleh spesifikasi, seperti yang dijelaskan dalam jawaban Sebastian . (Yang mengatakan, menjelajahi dan menjelaskan internal Go pasti menarik, mendidik, dan umumnya luar biasa!)
aliasvel
4

Saya bertanya-tanya apakah kebocoran memori bisa terjadi. Jadi saya menulis program uji:

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

Sepertinya GC memang membebaskan memori. Jadi tidak apa-apa.

vitvlkv
sumber
0

Singkatnya, ya. Lihat jawaban sebelumnya.

Dan juga ini, dari sini :

ianlancetaylor berkomentar pada 18 Feb 2015
Saya pikir kunci untuk memahami ini adalah untuk menyadari bahwa ketika mengeksekusi tubuh pernyataan for / range, tidak ada iterasi saat ini. Ada satu set nilai yang telah dilihat, dan satu set nilai yang belum terlihat. Saat mengeksekusi tubuh, salah satu pasangan kunci / nilai yang telah dilihat - pasangan terbaru - ditugaskan ke variabel (s) dari pernyataan rentang. Tidak ada yang istimewa tentang pasangan kunci / nilai itu, hanya salah satu yang sudah terlihat selama iterasi.

Pertanyaan yang dia jawab adalah tentang memodifikasi elemen peta yang ada selama rangeoperasi, itulah sebabnya dia menyebutkan "iterasi saat ini". Tetapi ini juga relevan di sini: Anda dapat menghapus kunci selama suatu rentang, dan itu berarti bahwa Anda tidak akan melihatnya nanti dalam rentang tersebut (dan jika Anda sudah melihatnya, tidak apa-apa).

Larry Clapp
sumber