Перейти к содержимому
Теория · Массивы и срезы · урок 6 из 10

Срез среза

В прошлых уроках срез был окном на ряд шкафчиков. Из одного окна можно выделить окно поменьше, которое смотрит на часть тех же шкафчиков.

Функции часто нужна не вся последовательность, а её часть: первые три результата или всё, кроме первого элемента. Копировать нужные элементы в новый срез по одному долго. Выражение s[i:j] берёт часть среза сразу:

GO
package main

import "fmt"

func main() {
    s := []int{10, 20, 30, 40, 50}
    t := s[1:3]
    fmt.Println(t, len(t))    // [20 30] 2
    fmt.Println(s[:2], s[3:]) // [10 20] [40 50]
}

Программа печатает две строки:

  1. [20 30] 2 — срез t и его длина.
  2. [10 20] [40 50] — начало и конец среза s.

Границы

У выражения среза три формы. Разберём каждую.

Оба индекса: s[i:j]

Выражение s[1:3] состоит из двух границ:

  • 1 — индекс первого элемента, он входит в результат;
  • 3 — индекс, перед которым срез заканчивается, сам он не входит.

Поэтому в t два элемента, и длина среза равна 3 - 1, то есть 2. Если оба индекса равны, срез пустой: s[2:2] печатается как [].

Выход за границы вызывает панику. Для среза []int{1, 2, 3, 4, 5} выражение s[2:9] останавливает программу: slice bounds out of range [:9] with capacity 5.

Без начала: s[:j]

Пропущенное начало означает 0. Поэтому s[:2] — первые два элемента, [10 20].

Без конца: s[i:]

Пропущенный конец означает len(s). Поэтому s[3:] — всё, начиная с индекса 3, то есть [40 50].

Тот же синтаксис работает и для массива: a[1:3] возвращает срез.

Общий массив

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

GO
a := []int{1, 2, 3, 4}
b := a[2:]
b[0] = 7
fmt.Println(a, b) // [1 2 7 4] [7 4]

Элемент b[0] и элемент a[2] — одна и та же ячейка памяти. Поэтому программа печатает [1 2 7 4] [7 4].

Так же ведёт себя срез, переданный в функцию:

  • параметр получает копию ссылки на массив, длины и ёмкости;
  • массив при этом остаётся общим;
  • поэтому запись s[0] = 9 внутри функции видна вызывающему коду.

Ёмкость и append

Этот вопрос часто задают на собеседовании, и в нём есть ловушка с append. Ёмкость среза среза считается от его начала до конца ёмкости исходного среза. Окно можно расширить вправо до конца ряда. Что сделает append, зависит от того, сколько индексов в выражении.

Два индекса: a[1:3]

Для a := []int{1, 2, 3, 4} у b := a[1:3] длина 2, а ёмкость 3. Поэтому b = append(b, 9) не создаёт новый массив, а пишет 9 в шкафчик a[3]. После этого a становится [1 2 3 9].

Нажмите кнопку и посмотрите, какие ячейки a меняются после записи через b и после append.

ВживуюСрез среза и общий массивlen(b) = 2 · cap(b) = 3
a
b := a[1:3]

b := a[1:3] — окно над тем же массивом, что и a.

Следите, какие ячейки a меняются

Три индекса: a[1:3:3]

Ёмкость ограничивают третьим индексом: a[1:3:3] — это полное выражение среза. Разберём его части:

  • 1 — начало, как и раньше;
  • 3 — конец длины: длина равна 3 - 1, то есть 2;
  • третий индекс 3 — конец ёмкости: ёмкость тоже равна 3 - 1, то есть 2.

Окно теперь нельзя расширить вправо: шкафчик a[3] остался за его рамкой. Давайте посмотрим, что сделает append с b := a[1:3:3]:

  1. b — это [2 3] с ёмкостью 2, свободного места в нём нет.
  2. b = append(b, 9) копирует элементы в новый массив и дописывает 9: b становится [2 3 9].
  3. Исходный массив никто не трогал, поэтому a по-прежнему [1 2 3 4].
  4. С этого момента у b свой ряд шкафчиков, и запись b[0] = 7 в a уже не попадёт.

Этот приём выручает, когда вы отдаёте часть среза другому коду. Если тот вызовет append, ваши элементы останутся целы. Третий индекс не может быть больше ёмкости исходного среза, иначе программа остановится с паникой.

Независимая копия

Независимую копию делает встроенная функция copy(dst, src):

  • она копирует элементы из src в dst;
  • возвращает число скопированных элементов;
  • у dst свой базовый массив, поэтому запись в него не видна через src.
GO
src := []int{1, 2, 3}
dst := make([]int, len(src))
n := copy(dst, src)
dst[0] = 9
fmt.Println(n, src, dst) // 3 [1 2 3] [9 2 3]

Здесь аналогия с окнами заканчивается: в жизни вещи из шкафчиков в новый ряд перекладывают руками. copy делает это за вас, но только в шкафчики, которые уже есть в окне dst.

Частая ошибка

Копию делают в пустой срез:

GO
src := []int{1, 2, 3}
var dst []int
fmt.Println(copy(dst, src), dst) // ✗

Программа печатает 0 []. Функция copy не добавляет элементы, а копирует столько элементов, сколько их в более коротком из двух срезов. Длина dst равна нулю, поэтому функция ничего не копирует. Создайте dst через make([]int, len(src)), как выше.

Главное

  • s[i:j] — элементы с индекса i по j - 1, длина j - i.
  • Срез среза делит базовый массив с исходным: запись через один видна через другой.
  • append в срез среза перезаписывает элементы исходного, пока хватает ёмкости; s[i:j:j] ограничивает ёмкость.
  • Независимую копию делает copy в срез нужной длины.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом