Ejercicio: Exploración de segmentos

Completado

En la sección anterior ha explorado las matrices y ha aprendido que son la base de los segmentos y las asignaciones. Va a entender el porqué en un momento. Al igual que las matrices, un segmento es un tipo de datos de Go que representa una secuencia de elementos del mismo tipo. Pero la diferencia más significativa con respecto a las matrices es que el tamaño de un segmento es dinámico, no fijo.

Un segmento es una estructura de datos sobre una matriz u otro segmento. Nos referimos a la matriz o al segmento de origen como la matriz subyacente. Con un segmento, se puede acceder a toda la matriz subyacente o solo a una subsecuencia de elementos.

Un segmento únicamente tiene tres componentes:

  • Un puntero al primer elemento accesible de la matriz subyacente. Este elemento no es necesariamente el primer elemento de la matriz, array[0].
  • Longitud del segmento. Número de elementos del segmento.
  • Capacidad del segmento. Número de elementos entre el inicio del segmento y el final de la matriz subyacente.

La imagen siguiente representa lo que es un segmento:

Diagrama que muestra el aspecto de los segmentos de Go.

Observe que el segmento solo es un subconjunto de la matriz subyacente. Veamos cómo se puede representar la imagen anterior en código.

Declaración e inicialización de un segmento

La declaración de un segmento se hace lo mismo que la de una matriz. Por ejemplo, el código siguiente representa lo que ha visto en la imagen del segmento:

package main

import "fmt"

func main() {
    months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    fmt.Println(months)
    fmt.Println("Length:", len(months))
    fmt.Println("Capacity:", cap(months))
}

Al ejecutar el código se ve el resultado siguiente:

[January February March April May June July August September October November December]
Length: 12
Capacity: 12

Observe que, de momento, un segmento no difiere mucho de una matriz. Se declaran de la misma manera. Para obtener la información de un segmento se pueden usar las funciones integradas len() y cap(). Vamos a seguir usando estas funciones para confirmar que un segmento puede tener una subsecuencia de elementos a partir de una matriz subyacente.

Elementos de un segmento

Go es compatible con el operador de segmento s[i:p], donde:

  • s representa la matriz.
  • i representa el puntero al primer elemento de la matriz subyacente (u otro segmento) que se agregará al nuevo segmento. La variable i corresponde al elemento situado en la ubicación i del índice en la matriz, array[i]. Recuerde que este elemento no es necesariamente el primer elemento de la matriz subyacente, array[0].
  • p representa el número de elementos de la matriz subyacente que se usará al crear el nuevo segmento, así como la posición del elemento. La variable p corresponde al último elemento de la matriz subyacente que se puede usar en el nuevo segmento. El elemento situado en la posición p de la matriz subyacente se encuentra en la ubicación array[i+1]. Tenga en cuenta que este elemento no es necesariamente el último elemento de la matriz subyacente, array[len(array)-1].

Por tanto, un segmento solo puede hacer referencia a un subconjunto de elementos.

Supongamos que quiere cuatro variables que representen cada trimestre del año y que tiene un segmento months (meses) con 12 elementos. En la imagen siguiente se muestra cómo segmentar months en cuatro segmentos quarter (trimestre):

Diagrama que muestra el aspecto de varios segmentos en Go.

Para representar en código lo que ha visto en la imagen anterior, se podría usar el código siguiente:

package main

import "fmt"

func main() {
    months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    quarter1 := months[0:3]
    quarter2 := months[3:6]
    quarter3 := months[6:9]
    quarter4 := months[9:12]
    fmt.Println(quarter1, len(quarter1), cap(quarter1))
    fmt.Println(quarter2, len(quarter2), cap(quarter2))
    fmt.Println(quarter3, len(quarter3), cap(quarter3))
    fmt.Println(quarter4, len(quarter4), cap(quarter4))
}

Al ejecutar el código, se obtiene el resultado siguiente:

[January February March] 3 12
[April May June] 3 9
[July August September] 3 6
[October November December] 3 3

Observe que la longitud de los segmentos es la misma, pero la capacidad es diferente. Vamos a explorar el segmento quarter2. Al declarar este segmento, está indicando que quiere que el segmento se inicie en la posición número tres y que el último elemento se encuentre en la posición número seis. La longitud del segmento es de tres elementos, pero la capacidad es de nueve, porque la matriz subyacente tiene más elementos o posiciones disponibles, aunque no visibles, para el segmento. Por ejemplo, si intenta imprimir algo como fmt.Println(quarter2[3]), obtiene el siguiente error: panic: runtime error: index out of range [3] with length 3.

La capacidad de un segmento solo indica cuánto se puede extender. Por esta razón, podría crear un segmento extendido de quarter2, como en este ejemplo:

package main

import "fmt"

func main() {
    months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    quarter2 := months[3:6]
    quarter2Extended := quarter2[:4]
    fmt.Println(quarter2, len(quarter2), cap(quarter2))
    fmt.Println(quarter2Extended, len(quarter2Extended), cap(quarter2Extended))
}

Al ejecutar el código anterior, se obtiene el siguiente resultado:

[April May June] 3 9
[April May June July] 4 9

Observe que al declarar la variable quarter2Extended no hay que especificar la posición inicial ([:4]). Cuando se hace, Go da por hecho que quiere la primera posición del segmento. Puede hacer lo mismo con la última posición ([1:]). Go da por hecho que quiere hacer referencia a todos los elementos hasta la última posición de un segmento (len()-1).

Anexión de elementos

Hemos visto cómo funcionan los segmentos y en qué se parecen a las matrices. Ahora vamos a descubrir en qué se diferencian de ellas. La primera diferencia es que el tamaño de un segmento no es fijo, sino dinámico. Después de crear un segmento, puede agregarle más elementos, con lo que se extiende. En un momento vamos a ver lo que sucede con la matriz subyacente.

Para agregar un elemento a un segmento, Go ofrece la función integrada append(slice, element). Debe pasar el segmento que quiere modificar y el elemento que quiere anexar como valores a la función. Luego, la función append devuelve un nuevo segmento que debe almacenar en una variable. Podría ser la misma variable para el segmento que se está cambiando.

Veamos el aspecto del proceso de anexión en el código:

package main

import "fmt"

func main() {
    var numbers []int
    for i := 0; i < 10; i++ {
        numbers = append(numbers, i)
        fmt.Printf("%d\tcap=%d\t%v\n", i, cap(numbers), numbers)
    }
}

Al ejecutar el código anterior debería ver el siguiente resultado:

0       cap=1   [0]
1       cap=2   [0 1]
2       cap=4   [0 1 2]
3       cap=4   [0 1 2 3]
4       cap=8   [0 1 2 3 4]
5       cap=8   [0 1 2 3 4 5]
6       cap=8   [0 1 2 3 4 5 6]
7       cap=8   [0 1 2 3 4 5 6 7]
8       cap=16  [0 1 2 3 4 5 6 7 8]
9       cap=16  [0 1 2 3 4 5 6 7 8 9]

Este resultado es interesante. Especialmente por lo que devuelve la llamada a la función cap(). Todo parece normal hasta la tercera iteración, donde la capacidad cambia a 4 y solo hay tres elementos en el segmento. En la quinta iteración la capacidad varía de nuevo a 8 y, en la novena, a 16.

¿Observa un patrón en el resultado de la capacidad? Cuando un segmento no tiene suficiente capacidad para contener más elementos, Go la duplica. Es decir, crea una nueva matriz subyacente con la nueva capacidad. No es necesario hacer nada para que se produzca este aumento de capacidad. Go lo hace automáticamente. Debe tener cuidado. En algún momento, un segmento podría tener más capacidad de la que necesita, con lo que se estaría desperdiciando memoria.

Quitar elementos

Es posible que se esté preguntando por la eliminación de elementos. Go no tiene ninguna función integrada para quitar elementos de un segmento. Puede usar el operador de segmento s[i:p] que hemos visto antes para crear un nuevo segmento que contenga solo los elementos necesarios.

Por ejemplo, el código siguiente quita un elemento de un segmento:

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    remove := 2

	if remove < len(letters) {

		fmt.Println("Before", letters, "Remove ", letters[remove])

		letters = append(letters[:remove], letters[remove+1:]...)

		fmt.Println("After", letters)
	}

}

Al ejecutar el código anterior, se obtiene el siguiente resultado:

Before [A B C D E] Remove  C
After [A B D E]

Este código quita un elemento de un segmento. Para ello, reemplaza el elemento que se quiere quitar por el siguiente elemento del segmento, o bien por ninguno si el elemento que se va a quitar es el último.

Otro enfoque podría ser crear una nueva copia del segmento. Veremos cómo realizar copias de segmentos en la siguiente sección.

Creación de copias de segmentos

Go tiene una función integrada copy(dst, src []Type) para crear copias de un segmento. Hay que enviar el segmento de destino y el de origen. Por ejemplo, podría crear una copia de un segmento como se muestra en este ejemplo:

slice2 := make([]string, 3)
copy(slice2, letters[1:4])

¿Por qué debería importarle cómo crear copias? Al cambiar un elemento de un segmento, también se está cambiando la matriz subyacente. Cualquier otro segmento que haga referencia a la misma matriz subyacente se va a ver afectado. Veamos este proceso en el código; luego vamos a corregirlo mediante la creación de una copia de un segmento.

Use el código siguiente para confirmar que un segmento apunta a una matriz y que cada cambio que se realice en un segmento va a afectar a la matriz subyacente.

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    fmt.Println("Before", letters)

    slice1 := letters[0:2]
    slice2 := letters[1:4]

    slice1[1] = "Z"

    fmt.Println("After", letters)
    fmt.Println("Slice2", slice2)
}

Al ejecutar el código anterior se ve el siguiente resultado:

Before [A B C D E]
After [A Z C D E]
Slice2 [Z C D]

Observe cómo el cambio realizado en slice1 ha afectado a la matriz letters y a slice2. En el resultado, puede ver que la letra B se ha reemplazado por Z y que eso afecta a todo lo que apunta a la matriz letters.

Para corregir este problema, debe crear una copia del segmento, lo que, por debajo, crea una nueva matriz subyacente. Puede usar el código siguiente:

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    fmt.Println("Before", letters)

    slice1 := letters[0:2]

    slice2 := make([]string, 3)
    copy(slice2, letters[1:4])

    slice1[1] = "Z"

    fmt.Println("After", letters)
    fmt.Println("Slice2", slice2)
}

Al ejecutar el código anterior se ve el siguiente resultado:

Before [A B C D E]
After [A Z C D E]
Slice2 [B C D]

Observe cómo el cambio en slice1 ha afectado a la matriz subyacente, pero no al nuevo slice2.