¿Por qué las listas se utilizan con poca frecuencia en Go?

82

Soy nuevo en Go y estoy muy emocionado por ello. Pero, en todos los lenguajes con los que he trabajado extensamente: Delphi, C #, C ++, Python: las listas son muy importantes porque se pueden cambiar de tamaño dinámicamente, a diferencia de las matrices.

En Golang, de hecho, hay una list.Listestructura, pero veo muy poca documentación al respecto, ya sea en Go By Example o en los tres libros de Go que tengo (Summerfield, Chisnal y Balbaert), todos pasan mucho tiempo en matrices y rebanadas y luego salte a mapas. En los ejemplos de código fuente también encuentro poco o ningún uso de list.List.

También parece que, a diferencia de Python, Rangeno es compatible con List, un gran inconveniente en mi opinión. ¿Me estoy perdiendo de algo?

Las rebanadas son ciertamente agradables, pero aún deben basarse en una matriz con un tamaño codificado de forma rígida. Ahí es donde entra List. ¿Hay alguna manera de crear una matriz / segmento en Go sin un tamaño de matriz codificado? ¿Por qué se ignora List?

Vector
fuente
9
Tenga en cuenta que el listtipo de Python no se implementa mediante una lista vinculada: se comporta de manera similar a un segmento de Go, y en ocasiones requiere que las copias de datos se expandan.
James Henstridge
@JamesHenstridge - debidamente anotado y corregido.
Vector
2
C ++ no usa listas de manera extensiva. std::listcasi siempre es una mala idea. std::vectores lo que desea administrar una secuencia de elementos. Por las mismas razones, std::vectorse prefiere, también se prefiere el segmento Go.
deft_code
@deft_code - entendido. En mi pregunta std::vector<T>se incluyó en la listcategoría porque no requiere un valor constante para la inicialización y se puede cambiar de tamaño dinámicamente. Cuando hice la pregunta, no me quedó claro que Go's slicepudiera usarse de manera similar; todo lo que leí en ese momento explicaba que un segmento era una "vista de una matriz" y, como en la mayoría de los otros idiomas, matrices simples de vainilla en Go deben declararse con un tamaño constante. (Pero gracias por el aviso.)
Vector

Respuestas:

83

Casi siempre, cuando esté pensando en una lista, utilice una porción en Go. Las rebanadas se redimensionan dinámicamente. Debajo de ellos hay una porción de memoria contigua que puede cambiar de tamaño.

Son muy flexibles, como verá si lee la página wiki de SliceTricks .

Aquí hay un extracto:

Copiar

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

Cortar

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

Eliminar

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

Eliminar sin conservar el orden

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

Popular

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

empujar

a = append(a, x)

Actualización : aquí hay un enlace a una publicación de blog sobre las secciones del propio equipo de Go, que explica muy bien la relación entre las secciones y las matrices y las partes internas de las secciones.

Nick Craig-Wood
fuente
1
OK, esto es lo que estaba buscando. Tuve un malentendido sobre las rebanadas. No es necesario declarar una matriz para usar un segmento. Puede asignar un segmento y eso asigna la tienda de respaldo. Suena similar a las transmisiones en Delphi o C ++. Ahora entiendo por qué tanto alboroto sobre las rebanadas.
Vector
2
@ComeAndGo, tenga en cuenta que a veces crear un segmento que apunte a una matriz "estática" es un modismo útil.
kostix
2
@FelikZ, los cortes crean "una vista" en su matriz de respaldo. A menudo, sabe de antemano que los datos en los que operará una función tendrán un tamaño fijo (o tendrán un tamaño no mayor que una cantidad conocida de bytes; esto es bastante común para los protocolos de red). Por lo tanto, puede declarar una matriz para contener estos datos en su función y luego
dividirlos como desee
52

Hice esta pregunta hace unos meses, cuando comencé a investigar Go. Desde entonces, todos los días he estado leyendo sobre Go y codificando en Go.

Debido a que no recibí una respuesta clara a esta pregunta (aunque había aceptado una respuesta), ahora la responderé yo mismo, basándome en lo que he aprendido, desde que la hice:

¿Hay alguna forma de crear una matriz / segmento en Go sin un tamaño de matriz codificado de forma rígida?

