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.