Apa cara ringkas untuk membuat potongan 2D di Go?

105

Saya mempelajari Go dengan mengikuti A Tour of Go . Salah satu latihan di sana meminta saya membuat potongan 2D dybaris dan dxkolom berisi uint8. Pendekatan saya saat ini, yang berhasil, adalah ini:

a:= make([][]uint8, dy)       // initialize a slice of dy slices
for i:=0;i<dy;i++ {
    a[i] = make([]uint8, dx)  // initialize a slice of dx unit8 in each of dy slices
}

Saya pikir iterasi melalui setiap potongan untuk menginisialisasi itu terlalu bertele-tele. Dan jika potongan memiliki lebih banyak dimensi, kodenya akan menjadi sulit digunakan. Apakah ada cara ringkas untuk menginisialisasi irisan 2D (atau n-dimensi) di Go?

hazrmard
sumber

Jawaban:

151

Tidak ada cara yang lebih ringkas, apa yang Anda lakukan adalah cara yang "benar"; karena irisan selalu satu dimensi tetapi dapat disusun untuk membangun objek berdimensi lebih tinggi. Lihat pertanyaan ini untuk lebih jelasnya: Pergi: Bagaimana representasi memori array dua dimensi .

Satu hal yang dapat Anda sederhanakan adalah dengan menggunakan for rangekonstruksi:

a := make([][]uint8, dy)
for i := range a {
    a[i] = make([]uint8, dx)
}

Perhatikan juga bahwa jika Anda menginisialisasi potongan Anda dengan literal komposit , Anda mendapatkan ini secara "gratis", misalnya:

a := [][]uint8{
    {0, 1, 2, 3},
    {4, 5, 6, 7},
}
fmt.Println(a) // Output is [[0 1 2 3] [4 5 6 7]]

Ya, ini ada batasnya karena Anda harus menghitung semua elemen; tetapi ada beberapa trik, yaitu Anda tidak harus menghitung semua nilai, hanya nilai yang bukan nilai nol dari jenis elemen potongan. Untuk detail selengkapnya tentang ini, lihat Item yang dimasukkan dalam inisialisasi larik golang .

Misalnya jika Anda menginginkan potongan di mana 10 elemen pertama adalah nol, lalu mengikuti 1dan 2, itu bisa dibuat seperti ini:

b := []uint{10: 1, 2}
fmt.Println(b) // Prints [0 0 0 0 0 0 0 0 0 0 1 2]

Perhatikan juga bahwa jika Anda menggunakan array, bukan irisan , ini dapat dibuat dengan sangat mudah:

c := [5][5]uint8{}
fmt.Println(c)

Outputnya adalah:

[[0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0]]

Dalam kasus array, Anda tidak perlu mengulang array "luar" dan menginisialisasi array "dalam", karena array bukanlah deskriptor tetapi nilai. Lihat entri blog Array, irisan (dan string): Mekanisme 'tambahkan' untuk detail selengkapnya.

Coba contoh di Go Playground .

icza
sumber
Karena menggunakan array menyederhanakan kode, saya ingin melakukan itu. Bagaimana seseorang menentukan itu dalam sebuah struct? Saya mengerti cannot use [5][2]string literal (type [5][2]string) as type [][]string in field valueketika saya mencoba untuk menetapkan array ke apa yang saya kira saya katakan Go is a slice.
Eric Lindsey
Mengetahui sendiri, dan mengedit jawabannya untuk menambahkan informasi.
Eric Lindsey
1
@EricLindsey Meskipun hasil edit Anda bagus, saya masih akan menolaknya karena saya tidak ingin mendorong penggunaan array hanya karena inisialisasi lebih mudah. Di Go, array adalah yang sekunder, irisan adalah cara untuk pergi. Untuk detailnya, lihat Apa cara tercepat untuk menambahkan satu larik ke larik lainnya di Go? Array juga memiliki tempatnya, untuk detailnya, lihat Mengapa ada array di Go?
icza
cukup adil, tetapi saya yakin informasi itu masih bermanfaat. Apa yang saya coba jelaskan dengan pengeditan saya adalah bahwa jika Anda membutuhkan fleksibilitas dimensi yang berbeda antar objek maka irisan adalah cara yang tepat. Di sisi lain, jika informasi Anda terstruktur secara kaku dan akan selalu sama, array tidak hanya lebih mudah untuk diinisialisasi, tetapi juga lebih efisien. Bagaimana cara meningkatkan pengeditan?
Eric Lindsey
@EricLindsey Saya melihat Anda membuat suntingan lain yang sudah ditolak oleh orang lain. Dalam suntingan Anda, Anda mengatakan untuk menggunakan array untuk memiliki akses elemen yang lebih cepat. Perhatikan bahwa Go mengoptimalkan banyak hal, dan ini mungkin tidak terjadi, slice mungkin sama cepatnya. Untuk detailnya, lihat Array vs Slice: kecepatan mengakses .
icza
12