Si. Las rebanadas no requieren una matriz codificada slicedesde:

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

Este código asigna un segmento slde tamaño lencon una capacidad de cap- leny capson variables que se pueden asignar en tiempo de ejecución.

¿Por qué se list.Listignora?

Parece que las principales razones que list.Listparecen recibir poca atención en Go son:

  • Como se explicó en la respuesta de @Nick Craig-Wood, no hay prácticamente nada que se pueda hacer con listas que no se pueda hacer con porciones, a menudo de manera más eficiente y con una sintaxis más limpia y elegante. Por ejemplo, la construcción de rango:

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

    no se puede usar con la lista; se requiere un estilo C para el bucle. Y en muchos casos, la sintaxis de estilo de colección de C ++ debe usarse con listas: push_backetc.

  • Quizás lo más importante list.Listes que no está muy tipado, es muy similar a las listas y diccionarios de Python, que permiten mezclar varios tipos en la colección. Esto parece ir en contra del enfoque Go de las cosas. Go es un lenguaje muy tipado; por ejemplo, las conversiones de tipo implícitas nunca se permiten en Go, incluso un upCast de inta int64debe ser explícito. Pero todos los métodos para list.List toman interfaces vacías, todo vale.

    Una de las razones por las que abandoné Python y me mudé a Go es por este tipo de debilidad en el sistema de tipos de Python, aunque Python afirma estar "fuertemente tipado" (IMO no lo es). Go's list.Listparece ser una especie de "mestizo", nacido de C ++ vector<T>y Python List(), y quizás esté un poco fuera de lugar en Go.

No me sorprendería si en algún momento en un futuro no muy lejano, encontráramos list.List obsoleto en Go, aunque tal vez permanecerá, para dar cabida a esas situaciones raras donde, incluso utilizando buenas prácticas de diseño, un problema se puede resolver mejor con una colección que contiene varios tipos. O tal vez esté ahí para proporcionar un "puente" para que los desarrolladores de la familia C se sientan cómodos con Go antes de que aprendan los matices de los cortes, que son exclusivos de Go, AFAIK. (En algunos aspectos, los segmentos parecen similares a las clases de flujo en C ++ o Delphi, pero no del todo).

Aunque provengo de una experiencia en Delphi / C ++ / Python, en mi exposición inicial a Go encontré list.Listque era más familiar que los segmentos de Go, ya que me he sentido más cómodo con Go, volví y cambié todas mis listas a segmentos. Todavía no he encontrado nada que slicey / o mapno me permita hacer, por lo que necesito usar list.List.

Vector
fuente
@Alok Go es un lenguaje de propósito general diseñado con la programación de sistemas en mente. Está fuertemente tipado ... - ¿Tampoco tienen idea de lo que están hablando? El uso de la inferencia de tipo no significa que GoLang no esté fuertemente tipado. También di una ilustración clara de este punto: las conversiones de tipo implícitas no están permitidas en GoLang, incluso cuando se están realizando versiones. (Los signos de exclamación no te hacen más correcto. Guárdalos para blogs para niños.)
Vector
@Alok: los mods eliminaron tu comentario, no yo. Simplemente decir que alguien "no sabe de lo que está hablando". es inútil a menos que proporcione una explicación y prueba. Además, se supone que este es un lugar profesional, por lo que podemos omitir los signos de exclamación y la hipérbole; guárdelos para blogs para niños. Si tiene un problema, simplemente diga "No veo cómo puede decir que GoLang está tan fuertemente tipado cuando tenemos A, B y C que parecen contradecir eso". Quizás el OP esté de acuerdo o explique por qué creen que estás equivocado. Sería un comentario útil y que suena profesional.
Vector
4
Lenguaje verificado estáticamente, que hace cumplir algunas reglas antes de que se ejecute el código. Los lenguajes, como C, le brindan un sistema de tipos primitivo: su código puede escribir check correctamente pero explota en tiempo de ejecución. Continúas en este espectro, obtienes Go, que te da mejores garantías que C. Sin embargo, no se acerca a los sistemas de tipos en lenguajes como OCaml (que tampoco es el final del espectro). Decir "Go es quizás el idioma más tipado que existe" es simplemente incorrecto. Es importante que los desarrolladores comprendan las propiedades de seguridad de los diferentes lenguajes para que puedan tomar una decisión informada.
Alok
4
Ejemplos específicos de cosas que faltan en Go: la falta de genéricos te obliga a usar moldes dinámicos. La falta de enumeraciones / capacidad para verificar la completitud del cambio implica verificaciones dinámicas donde otros lenguajes pueden proporcionar garantías estáticas.
Alok
@ Alok-1 I) dijo que quizás 2) Estamos hablando de lenguajes de uso bastante común. Go no es muy fuerte en estos días, pero Go tiene 10545 preguntas etiquetadas, aquí OCaml tiene 3,230. 3) Las deficiencias en Go que citas IMO no tienen mucho que ver con "fuertemente tipado" (un término nebuloso que no necesariamente se correlaciona con verificaciones de tiempo de compilación). 4) "Es importante ..." - lo siento, pero eso no tiene sentido - si alguien está leyendo esto, probablemente ya esté usando Go. Dudo que alguien esté usando esta respuesta para decidir si Go es para ellos. En mi opinión, debería encontrar algo más importante por lo que "preocuparse profundamente" ...
Vector
11

