seq: динамические последовательности

Разбираемся, как в Nim устроен динамический список — тип seq — и чем добавление элементов и срезы похожи на привычный list из Python.

seq (от англ. sequence — «последовательность») — динамический массив в Nim, который может расти и уменьшаться во время работы программы, храня элементы одного типа.

Зачем нужен растущий список

В прошлом уроке мы уже использовали фиксированный массив [Kniga(...), Kniga(...)] — но у него есть ограничение: его размер задан раз и навсегда при создании, добавить в него новый элемент нельзя. Это нормально, когда заранее известно точное количество элементов, но реальные задачи часто устроены иначе: список покупок пополняется, список игроков в матче меняется, результаты поиска приходят по одному. Для таких случаев в Nim есть тип seq — последовательность, которая умеет расти.

Если вы писали на Python, seq в Nim — это почти прямой аналог списка list. Разница в том, что seq — типизированная последовательность: все элементы внутри неё обязаны быть одного типа, объявленного один раз при создании — seq[int] хранит только целые числа, seq[string] — только строки. Смешать в одном seq число и строку, как иногда делают в Python-списках, в Nim не получится — компилятор такое не пропустит.

Создание seq

Самый частый способ создать seq — литерал @[] с элементами внутри.

var frukty = @["yabloko", "grusha", "banan"]
echo frukty
echo "Всего фруктов: ", frukty.len

Вывод:

@["yabloko", "grusha", "banan"]
Всего фруктов: 3

Разбираем построчно

Символ @ перед квадратными скобками — это то, что превращает обычный фиксированный массив-литерал в seq. Без символа @ запись ["yabloko", "grusha", "banan"] дала бы фиксированный массив, как в прошлом уроке; с @ — растущую последовательность. Компилятор сам определяет тип элементов по содержимому — здесь это будет seq[string], хотя явно это нигде не написано (вспомните вывод типов из второго раздела учебника).

Переменная frukty объявлена через var, а не let, потому что мы собираемся её менять — добавлять новые элементы. Метод .len возвращает текущее количество элементов — 3, что логично для трёх фруктов при создании.

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

Чтобы добавить элемент в конец seq, используется процедура add.

var frukty = @["yabloko", "grusha"]
frukty.add("banan")
frukty.add("kiwi")
echo frukty
echo "Теперь их: ", frukty.len

Вывод:

@["yabloko", "grusha", "banan", "kiwi"]
Теперь их: 4

Здесь frukty.add("banan") дописывает строку в конец последовательности, а не создаёт новую копию — seq под капотом сам управляет выделением памяти, увеличивая внутренний буфер при необходимости, похоже на то, как это делает list.append() в Python. Именно поэтому frukty обязательно должна быть var — метод add меняет саму последовательность на месте.

Пустой seq можно создать без начальных элементов и наполнять его в цикле — частый паттерн при обработке данных:

var kvadraty: seq[int] = @[]
for i in 1..5:
  kvadraty.add(i * i)
echo kvadraty

Вывод:

@[1, 4, 9, 16, 25]

Обратите внимание: у пустого @[] компилятору не из чего вывести тип элементов, поэтому здесь пришлось указать его явно — seq[int]. Это как раз тот случай из второго раздела учебника, когда вывод типов бессилен и явная типизация обязательна.

Срезы: seq внутри seq

Срез — это способ получить часть последовательности как новый seq, указав диапазон индексов через оператор ...

let chisla = @[10, 20, 30, 40, 50]
let srez = chisla[1..3]
echo srez

Вывод:

@[20, 30, 40]

Запись chisla[1..3] означает «взять элементы с индекса 1 по индекс 3 включительно» — индексация в Nim начинается с нуля, как в Python, поэтому индекс 1 — это 20 (второй элемент), а индекс 3 — это 40 (четвёртый). Обратите внимание на важное отличие от Python: в Python срез chisla[1:3] с оператором : НЕ включает правую границу, а в Nim срез с оператором .. включает обе границы. Это одна из тех мелочей, которая ловит новичков, пришедших из Python — синтаксис похож, а поведение чуть другое.

Если нужно поведение «до, но не включая», в Nim есть отдельный оператор ..<:

let chisla = @[10, 20, 30, 40, 50]
echo chisla[1..<3]

Вывод:

@[20, 30]

Мини-пример: очередь заданий

Соберём seq, add и срез в одной практической задаче — список заданий, из которого мы забираем первые несколько на выполнение сегодня.

var zadaniya = @["Matematika", "Fizika"]
zadaniya.add("Himiya")
zadaniya.add("Istoriya")

let naSegodnya = zadaniya[0..1]
echo "Всего заданий: ", zadaniya.len
echo "На сегодня: ", naSegodnya

Вывод:

Всего заданий: 4
На сегодня: @["Matematika", "Fizika"]

Частые ошибки новичков

Первая ошибка — забыть символ @ перед квадратными скобками и получить фиксированный массив вместо растущей последовательности, а потом удивляться ошибке компиляции при попытке вызвать add.

Вторая ошибка — путать границы среза с Python: ожидать, что chisla[1..3] не включит элемент с индексом 3, как это было бы в Python-нотации chisla[1:3]. В Nim оператор .. включает обе границы — для «Python-подобного» поведения нужен оператор ..<.

Третья ошибка — попытаться вызвать add на последовательности, объявленной через let. Как и с полями object в прошлом уроке, let запрещает любое изменение — включая добавление элементов — поэтому для последовательности, которая будет пополняться, всегда нужен var.

Итоги

  • seq[T] — динамическая типизированная последовательность, аналог списка Python, но с одним типом элементов на всю последовательность.
  • Литерал @[] создаёт seq, метод .add() дописывает элемент в конец, .len возвращает количество элементов.
  • Срез a[i..j] включает ОБЕ границы (в отличие от Python) — для исключающей верхней границы используется оператор ..<.
  • Для изменения seq (добавление, срезы на запись) переменная обязана быть объявлена через var, а не let.
Проверьте себя
1. Чем seq[int] в Nim принципиально отличается от списка list в Python?
Aseq вообще не может менять размер после создания
BВсе элементы seq обязаны быть одного объявленного типа — смешать в нём числа и строки нельзя, в отличие от list в Python
Cseq может хранить только числа, но не строки
DМежду ними нет никакой разницы, это одно и то же под разными именами
2. Что вернёт срез chisla[1..3] для seq chisla = @[10, 20, 30, 40, 50] и чем это отличается от Python?
A@[20, 30, 40] — оператор .. включает обе границы, в отличие от Python-среза [1:3], который не включает правую границу
B@[20, 30] — ведёт себя так же, как срез Python
C@[10, 20, 30] — берёт первые три элемента
DОшибку компиляции, срезы в Nim не поддерживаются