Ada dua cara menggunakan irisan untuk membuat matriks. Mari kita lihat perbedaan di antara keduanya.

Metode pertama:

matrix := make([][]int, n)
for i := 0; i < n; i++ {
    matrix[i] = make([]int, m)
}

Metode kedua:

matrix := make([][]int, n)
rows := make([]int, n*m)
for i := 0; i < n; i++ {
    matrix[i] = rows[i*m : (i+1)*m]
}

Berkenaan dengan metode pertama, melakukan makepanggilan berturut-turut tidak memastikan bahwa Anda akan berakhir dengan matriks yang berdekatan, jadi Anda mungkin memiliki matriks yang terbagi dalam memori. Mari kita pikirkan contoh dengan dua rutinitas Go yang dapat menyebabkan ini:

  1. Rutin # 0 berjalan make([][]int, n)untuk mendapatkan memori yang dialokasikan matrix, mendapatkan bagian memori dari 0x000 hingga 0x07F.
  2. Kemudian, ia memulai pengulangan dan melakukan baris pertama make([]int, m), dari 0x080 ke 0x0FF.
  3. Pada iterasi kedua itu didahului oleh penjadwal.
  4. Penjadwal memberikan prosesor ke rutinitas # 1 dan mulai berjalan. Yang ini juga menggunakan make(untuk tujuannya sendiri) dan berpindah dari 0x100 ke 0x17F (tepat di sebelah baris pertama rutinitas # 0).
  5. Setelah beberapa saat, itu akan didahului dan rutinitas # 0 mulai berjalan kembali.
  6. Itu make([]int, m)sesuai dengan iterasi loop kedua dan beralih dari 0x180 ke 0x1FF untuk baris kedua. Pada titik ini, kami sudah mendapatkan dua baris yang terbagi.

Dengan metode kedua, rutin dilakukan make([]int, n*m)untuk mendapatkan semua matriks dialokasikan dalam satu bagian, memastikan kedekatan. Setelah itu, diperlukan perulangan untuk memperbarui penunjuk matriks ke subleks yang sesuai dengan setiap baris.

Anda dapat bermain dengan kode yang ditunjukkan di atas di Go Playground untuk melihat perbedaan dalam memori yang ditetapkan dengan menggunakan kedua metode tersebut. Perhatikan bahwa saya runtime.Gosched()hanya menggunakan dengan tujuan menghasilkan prosesor dan memaksa penjadwal untuk beralih ke rutinitas lain.

Yang mana yang akan digunakan? Bayangkan kasus terburuk dengan metode pertama, yaitu setiap baris tidak berada di memori berikutnya ke baris lain. Kemudian, jika program Anda melakukan iterasi melalui elemen matriks (untuk membaca atau menulisnya), mungkin akan ada lebih banyak cache yang hilang (karenanya latensi lebih tinggi) dibandingkan dengan metode kedua karena lokalitas data yang lebih buruk. Di sisi lain, dengan metode kedua, tidak mungkin mendapatkan satu bagian memori yang dialokasikan untuk matriks, karena fragmentasi memori (potongan tersebar di seluruh memori), meskipun secara teoritis mungkin ada cukup memori kosong untuk itu. .

Oleh karena itu, kecuali jika ada banyak fragmentasi memori dan matriks yang akan dialokasikan cukup besar, Anda akan selalu ingin menggunakan metode kedua untuk memanfaatkan lokalitas data.

Marcos Canales Mayo
sumber
2
golang.org/doc/effective_go.html#slices menunjukkan cara cerdas untuk melakukan teknik memori bersebelahan dengan memanfaatkan sintaksis slice-native (misalnya tidak perlu secara eksplisit menghitung batas irisan dengan ekspresi seperti (i + 1) * m)
Magnus
0

Dalam jawaban sebelumnya kami tidak mempertimbangkan situasi ketika panjang awalnya tidak diketahui. Untuk kasus ini, Anda dapat menggunakan logika berikut untuk membuat matriks

items := []string{"1.0", "1.0.1", "1.0.2", "1.0.2.1.0"}
mx := make([][]string, 0)
for _, item := range items {
    ind := strings.Count(item, ".")
    for len(mx) < ind+1 {
        mx = append(mx, make([]string, 0))
    }
    mx[ind] = append(mx[ind], item)

}

fmt.Println(mx)

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

Pepatah
sumber
1
Saya tidak yakin apakah ini berada dalam batas OP "cara ringkas", karena dia menyatakan "Saya pikir iterasi melalui setiap potongan untuk menginisialisasi terlalu bertele-tele".
Marcos Canales Mayo