Типы данных и шаблоны
Дальше: Архитектура компиляции · Операции seq, Table, HashSet — ниже
Перед деталями типов полезно пройти основы и общую статью про типизацию в коде. Здесь — практика Nim — var / let / const, коллекции, distinct и generics с примерами "простой → сложный".
Типы данных и шаблоны
Переменные
Переменная в Nim — это именованная область памяти, предназначенная для хранения значения определённого типа. Каждая переменная имеет имя, тип и значение. Имя служит человекочитаемым идентификатором, тип определяет допустимые операции и объём занимаемой памяти, а значение представляет собой конкретные данные, которые могут изменяться в ходе выполнения программы.
Объявление переменной в Nim выполняется с помощью ключевого слова var. Например:
var age: int = 25
Разбор:
varсоздаёт изменяемую переменную.age— имя переменной, по которому к ней обращаются в коде.: intфиксирует целочисленный тип на этапе компиляции.= 25задаёт начальное значение сразу при объявлении.- Такой синтаксис полезен, когда важна явная типизация в учебных и публичных примерах.
Здесь age — имя переменной, int — её тип (целое число), а 25 — начальное значение. Nim поддерживает вывод типа, поэтому в большинстве случаев указание типа можно опустить:
var name = "Alice"
Разбор:
- Тип справа (
"Alice") позволяет компилятору вывести тип автоматически. nameполучает типstringбез явной аннотации: string.varоставляет переменную изменяемой, поэтому строку позже можно переназначить.- Такой стиль сокращает шум, если тип очевиден из литерала.
Компилятор автоматически определит, что name имеет тип string, поскольку присваиваемое значение является строковым литералом. Однако явное указание типа повышает читаемость кода и помогает избежать неожиданностей при рефакторинге или работе с полиморфными выражениями.
Переменные в Nim являются изменяемыми по умолчанию. Это означает, что после объявления их значение можно переназначать:
var counter = 0
counter = counter + 1
Разбор:
- Первая строка объявляет изменяемый счётчик и инициализирует его нулём.
- Во второй строке читается текущее значение
counter, прибавляется1, затем результат записывается обратно. - Это базовый шаблон инкремента для циклов, статистики и накопления состояния.
- Компилятор выводит тип
intдляcounterпо начальному значению0.
Если требуется создать неизменяемую переменную, используется ключевое слово let:
let pi = 3.14159
Разбор:
letсоздаёт неизменяемую привязку: значение нельзя переназначить после инициализации.piполучает типfloat64по литералу с точкой.- Такой стиль уменьшает риск случайных изменений и делает код предсказуемее.
letхорошо подходит для промежуточных вычислений и конфигурации на время выполнения.
Попытка изменить значение переменной, объявленной через let, приведёт к ошибке компиляции. Это способствует написанию более безопасного и предсказуемого кода, особенно в функциональном стиле.
Для констант, известных на этапе компиляции и не подлежащих изменению ни при каких обстоятельствах, применяется ключевое слово const:
const MaxUsers = 1000
Разбор:
constзадаёт константу времени компиляции.MaxUsersподставляется в код как compile-time значение.- Такая константа подходит для лимитов, размеров буферов и параметров сборки.
- При использовании
constкомпилятор дополнительно проверяет, что выражение вычислимо на этапе компиляции.
Константы вычисляются во время компиляции и не занимают место в памяти во время выполнения программы. Они подходят для определения фиксированных параметров, таких как максимальные размеры буферов, порты по умолчанию или математические константы.
Play ITЗагрузка интерактивного демо…
Система типов в Nim
Nim обладает статической системой типов с выводом типов и поддержкой пользовательских типов. Все типы делятся на несколько категорий: базовые (встроенные), составные (структурированные) и пользовательские. Каждый тип определяет множество допустимых значений и операций, применимых к этим значениям.
Базовые типы
Базовые типы предоставляются языком "из коробки" и включают числовые, логические и символьные типы.
-
Целочисленные типы —
int,int8,int16,int32,int64, а также беззнаковые аналогиuint,uint8,uint16,uint32,uint64. Типintявляется платформозависимым: на 64-битных системах он соответствуетint64, на 32-битных —int32. Это позволяет писать переносимый код, не заботясь о точном размере указателей или индексов. -
Вещественные типы:
float32иfloat64. Типfloatпо умолчанию синонимиченfloat64. Эти типы соответствуют стандарту IEEE 754 и поддерживают все арифметические операции с плавающей запятой. -
Логический тип:
boolпринимает два значения —trueиfalse. Он используется в условиях, циклах и логических выражениях. -
Символьный тип:
charпредставляет один символ в кодировке UTF-8. Он занимает один байт и может хранить только ASCII-символы. Для работы с полноценными Unicode-символами используется типRune. -
Строковый тип:
string— это изменяемая последовательность байтов в кодировке UTF-8. Строки в Nim реализованы как динамические массивы и поддерживают конкатенацию, срезы, поиск подстрок и другие операции.
type Priority = enum low, normal, high
let p: Priority = high
echo ord(p), " ", $p
Разбор:
enumобъявляет именованные константы с дискретным набором значений.high— конкретный вариант перечисления.ord(p)возвращает порядковый номер варианта (целое число).$pпреобразует значениеenumв строку для вывода.
Составные типы
Составные типы позволяют объединять несколько значений в одну структуру.
-
Массивы (
array): фиксированной длины, известной на этапе компиляции. Все элементы имеют одинаковый тип. Например,array[5, int]— массив из пяти целых чисел. -
Последовательности (
seq): динамические массивы, размер которых может меняться во время выполнения. Они аналогичны спискам в Python или векторам в C++.Операции
seq:Действие Синтаксис Добавить в конец add(s, value)илиs.add(value)(сimport std/sequtils)Вставить insert(s, index, value)Прочитать / заменить s[index],s[index] = valueУдалить delete(s, index)Создание —
@[],newSeq[T](),@[1, 2, 3].
var nums = @[10, 20]
nums.add(30)
nums[1] = 25
echo nums
Разбор:
-
@[10, 20]создаётseq[int]с двумя элементами. -
addдобавляет30в конец последовательности. -
nums[1] = 25заменяет второй элемент (индексация с нуля). -
echo numsвыводит итоговую последовательность. -
Таблицы (
Table) изstd/tables— словарь с хешированием ключей.Операции
Table:Действие Синтаксис Добавить или заменить t[key] = valueПрочитать t[key]илиt.getOrDefault(key)Удалить t.del(key)илиdel t[key]Создание —
initTable[K, V](), литерал{"a": 1, "b": 2}.toTable.
import std/tables
var users = initTable[string, int]()
users["alice"] = 1
users["bob"] = 2
users.del("bob")
echo users.getOrDefault("alice"), " ", users.len
Разбор:
-
initTable[string, int]()создаёт пустой словарьключ -> значение. -
users["alice"] = 1добавляет или обновляет запись. -
delудаляет ключbob. -
getOrDefault("alice")безопасно читает значение, если ключ есть. -
users.lenпоказывает текущее число пар в таблице. -
Множества (
HashSet) изstd/sets— уникальные ключи без значения.Операции
HashSet:Действие Синтаксис Добавить s.incl(item)илиincl s, itemУдалить s.excl(item)Проверить наличие item in s
import std/sets
var seen = initHashSet[string]()
seen.incl("nim")
seen.incl("rust")
seen.excl("rust")
echo "nim" in seen, " ", seen.len
Разбор:
-
initHashSet[string]()создаёт множество уникальных строк. -
inclдобавляет элемент, повторная вставка не меняет множество. -
exclудаляет элемент, если он был. -
Выражение
"nim" in seenпроверяет наличие ключа. -
seen.lenвозвращает количество уникальных элементов. -
Кортежи (
tuple): упорядоченные наборы значений разных типов с фиксированной структурой. Каждый элемент кортежа имеет позицию, но не обязательно имя. Кортежи удобны для возврата нескольких значений из функции. -
Объекты (
object): основной способ создания пользовательских структур данных. Объекты поддерживают наследование, вариативность (case objects) и инкапсуляцию. Они используются для моделирования сложных сущностей, таких как пользователи, документы или сетевые пакеты. -
Указатели и ссылки: Nim поддерживает явные указатели (
ptr) и ссылки на кучу (ref).ptrне участвует в ORC/ARC — ответственность разработчика;refосвобождается выбранной моделью памяти (по умолчанию ORC в Nim 2.x, см. основы).
Пользовательские типы
Одной из сильных сторон Nim является возможность создавать собственные типы на основе существующих. Это достигается с помощью ключевого слова type:
type
UserId* = distinct int
Username* = string
User* = object
id*: UserId
name*: Username
isActive*: bool
proc newUserId(v: int): UserId = UserId(v)
Разбор:
- Блок
typeобъявляет сразу несколько пользовательских типов. UserId* = distinct intсоздаёт отдельный номинальный тип поверхint; звёздочка экспортирует его из модуля.Username* = string— псевдоним, он остаётся совместимым с обычнымstring.User* = objectопределяет структуру с публичными полямиid,name,isActive.newUserId— фабричная процедура, которая делает явное приведениеint -> UserId.- Такой шаблон снижает риск перепутать "сырой"
intи доменный идентификатор.
UserId — distinct-тип: совместим с int только через явное преобразование, поэтому его нельзя перепутать с другим int. Username — обычный псевдоним (= string), совместим со string.
Различимые типы (distinct) создают новый номинальный тип с тем же представлением в памяти:
type
Meters = distinct float
Feet = distinct float
proc toMeters(f: Feet): Meters = Meters(float(f) * 0.3048)
Разбор:
- Здесь созданы два разных
distinct-типа для единиц измерения. - Несмотря на одинаковое физическое представление (
float),MetersиFeetнесовместимы без преобразования. toMetersпринимает толькоFeet, что фиксирует единицы измерения в сигнатуре.float(f)снимает "обёртку" distinct-типа для арифметики.Meters(...)снова оборачивает результат в целевой тип.
Смешать Meters и Feet в арифметике без преобразования компилятор не даст.
Безопасность типов и проверка на этапе компиляции
Nim обеспечивает строгую проверку типов на этапе компиляции. Любая операция, нарушающая правила типовой совместимости, приводит к ошибке ещё до запуска программы. Например, нельзя сложить строку и число, присвоить bool переменной типа int или вызвать метод, не определённый для данного типа.
Эта строгость сочетается с гибкостью благодаря механизму преобразования типов. Явное преобразование выполняется с помощью вызова типа как функции:
let x: int = 42
let y: float = float(x)
Разбор:
xобъявляется как целое число с явной аннотацией типа.float(x)выполняет явное преобразованиеint -> float.- Результат сохраняется в
y, объявленную какfloat. - Такой стиль делает преобразования очевидными и предотвращает тихие потери точности.
Неявные преобразования ограничены и происходят только в безопасных контекстах, таких как расширение диапазона (например, int8 → int32). Это предотвращает скрытые потери точности или неожиданные побочные эффекты.
Особенности управления памятью
В Nim 2.x по умолчанию для ref действует ORC (см. основы). Указатели ptr и ручные alloc/dealloc — для низкоуровневого кода и режима --mm:none.
array,tuple, значенияobjectбезref— обычно на стеке или внутри других структур;seq,string,ref object— в куче, освобождение через выбранную модель памяти.
Play ITЗагрузка интерактивного демо…
Целочисленные типы
Целочисленные типы в Nim делятся на знаковые и беззнаковые, с фиксированной разрядностью. Наиболее часто используемый тип — int, который адаптируется под разрядность целевой платформы. Это делает его удобным для индексации массивов, работы с размерами и системными вызовами.
Простой пример:
var count: int = 100
echo count # Вывод: 100
Разбор:
countсоздаётся как изменяемыйintсо значением100.echo countвыводит значение в консоль.- Комментарий справа фиксирует ожидаемый вывод для проверки примера.
- Это минимальный шаблон работы с целочисленной переменной и выводом.
Сложный пример:
Предположим, требуется вычислить количество дней между двумя датами, представленными как метки времени (Unix timestamp). Результат должен быть строго положительным целым числом:
import std/times
let
start = initDateTime(2025, mJan, 1, 0, 0, 0, utc())
finish = initDateTime(2026, mDec, 31, 23, 59, 59, utc())
let durationSeconds = finish.toTime() - start.toTime()
let daysBetween: int = int(durationSeconds / 86400.0) # 86400 секунд в сутках
echo "Дней между датами: ", daysBetween
Разбор:
import std/timesподключает API дат и времени.initDateTime(...)создаёт две даты в зонеutc(), чтобы избежать смещений по часовым поясам.toTime()переводитDateTimeк моменту времени, пригодному для вычитания.- Разность
finish.toTime() - start.toTime()даёт длительность в секундах. - Деление на
86400.0переводит секунды в дни;int(...)приводит к целому числу. - Финальный
echoпоказывает результат пользователю.
Здесь результат арифметической операции над временем преобразуется в int — тип, подходящий для дальнейших вычислений или отображения.
Для случаев, где важна точная разрядность (например, при работе с сетевыми протоколами или бинарными форматами), используются фиксированные типы:
var packetId: uint32 = 0x1A2B3C4D
var flags: uint8 = 0b00101100
Разбор:
uint32иuint8задают фиксированную разрядность без знака.0x...— шестнадцатеричный литерал, удобен для идентификаторов и масок.0b...— двоичный литерал, удобен для битовых флагов.- Такой формат часто применяют в сетевых протоколах и бинарных форматах.
Вещественные типы
Типы float32 и float64 предназначены для представления чисел с плавающей запятой. По умолчанию литералы вида 3.14 имеют тип float64.
Простой пример:
var price: float = 19.99
echo price # Вывод: 19.99
Разбор:
priceхранит вещественное значение в типеfloat(float64по умолчанию).- Литерал
19.99сразу указывает компилятору, что нужен тип с плавающей точкой. echoпечатает значение в стандартный вывод.- Пример показывает базовую работу с числами с дробной частью.
Сложный пример:
Вычисление среднего арифметического значений из последовательности, полученной в результате математической трансформации:
import std/[math, sequtils]
let values = @[1.0, 2.0, 3.0, 4.0, 5.0]
let transformed = values.map(proc(x: float): float = sqrt(x * x + 1.0))
let average = transformed.foldl(a + b, 0.0) / float(transformed.len)
echo "Среднее значение: ", average
Разбор:
- Подключаются модули
math(дляsqrt) иsequtils(дляmap/foldl). values— литерал последовательности вещественных чисел.mapприменяет анонимную процедуру к каждому элементу и создаёт новуюseq.- Внутри преобразования считается
sqrt(x * x + 1.0). foldl(a + b, 0.0)аккумулирует сумму всех элементов, начиная с0.0.- Деление на
float(transformed.len)даёт среднее значение без целочисленного деления.
Этот пример демонстрирует работу с функциональными примитивами (map, foldl) и корректное использование float для накопления результата.
Логический тип
Тип bool принимает значения true и false. Он является результатом всех логических и сравнительных операций.
Простой пример:
var isReady: bool = true
echo isReady # Вывод: true
Разбор:
boolограничивает переменную двумя состояниями:trueилиfalse.isReadyсоздаётся как флаг готовности со стартовым значениемtrue.echoпомогает визуально проверить текущее состояние флага.- Такой паттерн часто применяют в условиях и проверках состояния.
Сложный пример:
Проверка сложного условия, например, валидности пользователя по нескольким критериям:
Код ITЗагрузка примера кода…
Разбор:
- Через
type ... objectзадаётся структураUserс несколькими полями. isValidUserвозвращаетboolи объединяет проверки через логическоеand.u.name.len > 0проверяет непустое имя;u.age >= 18— возраст.u.email.contains('@')— базовая валидация формата e-mail.u.isActiveтребует, чтобы пользователь был активен.
См. Проверка и валидация — таблица видов проверок.
- В конце создаётся конкретный
user, затем вызывается валидация и выводится результат.
Здесь логическое выражение объединяет несколько условий, и результат присваивается переменной типа bool.
Символьный и строковый типы
Тип char представляет один байт в кодировке UTF-8 и ограничен ASCII-символами. Для полноценной работы с Unicode используется тип Rune.
Простой пример:
var letter: char = 'A'
echo letter # Вывод: A
Разбор:
charхранит один символ.- Литерал символа пишется в одинарных кавычках (
'A'). echoвыводит символ в консоль.- Пример демонстрирует разницу между символом (
char) и строкой (string).
Сложный пример:
Подсчёт количества гласных в строке с учётом Unicode:
import std/unicode
proc countVowels(s: string): int =
for r in s.runes:
if r in {'а', 'е', 'ё', 'и', 'о', 'у', 'ы', 'э', 'ю', 'я',
'a', 'e', 'i', 'o', 'u'}:
inc(result)
let text = "Привет, мир! Hello, world!"
echo "Гласных: ", countVowels(text)
Разбор:
import std/unicodeподключает корректную работу с Unicode-символами.countVowelsобъявлена как процедура, возвращающая количество (int).for r in s.runesитерирует по рунам, а не по байтам, что важно для кириллицы.- Условие
if r in {...}проверяет принадлежность символа набору гласных. inc(result)увеличивает встроенную переменную результата процедуры.- После этого функция вызывается для строки
text, и итог печатается черезecho.
Здесь строка преобразуется в последовательность Rune, что позволяет корректно обрабатывать многоязычный текст.
Строки в Nim изменяемы, что отличает их от многих других языков:
var message = "Привет"
message.add(", друг!")
echo message # Вывод: Привет, друг!
Разбор:
message— изменяемая строка (var+string).- Метод
addдописывает суффикс к текущему содержимому строки. - В результате исходная переменная меняется на месте.
echoподтверждает итоговую склейку текста.
Интерактивное демо ниже — на Python/JavaScript/C#; синтаксис Nim для
arrayиseq— в примерах выше.
Play ITЗагрузка интерактивного демо…
Массивы и последовательности
Массивы (array) имеют фиксированную длину, известную на этапе компиляции. Последовательности (seq) — динамические и могут расти.
Простой пример (массив):
var rgb: array[3, int] = [255, 128, 0]
echo rgb[0] # Вывод: 255
Разбор:
array[3, int]задаёт массив фиксированной длины3.- Элементы инициализируются литералом
[255, 128, 0]. - Индексация начинается с нуля, поэтому
rgb[0]— первый элемент. - Такой тип подходит для небольших фиксированных структур вроде RGB.
Сложный пример (последовательность):
Фильтрация и преобразование данных:
import std/sequtils
let numbers = @[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let evenSquares = numbers.filter(proc(x: int): bool = x mod 2 == 0)
.map(proc(x: int): int = x * x)
echo evenSquares # Вывод: @[4, 16, 36, 64, 100]
Разбор:
numbersсоздаётся как динамическая последовательность целых.filterоставляет только чётные элементы по условиюx mod 2 == 0.- Затем
mapпреобразует каждый оставшийся элемент в квадратx * x. - Цепочка вызовов возвращает новую
seq[int], не меняя исходнуюnumbers. echo evenSquaresпечатает полученный результат.
Этот пример показывает цепочку функциональных операций над seq[int].
Кортежи
Кортежи позволяют группировать значения разных типов без необходимости определять новый тип.
Простой пример:
let point = (x: 10, y: 20)
echo point.x # Вывод: 10
Разбор:
- Литерал кортежа создаёт структуру с именованными полями
xиy. pointнеизменяем, так как объявлен черезlet.- Доступ к полю идёт через точку:
point.x. - Кортежи удобны для компактной упаковки нескольких связанных значений.
Сложный пример:
Возврат нескольких значений из процедуры:
proc parseVersion(s: string): (major, minor, patch: int) =
let parts = s.split('.')
result.major = parseInt(parts[0])
result.minor = parseInt(parts[1])
result.patch = parseInt(parts[2])
let (maj, min, pat) = parseVersion("1.15.3")
echo "Версия: ", maj, ".", min, ".", pat
Разбор:
- Сигнатура возвращает кортеж из трёх именованных
int-полей. s.split('.')делит строку версии на части по разделителю.parseIntпреобразует текстовые части в числа.result.major/minor/patchзаполняют возвращаемый кортеж по именам полей.- Деструктуризация
let (maj, min, pat) = ...сразу распаковывает результат в отдельные переменные.
Кортежи здесь обеспечивают читаемый и компактный способ передачи структурированных данных.
Play ITЗагрузка интерактивного демо…
Объекты
Объекты — основа пользовательских типов в Nim. Они поддерживают наследование и вариативность.
Простой пример:
type
Person = object
name: string
age: int
var p = Person(name: "Иван", age: 30)
echo p.name
Разбор:
- Определяется объект
Personс полямиnameиage. - Конструкторный литерал
Person(...)создаёт экземпляр и заполняет поля по именам. var pделает переменную изменяемой, если позже понадобится изменить поля.echo p.nameчитает и выводит конкретное поле объекта.
Сложный пример (вариантный объект):
Моделирование геометрических фигур:
Код ITЗагрузка примера кода…
Разбор:
enum ShapeKindзадаёт фиксированный набор вариантов формы.- В
Shapeиспользуется variant object черезcase kind, где набор полей зависит отkind. proc areaповторно делаетcaseпо типу фигуры и выбирает нужную формулу площади.- Для
skCircleиспользуетсяradius, дляskRectangle—widthиheight. - Создаются два объекта разных вариантов, затем обе площади выводятся в консоль.
- Такой подход сохраняет типобезопасность и предотвращает обращение к невалидным полям.
Этот пример демонстрирует мощь вариантных объектов для безопасного моделирования полиморфных данных.
Шаблоны и обобщения (generics)
Обобщённые процедуры параметризуются типом в угловых скобках. Компилятор создаёт отдельную мономорфную версию для каждого использованного типа:
proc max[T](a, b: T): T =
if a > b: a else: b
echo max(3, 7) # int
echo max(1.5, 2.0) # float
Разбор:
max[T]— обобщённая процедура:Tподставляется под конкретный тип при вызове.- Условие
if a > b: a else: bвыбирает большее значение. - При
max(3, 7)компилятор инстанцирует версию дляint. - При
max(1.5, 2.0)создаётся версия дляfloat. - Код остаётся один, а типовая безопасность сохраняется для каждого варианта.
Обобщённые типы объявляют контейнеры и структуры:
type Box[T] = object
value: T
proc get[T](b: Box[T]): T = b.value
let n = Box[int](value: 42)
echo get(n)
Разбор:
Box[T]— обобщённый контейнер, который может хранить значение любого типаT.- Поле
value: Tопределяет, что внутри лежит именно параметризованный тип. get[T]возвращает содержимоеBox[T]без преобразований.Box[int](value: 42)создаёт экземпляр контейнера с конкретным типомint.echo get(n)выводит извлечённое значение.
Шаблоны (template) — метапрограммирование без рантайм-стоимости: тело подставляется в место вызова (см. также функции и макросы):
template withLog(msg: string, body: untyped) =
echo "[start] ", msg
body
echo "[done] ", msg
withLog("загрузка"):
echo "работа..."
Разбор:
template withLogсоздаёт compile-time обёртку вокруг произвольного телаbody.- Параметр
body: untypedпринимает AST-фрагмент без жёсткой типизации сигнатуры. - Перед пользовательским кодом печатается стартовое сообщение, после — завершение.
- Вызов с двоеточием формирует блок кода, который подставляется в шаблон.
- Такой паттерн удобен для кросс-срезочной логики (логирование, тайминг, трассировка).
Макросы работают с AST и нужны для синтаксических расширений; шаблонов достаточно для большинства макросов "замены кода".
Ограничения на тип T задают через concepts или проверки внутри шаблона; при несовместимости типа компилятор сообщает об ошибке на этапе инстанцирования.
Дальше: управляющие конструкции · функции и макросы · простые приложения.