Qual è un modo conciso per creare una sezione 2D in Go?


103

Sto imparando il Go eseguendo A Tour of Go . Uno degli esercizi lì mi chiede di creare una sezione 2D di dyrighe e dxcolonne che contengono uint8. Il mio attuale approccio, che funziona, è questo:

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
}

Penso che l'iterazione di ogni slice per inizializzarla sia troppo prolissa. E se la sezione avesse più dimensioni, il codice diventerebbe ingombrante. Esiste un modo conciso per inizializzare sezioni 2D (o n-dimensionali) in Go?

Risposte:


148

Non esiste un modo più conciso, quello che hai fatto è il modo "giusto"; perché le fette sono sempre unidimensionali ma possono essere composte per costruire oggetti di dimensioni superiori. Vedi questa domanda per maggiori dettagli: Vai: Com'è la rappresentazione della memoria dell'array bidimensionale .

Una cosa che puoi semplificare è usare il for rangecostrutto:

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

Si noti inoltre che se si inizializza la sezione con un letterale composto , si ottiene "gratuitamente", ad esempio:

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

Sì, questo ha i suoi limiti poiché apparentemente devi enumerare tutti gli elementi; ma ci sono alcuni trucchi, vale a dire non devi enumerare tutti i valori, solo quelli che non sono i valori zero del tipo di elemento della fetta. Per ulteriori dettagli su questo argomento , vedere Elementi con chiave nell'inizializzazione dell'array golang .

Ad esempio, se vuoi una fetta in cui i primi 10 elementi sono zeri, e poi segue 1e 2, può essere creata in questo modo:

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

Si noti inoltre che se si utilizzano array invece di slice , è possibile crearli molto facilmente:

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

L'output è:

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

In caso di array non è necessario iterare sull'array "esterno" e inizializzare gli array "interni", poiché gli array non sono descrittori ma valori. Vedi il post del blog Array, slice (e stringhe): The Mechanics of 'append' per maggiori dettagli.

Prova gli esempi su Go Playground .


Poiché l'utilizzo di un array semplifica il codice, mi piacerebbe farlo. Come si specifica che in una struttura? Ottengo cannot use [5][2]string literal (type [5][2]string) as type [][]string in field valuequando provo ad assegnare l'array a ciò che immagino di dire a Go è uno slice.
Eric Lindsey

L'ho capito da solo e ho modificato la risposta per aggiungere le informazioni.
Eric Lindsey

1
@EricLindsey Sebbene la tua modifica sia buona, la rifiuterò comunque perché non voglio incoraggiare l'uso di array solo perché l'inizializzazione è più semplice. In Go, gli array sono secondari, le slice sono la strada da percorrere. Per i dettagli, vedi Qual è il modo più veloce per aggiungere un array a un altro in Go? Anche gli array hanno la loro posizione, per i dettagli, vedere Perché gli array in Go?
icza

abbastanza giusto, ma credo che l'informazione abbia ancora valore. Quello che stavo cercando di spiegare con la mia modifica era che se hai bisogno della flessibilità di dimensioni diverse tra gli oggetti, le fette sono la strada da percorrere. D'altra parte, se le tue informazioni sono strutturate in modo rigido e saranno sempre le stesse, gli array non solo sono più facili da inizializzare, ma sono anche più efficienti. Come potrei migliorare la modifica?
Eric Lindsey,

@EricLindsey Vedo che hai apportato un'altra modifica che è stata già rifiutata da altri. Nella tua modifica stavi dicendo di usare gli array per avere un accesso più veloce agli elementi. Nota che Go ottimizza molte cose, e questo potrebbe non essere il caso, le sezioni potrebbero essere altrettanto veloci. Per i dettagli, vedere Array vs Slice: accesso alla velocità .
icza

12

Esistono due modi per utilizzare le sezioni per creare una matrice. Diamo un'occhiata alle differenze tra loro.

Primo metodo:

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

Secondo metodo:

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

Per quanto riguarda il primo metodo, l'esecuzione di makechiamate successive non garantisce che si finirà con una matrice contigua, quindi la matrice potrebbe essere divisa in memoria. Pensiamo a un esempio con due routine Go che potrebbero causare questo:

  1. La routine # 0 viene eseguita make([][]int, n)per ottenere la memoria allocata matrix, ottenendo un pezzo di memoria da 0x000 a 0x07F.
  2. Quindi, avvia il ciclo e fa la prima riga make([]int, m), passando da 0x080 a 0x0FF.
  3. Nella seconda iterazione viene anticipata dallo scheduler.
  4. Lo scheduler assegna al processore la routine n. 1 e inizia a funzionare. Anche questo usa make(per i propri scopi) e va da 0x100 a 0x17F (proprio accanto alla prima riga della routine # 0).
  5. Dopo un po ', viene interrotto e la routine # 0 ricomincia a funzionare.
  6. make([]int, m)Esegue la corrispondente iterazione del secondo ciclo e passa da 0x180 a 0x1FF per la seconda riga. A questo punto, abbiamo già due righe divise.

Con il secondo metodo, la routine fa make([]int, n*m)per ottenere tutta la matrice allocata in una singola fetta, garantendo la contiguità. Dopodiché, è necessario un ciclo per aggiornare i puntatori della matrice alle sottosezioni corrispondenti a ciascuna riga.

Puoi giocare con il codice mostrato sopra nel Go Playground per vedere la differenza nella memoria assegnata utilizzando entrambi i metodi. Si noti che l'ho usato runtime.Gosched()solo con lo scopo di cedere il processore e costringere lo scheduler a passare a un'altra routine.

Quale usare? Immagina il caso peggiore con il primo metodo, cioè ogni riga non è successiva in memoria a un'altra riga. Quindi, se il tuo programma itera attraverso gli elementi della matrice (per leggerli o scriverli), ci saranno probabilmente più cache miss (quindi maggiore latenza) rispetto al secondo metodo a causa della peggiore localizzazione dei dati. D'altra parte, con il secondo metodo potrebbe non essere possibile ottenere un singolo pezzo di memoria allocato per la matrice, a causa della frammentazione della memoria (blocchi sparsi per tutta la memoria), anche se teoricamente potrebbe esserci abbastanza memoria libera per esso .

Pertanto, a meno che non ci sia molta frammentazione della memoria e la matrice da allocare sia abbastanza grande, si vorrà sempre utilizzare il secondo metodo per ottenere il vantaggio della località dei dati.


2
golang.org/doc/effective_go.html#slices mostra un modo intelligente per eseguire la tecnica della memoria contigua sfruttando la sintassi nativa della sezione (ad esempio, non è necessario calcolare esplicitamente i limiti delle sezioni con espressioni come (i + 1) * m)
Magnus

0

Nelle risposte precedenti non abbiamo considerato la situazione in cui la lunghezza iniziale è sconosciuta. In questo caso è possibile utilizzare la seguente logica per creare la matrice

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
Non sono sicuro che questo sia entro i limiti dell'OP di "modo conciso", come ha affermato "Penso che l'iterazione attraverso ogni fetta per inizializzarla sia troppo prolissa".
Marcos Canales Mayo
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.