Який лаконічний спосіб створити 2D-фрагмент в Go?


103

Я навчаюсь Go, пройшовши екскурсію Go . Одна з вправ там просить мене створити 2D фрагмент dyрядків і dxстовпців, що містять uint8. Мій сучасний підхід, який працює, такий:

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
}

Я думаю, що повторення через кожен фрагмент для його ініціалізації занадто багатослівне. І якби фрагмент мав більше розмірів, код став би непростим. Чи є лаконічний спосіб ініціалізації 2D (або n-мірних) фрагментів у Go?

Відповіді:


148

Не існує більш стислого способу: те, що ви зробили, це "правильний" шлях; оскільки фрагменти завжди є одновимірними, але можуть складатися для побудови об'єктів більш високого розміру. Детальніше див. У цьому питанні: Перейдіть: Як представлено пам'ять двовимірного масиву .

Одне з них можна спростити, це використовувати for rangeконструкцію:

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

Також зауважте, що якщо ви ініціалізуєте свій фрагмент складеним буквалом , ви отримуєте це для "безкоштовно", наприклад:

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

Так, це має свої межі, оскільки, здавалося б, вам потрібно перерахувати всі елементи; але є деякі хитрощі, а саме вам не доведеться перераховувати всі значення, лише ті, які не є нульовими значеннями типу елемента фрагмента. Детальніше про це див. У розділі Ключові елементи в ініціалізації масиву golang .

Наприклад, якщо ви хочете фрагмент, де перші 10 елементів є нулями, а потім слід, 1і 2його можна створити так:

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

Також зауважте, що якщо ви використовуєте масиви замість фрагментів , їх можна створити дуже легко:

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

Вихід:

[[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]]

У разі масивів вам не доведеться повторювати "зовнішній" масив та ініціалізувати "внутрішні" масиви, оскільки масиви - це не дескриптори, а значення. Дивіться публікацію в блозі Масиви, фрагменти (та рядки): Механіка "додавання" для отримання більш детальної інформації.

Спробуйте приклади на Go Playground .


Оскільки використання масиву спрощує код, я хотів би це зробити. Як можна вказати це в структурі? Я отримую, cannot use [5][2]string literal (type [5][2]string) as type [][]string in field valueколи намагаюся призначити масив тому, що, напевно, кажу, що Go - це фрагмент.
Ерік Ліндсі

Я зрозумів це сам і відредагував відповідь, щоб додати інформацію.
Ерік Ліндсі

1
@EricLindsey Хоча ваше редагування добре, я все одно відхилю його, оскільки я не хочу заохочувати використання масивів лише тому, що ініціалізація простіша. У Go, масиви є вторинними, фрагменти - це шлях. Докладніше див. Який найшвидший спосіб додати один масив до іншого в Go? Масиви теж мають свої місця, детальніше див. Чому масиви в Go?
icza

досить справедливо, але я вважаю, що інформація все ж має заслугу. Що я намагався пояснити своїм редагуванням, це те, що якщо вам потрібна гнучкість різних розмірів між об’єктами, тоді фрагменти - це шлях. З іншого боку, якщо ваша інформація жорстко структурована і завжди буде однаковою, то масиви не тільки простіше ініціалізувати, вони також є більш ефективними. Як я міг покращити редагування?
Ерік Ліндсі

@EricLindsey Я бачу, що ви зробили ще одну редакцію, яку інші люди вже відхилили. У своїй редакції ви казали використовувати масиви для швидшого доступу до елементів. Зауважте, що Go оптимізує багато речей, і це може бути не так, скибочки можуть бути настільки ж швидкими. Детальніше дивіться у розділі Array vs Slice: швидкість доступу .
icza

12

Існує два способи використання фрагментів для створення матриці. Давайте розглянемо відмінності між ними.

Перший метод:

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

Другий метод:

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

Що стосується першого методу, то здійснення послідовних makeвикликів не гарантує, що ви отримаєте суміжну матрицю, тому у вас може бути матриця розділена в пам'яті. Давайте придумаємо приклад з двома рутинами Go, які можуть спричинити це:

  1. Підпрограма № 0 працює make([][]int, n)для отримання виділеної пам’яті для matrixотримання фрагмента пам'яті від 0x000 до 0x07F.
  2. Потім він запускає цикл і виконує перший ряд make([]int, m), отримуючи від 0x080 до 0x0FF.
  3. Під час другої ітерації планувальник випереджає його.
  4. Планувальник надає процесору режим 1 і він починає працювати. Цей також використовує make(для власних цілей) і отримує від 0x100 до 0x17F (безпосередньо біля першого рядка процедури # 0).
  5. Через деякий час він отримує попередження, і рутина № 0 починає працювати знову.
  6. Він виконує make([]int, m)відповідну другу ітерацію циклу і отримує від 0x180 до 0x1FF для другого ряду. На цьому етапі ми вже отримали два розділені ряди.

Під час другого методу звичайна програма make([]int, n*m)отримує всю матрицю, виділену в одному фрагменті, забезпечуючи сумісність. Після цього потрібен цикл для оновлення покажчиків матриці на субліски, відповідні кожному рядку.

Ви можете пограти з кодом, показаним вище на майданчику Go, щоб побачити різницю в пам'яті, призначеній за допомогою обох методів. Зауважте, що я використовував runtime.Gosched()лише для того, щоб дати процесор і змусити планувальник перейти на іншу процедуру.

Який використовувати? Уявіть найгірший випадок із першим методом, тобто кожен рядок не є наступним у пам'яті до іншого рядка. Тоді, якщо ваша програма повторює елементи матриці (читати чи записувати їх), ймовірно, буде більше пропусків кешу (отже, більша затримка) порівняно з другим методом через гіршу локальність даних. З іншого боку, за допомогою другого методу неможливо отримати жоден фрагмент пам’яті, виділений для матриці, через фрагментацію пам’яті (шматки, розповсюджені по всій пам’яті), хоча теоретично для неї може бути достатньо вільної пам’яті. .

Тому, якщо не дуже багато фрагментації пам'яті і матриця, яка буде виділена, є достатньо величезною, ви завжди хочете скористатися другим методом, щоб отримати перевагу локальності даних.


2
golang.org/doc/effective_go.html#slices показує розумний спосіб зробити техніку безперервної пам’яті, використовуючи синтаксис нарізного синтаксису (наприклад, не потрібно чітко обчислювати межі фрагментів з виразами типу (i + 1) * m)
Magnus

0

У попередніх відповідях ми не розглядали ситуацію, коли початкова довжина невідома. У цьому випадку ви можете використовувати наступну логіку для створення матриці

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


1
Я не впевнений, чи це в межах ОП "стисло", тому що він заявив, "я вважаю, що повторення через кожен фрагмент для його ініціалізації занадто багатослівне".
Marcos Canales Mayo
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.