web-dev-qa-db-fra.com

Pourquoi les listes sont-elles rarement utilisées dans Go?

Je suis nouveau sur Go et très excité à ce sujet. Mais, dans tous les langages avec lesquels j'ai beaucoup travaillé: Delphi, C #, C++, Python - Les listes sont très importantes car elles peuvent être redimensionnées de façon dynamique, par opposition aux tableaux.

À Golang, il y a bien un list.Liststruct, mais je vois très peu de documentation à ce sujet - que ce soit dans Go By Example ou dans les trois livres Go que j'ai - Summerfield, Chisnal et Balbaert - ils passent tous beaucoup de temps sur des tableaux et des tranches puis passez aux cartes. Dans les exemples de code source, je ne trouve également que peu ou pas d'utilisation de list.List.

Il semble également que, contrairement à Python, Range ne soit pas pris en charge pour List - Big Inconvénient IMO. Est-ce que je manque quelque chose?

Les tranches sont certes agréables, mais elles doivent toujours être basées sur un tableau avec une taille codée en dur. C'est là qu'intervient List. Est-il possible de créer un tableau/une tranche dans Go sans une taille de tableau codée en dur? Pourquoi la liste est-elle ignorée?

61
Vector

J'ai posé cette question il y a quelques mois, lorsque j'ai commencé à enquêter sur Go. Depuis lors, tous les jours, j'ai lu des articles sur Go et codé dans Go.

Comme je n'ai pas reçu de réponse précise à cette question (bien que j'aie accepté une réponse), je vais maintenant y répondre moi-même, en fonction de ce que j'ai appris, depuis que je l'ai posée:

Est-il possible de créer un tableau/une tranche dans Go sans une taille de tableau codée en dur?

Oui. Les tranches ne nécessitent pas de tableau codé en dur pour slice à partir de:

var sl []int = make([]int,len,cap)

Ce code alloue la tranche sl, de taille len d’une capacité de cap - len et cap sont des variables pouvant être affectées à runtime.

Pourquoi list.List Est-il ignoré?

Il semble que les principales raisons pour lesquelles list.List Semblent attirer peu l'attention dans Go sont les suivantes:

  • Comme cela a été expliqué dans la réponse de @Nick Craig-Wood, il n’ya pratiquement rien qui puisse être fait avec des listes qui ne peuvent pas être faites avec des tranches, souvent de manière plus efficace et avec une syntaxe plus propre et plus élégante. Par exemple, la construction de plage:

    for i:=range sl {
      sl[i]=i
    }
    

    ne peut pas être utilisé avec list - un style C pour la boucle est requis. Et dans de nombreux cas, la syntaxe de style de collection C++ doit être utilisée avec les listes: Push_back Etc.

  • Peut-être plus important encore, list.List N'est pas fortement typé - il est très similaire aux listes et dictionnaires de Python, qui permettent de mélanger différents types dans la collection. Cela semble aller à l’encontre de la démarche Go. Go est un langage très typé - par exemple, les conversions de types implicites jamais autorisées dans Go, même un upCast de int à int64 Doit être explicite. Mais toutes les méthodes de list.List prennent des interfaces vides - tout est permis.

    Une des raisons pour lesquelles j'ai abandonné Python et déplacé à Go) est à cause de ce type de faiblesse dans le système de types de Python, bien que Python prétend être "fortement typé" "(IMO ce n’est pas). Le list.List De Go semble être une sorte de" bâtard ", né du vector<T> De C++ et de la List() de Python, et est peut-être un peu hors de propos dans Go lui-même.

Cela ne me surprendrait pas si, à un moment donné dans un avenir assez rapproché, nous trouvions list.List obsolète dans Go, même si cela restera peut-être, pour tenir compte de ces rares situations dans lesquelles, même en utilisant de bonnes pratiques de conception , un problème peut être mieux résolu avec une collection contenant différents types. Ou peut-être est-il là pour fournir un "pont" aux développeurs de la famille C afin qu'ils se familiarisent avec Go avant d'apprendre les nuances des tranches, qui sont uniques à Go, autant que je sache. (À certains égards, les tranches semblent similaires aux classes de flux en C++ ou Delphi, mais pas entièrement.)

Bien que mes origines soient celles de Delphi/C++/Python, lors de mon premier contact avec Go, j’ai trouvé que list.List Était plus familier que les tranches de Go. En étant plus à l’aise avec Go, j’ai changé tous mes listes en tranches. Je n'ai encore rien trouvé que slice et/ou map ne me permettent pas de faire, de sorte que je dois utiliser list.List.

43
Vector

Presque toujours quand vous pensez à une liste - utilisez plutôt une tranche dans Go. Les tranches sont redimensionnées de manière dynamique. Sous-jacente se trouve une tranche de mémoire contiguë qui peut changer de taille.

Ils sont très flexibles, comme vous le verrez si vous lisez la page SliceTricks wiki .

Voici un extrait: -

Copie

b = make([]T, len(a))
copy(b, a) // or b = append([]T(nil), a...)

Couper

a = append(a[:i], a[j:]...)

Supprimer

a = append(a[:i], a[i+1:]...) // or a = a[:i+copy(a[i:], a[i+1:])]

Supprimer sans conserver l'ordre

a[i], a = a[len(a)-1], a[:len(a)-1]

Pop

x, a = a[len(a)-1], a[:len(a)-1]

Pousser

a = append(a, x)

Mise à jour : Voici un lien vers un article de blog sur les tranches de l'équipe de tournage elle-même, qui fait un bon travail de expliquant la relation entre les tranches et les tableaux et les éléments internes des tranches.

67
Nick Craig-Wood