Creo que eso se debe a que no hay mucho que decir sobre ellos, ya que el container/listpaquete se explica por sí mismo una vez que asimila cuál es el idioma principal de Go para trabajar con datos genéricos.

En Delphi (sin genéricos) o en C, almacenaría punteros TObjectos en la lista y luego los devolvería a sus tipos reales al obtenerlos de la lista. En C ++, las listas STL son plantillas y, por lo tanto, están parametrizadas por tipo, y en C # (estos días) las listas son genéricas.

En Go, container/listalmacena valores de tipo interface{}que es un tipo especial capaz de representar valores de cualquier otro tipo (real), almacenando un par de punteros: uno a la información de tipo del valor contenido y un puntero al valor (o el valor directamente, si su tamaño no es mayor que el tamaño de un puntero). Entonces, cuando desee agregar un elemento a la lista, simplemente hágalo como parámetros de función de tipo interface{}aceptar valores de cualquier tipo. Pero cuando extrae valores de la lista, y qué trabajar con sus tipos reales, debe marcarlos a máquina o hacer un cambio de tipo en ellos; ambos enfoques son solo formas diferentes de hacer esencialmente lo mismo.

Aquí hay un ejemplo tomado de aquí :

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))
    }
}

Aquí obtenemos el valor de un elemento usando e.Value()y luego lo afirmamos como intun tipo del valor insertado original.

Puede leer sobre afirmaciones de tipo y cambios de tipo en "Effective Go" o en cualquier otro libro de introducción. La container/listdocumentación del paquete resume todos los métodos compatibles con las listas.

kostix
fuente
Bueno, dado que las listas de Go no actúan como otras listas o vectores: no se pueden indexar (Lista [i]) AFAIK (tal vez me falta algo ...) y tampoco son compatibles con Range, algunas explicaciones estaría en orden. Pero gracias por las afirmaciones / cambios de tipo, eso era algo que me faltaba hasta ahora.
Vector
@ComeAndGo, sí, no admiten rangos porque rangees un lenguaje integrado que solo es aplicable a tipos integrados (matrices, segmentos, cadenas y mapas) porque cada "invocación" o rangede hecho producirá un código de máquina diferente para atravesar el contenedor es aplicado a.
kostix
2
@ComeAndGo, en cuanto a la indexación ... De la documentación del paquete está claro que container/listproporciona una lista de doble enlace . Esto significa que la indexación es una O(N)operación (tiene que comenzar por la cabeza y atravesar cada elemento hacia la cola, contando), y uno de los paradigmas de diseño de piedra angular de Go no tiene costos de rendimiento ocultos; y otro es que poner una pequeña carga adicional en el programador (implementar una función de indexación para una lista de doble enlace es una obviedad de 10 líneas) está bien. Entonces, el contenedor solo implementa operaciones "canónicas" sensibles a su tipo.
kostix
@ComeAndGo, tenga en cuenta que en Delphi TListy otros de su tipo usan una matriz dinámica debajo, por lo que extender dicha lista no es barato mientras que indexarlo es barato. Entonces, mientras que las "listas" de Delphi parecen listas abstractas, de hecho son matrices, para lo que usarías porciones en Go. Lo que quiero resaltar es que Go se esfuerza por dejar las cosas claras sin acumular "hermosas abstracciones" y "ocultar" los detalles al programador. El enfoque de Go es más parecido al de C, donde usted sabe explícitamente cómo se distribuyen sus datos y cómo accede a ellos.
kostix
3
@ComeAndGo, precisamente lo que se puede hacer con los cortes de Go, que tienen tanto longitud como capacidad.
kostix
6

