J'apprends à aller en passant par n tour de Go . Un des exercices me demande de créer une tranche 2D de dy
rangées et dx
colonnes contenant uint8
. Mon approche actuelle, qui fonctionne, est la suivante:
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
}
Je pense que parcourir chaque tranche pour l'initialiser est trop détaillé. Et si la tranche avait plus de dimensions, le code deviendrait difficile à manier. Existe-t-il un moyen concis d'initialiser des tranches 2D (ou n dimensions) dans Go?
Il n'y a pas de manière plus concise, ce que vous avez fait est la "bonne" façon; parce que les tranches sont toujours unidimensionnelles mais peuvent être composées pour construire des objets de dimension supérieure. Voir cette question pour plus de détails: Aller: Comment est la représentation en mémoire du tableau à deux dimensions .
Une chose que vous pouvez simplifier consiste à utiliser la construction for range
:
a := make([][]uint8, dy)
for i := range a {
a[i] = make([]uint8, dx)
}
Notez également que si vous initialisez votre tranche avec un littéral composite , vous l'obtenez pour "gratuit", par exemple:
a := [][]uint8{
{0, 1, 2, 3},
{4, 5, 6, 7},
}
fmt.Println(a) // Output is [[0 1 2 3] [4 5 6 7]]
Oui, cela a ses limites car vous devez apparemment énumérer tous les éléments; mais il y a quelques astuces, à savoir qu'il n'est pas nécessaire d'énumérer toutes les valeurs, mais uniquement celles qui ne sont pas les valeurs zéro du type d'élément de la tranche. Pour plus de détails à ce sujet, voir Eléments à clé dans l'initialisation du tableau golang .
Par exemple, si vous voulez une tranche où les 10 premiers éléments sont des zéros et que vous suivez ensuite 1
et 2
, vous pouvez le créer comme suit:
b := []uint{10: 1, 2}
fmt.Println(b) // Prints [0 0 0 0 0 0 0 0 0 0 1 2]
Notez également que si vous utilisiez tableaux au lieu de tranches , il peut être créé très facilement:
c := [5][5]uint8{}
fmt.Println(c)
La sortie est:
[[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]]
Dans le cas de tableaux, vous n'avez pas besoin de parcourir le tableau "externe" et d'initialiser les tableaux "internes", car les tableaux ne sont pas des descripteurs, mais des valeurs. Voir l'article de blog Tableaux, tranches (et chaînes): la mécanique de 'append' pour plus de détails.
Essayez les exemples sur le Go Playground .
Il existe deux manières d'utiliser des tranches pour créer une matrice. Jetons un coup d'oeil aux différences entre eux.
Première méthode:
matrix := make([][]int, n)
for i := 0; i < n; i++ {
matrix[i] = make([]int, m)
}
Deuxième méthode:
matrix := make([][]int, n)
rows := make([]int, n*m)
for i := 0; i < n; i++ {
matrix[i] = rows[i*m : (i+1)*m]
}
En ce qui concerne la première méthode, passer des appels make
successifs ne garantit pas que vous obtiendrez une matrice contiguë. Vous pouvez donc diviser la matrice en mémoire. Pensons à un exemple avec deux routines Go pouvant causer ceci:
make([][]int, n)
pour obtenir la mémoire allouée pour matrix
, en obtenant un morceau de mémoire de 0x000 à 0x07F.make([]int, m)
, en passant de 0x080 à 0x0FF.make
(pour ses propres besoins) et passe de 0x100 à 0x17F (juste à côté de la première ligne de la routine # 0).make([]int, m)
correspondant à la deuxième itération de la boucle et passe de 0x180 à 0x1FF pour la deuxième ligne. À ce stade, nous avons déjà deux lignes divisées.Avec la deuxième méthode, la routine fait make([]int, n*m)
pour obtenir toute la matrice allouée dans une seule tranche, assurant ainsi la contiguïté. Après cela, une boucle est nécessaire pour mettre à jour les pointeurs de la matrice vers les sous-divisions correspondant à chaque ligne.
Vous pouvez jouer avec le code indiqué ci-dessus dans le Go Playground pour voir la différence de mémoire allouée en utilisant les deux méthodes. Notez que j’ai utilisé runtime.Gosched()
uniquement dans le but de céder le processeur et d’obliger le planificateur à passer à une autre routine.
Lequel utiliser? Imaginez le pire des cas avec la première méthode, c’est-à-dire que chaque ligne n’est pas la suivante en mémoire d’une autre. Ensuite, si votre programme parcourt les éléments de la matrice (pour les lire ou les écrire), il y aura probablement plus d'erreurs dans le cache (donc une latence plus élevée) par rapport à la seconde méthode en raison de la pire localisation des données. D'autre part, avec la seconde méthode, il peut ne pas être possible d'attribuer une seule pièce de mémoire à la matrice, en raison de la fragmentation de la mémoire (des fragments répartis dans toute la mémoire), même si, en théorie, il peut y avoir suffisamment de mémoire libre. .
Par conséquent, sauf si la fragmentation de la mémoire est importante et si la matrice à allouer est suffisamment énorme, vous voudrez toujours utiliser la seconde méthode pour tirer parti de la localisation des données.
Vous pouvez vous référer à ce morceau de code -
package main
import "fmt"
func main() {
var row, col int
fmt.Print("enter rows cols: ")
fmt.Scan(&row, &col)
// allocate composed 2d array
a := make([][]int, row)
for i := range a {
a[i] = make([]int, col)
}
// array elements initialized to 0
fmt.Println("a[0][0] =", a[0][0])
// assign
a[row-1][col-1] = 7
// retrieve
fmt.Printf("a[%d][%d] = %d\n", row-1, col-1, a[row-1][col-1])
// remove only reference
a = nil
// memory allocated earlier with make can now be garbage collected.
}