Упражнение. Изучение срезов

Завершено XP: 100

В предыдущем разделе мы изучили массивы и узнали, что они являются основой для срезов и карт. Чуть позже вы поймете, почему. Как и массив, срез — это тип данных в Go, который представляет последовательность элементов одинакового типа. Но существенное отличие срезов от массивов заключается в том, что размер среза является динамическим, а не фиксированным.

Срез — это структура данных, расположенная поверх массива или другого среза. Мы будем называть исходный массив или срез базовым массивом. С помощью среза можно получить доступ ко всему базовому массиву или только к последовательности элементов.

Срез содержит всего три компонента:

  • Указатель на первый достижимый элемент базового массива. Этот элемент не обязательно является первым элементом массива array[0].
  • Длина среза. Число элементов в срезе.
  • Емкость среза. Количество элементов между началом среза и концом базового массива.

Схема среза представлена на следующем рисунке:

Схема, на которой показано, как выглядят срезы в Go.

Обратите внимание, что срез является только подмножеством базового массива. Давайте посмотрим, как можно представить приведенную выше схему в коде.

Объявление и инициализация среза

Срез объявляется точно таким же образом, как и массив. Например, в следующем коде представлен срез, который вы видели на схеме:

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

После выполнения кода вы увидите следующий результат:

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

Обратите внимание, что пока срез не слишком отличается от массива. Срезы и массивы объявляются одинаковым образом. Для получения элементов среза можно использовать встроенные функции len() и cap(). С помощью этих функций мы подтвердим, что срез может иметь последовательность элементов из базового массива.

Элементы среза

Go поддерживает оператор среза s[i:p], в котором:

  • s представляет массив.
  • i представляет указатель на первый элемент базового массива (или другого среза) для добавления в новый срез. Переменная i соответствует элементу в позиции индекса i в массиве array[i]. Помните, что этот элемент не обязательно является первым элементом базового массива array[0].
  • p представляет количество элементов в базовом массиве, используемое при создании нового среза, а также положение элемента. Переменная p соответствует последнему элементу в базовом массиве, который можно использовать в новом срезе. Элемент в позиции p в базовом массиве находится в расположении array[i+1]. Обратите внимание, что этот элемент не обязательно является последним элементом базового массива array[len(array)-1].

Таким образом, срез может ссылаться только на подмножество элементов.

Предположим, что требуются четыре переменные, чтобы представить каждый квартал года, а у вас есть срез months с 12 элементами. На следующем рисунке показано, как разделить срез months на четыре новых среза quarter:

Схема, на которой показаны несколько срезов в Go.

Для представления этой схемы в Go можно использовать следующий код:

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

После выполнения кода вы увидите следующий результат:

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

Обратите внимание, что длина срезов одинакова, но емкость отличается. Давайте рассмотрим срез quarter2. При объявлении этого среза вы указываете, что нужно начать срез с позиции 3, а последний элемент среза находится в позиции 6. Длина среза равна трем элементам, но емкость среза равна девяти элементам, так как базовый массив содержит больше элементов или позиций, которые невидимы для среза. Например, если вы попытаетесь вывести что-то подобное fmt.Println(quarter2[3]), то получите следующую ошибку: panic: runtime error: index out of range [3] with length 3.

Емкость среза показывает, насколько вы можете расширить срез. Поэтому можно создать расширенный срез на основе quarter2, как показано в следующем примере:

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

При выполнении приведенного выше кода получается следующий результат:

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

Обратите внимание, что при объявлении переменной quarter2Extended не нужно указывать начальную позицию ([:4]). Если начальная позиция не указана, Go предполагает, что используется первый элемент среза. То же самое можно сделать для последней позиции ([1:]). В go предполагается, что вы хотите ссылаться на все элементы до последней позиции среза (len()-1).

Добавление элементов

Мы изучили как работают срезы и чем они похожи на массивы. Теперь давайте разберем, чем они отличаются от массивов. Первое отличие заключается в том, что размер среза не является фиксированным, он — динамический. После создания среза к нему можно добавить дополнительные элементы, и срез будет расширен. Чуть позже вы узнаете, что произойдет с базовым массивом при расширении среза.

Для добавления элементов в срез в Go используется встроенная функция append(slice, element). Необходимо передать срез, который требуется изменить, и элемент, который нужно добавить в качестве значений функции. Функция append возвращает новый срез, который нужно сохранить в переменной. Если вы изменяете срез, то новый срез можно сохранить в той же переменной.

Давайте посмотрим, как выглядит процесс добавления в коде:

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

При выполнении приведенного выше кода получается следующий результат:

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]

Эти выходные данные интересны. Особенно с точки зрения того, что возвращает функция cap(). До третьей итерации все выглядит нормально, затем емкость изменяется на 4, хотя в срезе всего три элемента. В пятой итерации емкость снова изменяется на 8, а в девятой — на 16.

Вы заметили закономерность, с которой изменяется емкость? Если емкости среза недостаточно для хранения большего числа элементов, Go удваивает емкость. Он создает новый базовый массив с новой емкостью. Для такого увеличения емкости не нужно ничего делать. Go увеличивает емкость автоматически. Необходимо соблюдать осторожность. В какой-то момент емкость среза может превышать необходимую, что приведет к напрасному использованию памяти.

Удаление элементов

Возможно, вы задаетесь вопросом: как же удалить элементы? В Go нет встроенной функции для удаления элементов из среза. Можно использовать оператор среза s[i:p], рассмотренный ранее, чтобы создать новый срез, включающий только нужные элементы.

Например, следующий код удаляет элемент из среза:

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

}

При выполнении приведенного выше кода получается следующий результат:

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

Код удаляет элемент из среза. Он заменяет элемент, который нужно удалить со следующим элементом в срезе, или не удаляет ничего, если удаляется последний элемент.

Другой подход заключается в создании новой копии среза. Мы научимся создавать копии срезов в следующем разделе.

Создание копий срезов

В Go есть встроенная функция copy(dst, src []Type) для создания копий среза. В качестве аргументов этой функции указываются исходный и целевой срезы. Например, можно создать копию среза, как показано в следующем примере:

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

Зачем нужно создавать копии срезов? При изменении элемента из среза также изменяется и базовый массив. Это повлияет на все остальные срезы, использующие тот же базовый массив. Давайте убедимся в этом на примере кода, а затем исправим этот пример, создав копию среза.

Чтобы убедиться, что срез указывает на массив и каждое изменение среза влияет на базовый массив, воспользуйтесь следующим кодом.

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

При выполнении приведенного выше кода получается следующий результат:

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

Обратите внимание, как изменение среза slice1 повлияло на массив letters и срез slice2. В выходных данных можно увидеть, что буква B была заменена на Z, и это произошло во всех срезах, указывающих на массив letters.

Чтобы устранить эту проблему, необходимо создать копию среза, для которой создается новый базовый массив. Можно использовать следующий код:

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

При выполнении приведенного выше кода получается следующий результат:

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

Обратите внимание, что изменение среза slice1 повлияло на базовый массив, но не повлияло на новый срез slice2.


Следующий урок: Упражнение. Использование карт

Предыдущий Следующая