Tenga en cuenta que los cortes de Go se pueden expandir mediante la append()función incorporada. Si bien esto a veces requerirá hacer una copia de la matriz de respaldo, no sucederá siempre, ya que Go sobredimensionará la nueva matriz, lo que le dará una capacidad mayor que la longitud informada. Esto significa que se puede completar una operación de adición posterior sin otra copia de datos.

Si bien termina con más copias de datos que con el código equivalente implementado con listas vinculadas, elimina la necesidad de asignar elementos en la lista individualmente y la necesidad de actualizar los Nextpunteros. Para muchos usos, la implementación basada en matrices proporciona un rendimiento mejor o suficientemente bueno, así que eso es lo que se enfatiza en el lenguaje. Curiosamente, el listtipo estándar de Python también está respaldado por una matriz y tiene características de rendimiento similares al agregar valores.

Dicho esto, hay casos en los que las listas enlazadas son una mejor opción (por ejemplo, cuando necesita insertar o eliminar elementos del principio / medio de una lista larga), y es por eso que se proporciona una implementación de biblioteca estándar. Supongo que no agregaron ninguna característica de lenguaje especial para trabajar con ellos porque estos casos son menos comunes que aquellos en los que se usan porciones.

James Henstridge
fuente
Aún así, los cortes deben estar de vuelta en una matriz con un tamaño codificado, ¿correcto? Eso es lo que no me gusta.
Vector
3
El tamaño de una porción no está codificado en el código fuente del programa, si eso es lo que quiere decir. Se puede expandir dinámicamente a través de la append()operación, como expliqué (que a veces implicará una copia de datos).
James Henstridge
4

A menos que el segmento se actualice con demasiada frecuencia (eliminar, agregar elementos en ubicaciones aleatorias), la contigüidad de los segmentos en la memoria ofrecerá una excelente proporción de aciertos de caché en comparación con las listas vinculadas.

Charla de Scott Meyer sobre la importancia del caché ... https://www.youtube.com/watch?v=WDIkqP4JbkE

Manohar
fuente
4

list.Listse implementa como una lista doblemente enlazada. Las listas basadas en matrices (vectores en C ++ o porciones en golang) son una mejor opción que las listas enlazadas en la mayoría de las condiciones si no inserta con frecuencia en el medio de la lista. La complejidad del tiempo amortizado para añadir es O (1) tanto para la lista de matrices como para la lista vinculada, aunque la lista de matrices tiene que ampliar la capacidad y copiar los valores existentes. Las listas de matrices tienen un acceso aleatorio más rápido, una huella de memoria más pequeña y, lo que es más importante, son amigables para el recolector de basura porque no tienen punteros dentro de la estructura de datos.

carpinterías
fuente
3

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

Depende mucho de la cantidad de elementos en tus listas,
 si una lista real o una porción será más eficiente
 cuando necesita hacer muchas eliminaciones en el "medio" de la lista.

# 1
Cuantos más elementos, menos atractivo se vuelve un corte. 

# 2
Cuando el orden de los elementos no es importante,
 es más eficiente usar una rebanada y
 eliminar un elemento reemplazándolo por el último elemento en el segmento y
 rebanar la rebanada para reducir la longitud en 1
 (como se explica en la wiki de SliceTricks)

Por lo tanto,
use el segmento
1. Si el orden de los elementos en la lista no es importante y necesita eliminarlo, simplemente
use Lista intercambiar el elemento para eliminar con el último elemento y volver a dividirlo en (longitud-1)
2. cuando los elementos sean más ( cualquier otro medio)


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.

Por lo tanto,
use el segmento
1. Si necesita velocidad en el recorrido

Manohar Reddy Poreddy
fuente