Je pense que c'est parce qu'il n'y a pas grand chose à dire à leur sujet car le paquet container/list Est assez explicite une fois vous avez absorbé ce qui est le principal Aller idiome pour travailler avec des données génériques.

Dans Delphi (sans les génériques) ou en C, vous stockeriez les pointeurs ou TObjects dans la liste, puis les renvoyiez à leur type réel lors de l'obtention de la liste. En C++, les listes STL sont des modèles et donc paramétrés par type, et en C # (ces jours-ci), les listes sont génériques.

Dans Go, container/list Stocke les valeurs de type interface{}, Qui est un type spécial capable de représenter les valeurs de tout autre type (réel) - en stockant une paire de pointeurs: l'un vers le type d'informations du type. valeur contenue et un pointeur sur la valeur (ou la valeur directement, si sa taille n’est pas supérieure à la taille d’un pointeur). Ainsi, lorsque vous souhaitez ajouter un élément à la liste, il vous suffit de le faire en tant que paramètres de fonction de type interface{}. Acceptez les valeurs de tous types. Mais lorsque vous extrayez des valeurs de la liste et que vous devez utiliser leurs types réels, vous devez soit taper-asert ou faire Tapez sur eux - les deux approches ne sont que des façons différentes de faire essentiellement la même chose.

Voici un exemple tiré de here :

package main

import ("fmt" ; "container/list")

func main() {
    var x list.List
    x.PushBack(1)
    x.PushBack(2)
    x.PushBack(3)

    for e := x.Front(); e != nil; e=e.Next() {
        fmt.Println(e.Value.(int))
    }
}

Ici, nous obtenons la valeur d'un élément en utilisant e.Value(), puis nous l'assertons comme int, un type de la valeur insérée d'origine.

Vous pouvez en savoir plus sur les assertions de types et les types dans "Effective Go" ou tout autre livre d'introduction. La documentation du paquet container/list Résume toutes les listes de méthodes prises en charge.

9
kostix

Notez que les tranches Go peuvent être développées via la fonction intégrée append(). Bien que cela nécessite parfois de réaliser une copie du tableau de sauvegarde, cela ne se produira pas à chaque fois, car Go sur-dimensionnera le nouveau tableau en lui donnant une capacité supérieure à la longueur indiquée. Cela signifie qu'une opération d'ajout ultérieure peut être complétée sans une autre copie de données.

Bien que vous obteniez plus de copies de données que de code équivalent implémenté avec des listes chaînées, vous supprimez la nécessité d'allouer des éléments de la liste individuellement et la nécessité de mettre à jour les pointeurs Next. Pour de nombreuses utilisations, l’implémentation basée sur les baies fournit des performances meilleures ou assez bonnes, c’est ce qui est souligné dans le langage. Il est intéressant de noter que le type list standard de Python est également sauvegardé par tableau et présente des caractéristiques de performances similaires lors de l'ajout de valeurs.

Cela dit, il existe des cas où les listes chaînées constituent un meilleur choix (par exemple, lorsque vous devez insérer ou supprimer des éléments à partir du début/du milieu d'une longue liste), et c'est pourquoi une implémentation de bibliothèque standard est fournie. Je suppose qu’ils n’ont pas ajouté de fonctionnalités linguistiques spéciales, car ces cas sont moins courants que ceux où des tranches sont utilisées.

4
James Henstridge

À moins que la tranche ne soit mise à jour trop souvent (supprimer, ajouter des éléments à des emplacements aléatoires), la contiguïté en mémoire des tranches offrira un excellent taux d'accès au cache par rapport aux listes chaînées.

Exposé de Scott Meyer sur l'importance du cache .. https://www.youtube.com/watch?v=WDIkqP4JbkE

3
Manohar

list.List est implémenté comme une liste doublement chaînée. Les listes basées sur des tableaux (vecteurs en C++ ou slices en golang) constituent un meilleur choix que les listes chaînées dans la plupart des cas si vous n'insérez pas fréquemment au milieu de la liste. La complexité temporelle amortie pour append est O(1) pour la liste de tableaux et la liste chaînée, même si la liste de tableaux doit étendre la capacité et copier sur les valeurs existantes. Les listes de tableaux ont un accès aléatoire plus rapide, plus petit encombrement de la mémoire, et surtout, convivial pour le ramasse-miettes en raison de l'absence de pointeurs dans la structure de données.

3
woodings

De: https://groups.google.com/forum/#!msg/golang-nuts/mPKCoYNwsoU/tLefhE7tQjMJ

 
 Cela dépend beaucoup du nombre d'éléments dans vos listes, 
 Si une liste vraie ou une tranche sera plus efficace 
 Lorsque vous devez en faire beaucoup suppressions au "milieu" de la liste. 
 
 # 1 
 Plus le nombre d'éléments est élevé, moins la tranche devient attractive. 
 
 # 2 
 Lorsque l'ordre des éléments n'a pas d'importance, 
, Il est plus efficace d'utiliser une tranche et de supprimer 
 en le remplaçant par le dernier élément de la tranche et par redimensionner la tranche pour réduire la longueur de 1. 
 (comme expliqué dans le wiki SliceTricks) 

Donc
use slice
1. Si l’ordre des éléments de la liste n’est pas important et que vous devez le supprimer,
Utilisez l’échange de liste de l’élément à supprimer avec le dernier élément, puis redimensionnez-le en (longueur-1)
2. quand les éléments sont plus (quels que soient les moyens)


There are ways to mitigate the deletion problem --
e.g. the swap trick you mentioned or
just marking the elements as logically deleted.
But it's impossible to mitigate the problem of slowness of walking linked lists.

Donc
use slice
1. Si vous avez besoin de vitesse en traversée

2