Перейти к основному содержимому

Типы данных и шаблоны

Разработчику Архитектору

Дальше: Архитектура компиляции · Операции 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 и доменный идентификатор.

UserIddistinct-тип: совместим с 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.
  • Такой стиль делает преобразования очевидными и предотвращает тихие потери точности.

Неявные преобразования ограничены и происходят только в безопасных контекстах, таких как расширение диапазона (например, int8int32). Это предотвращает скрытые потери точности или неожиданные побочные эффекты.


Особенности управления памятью

В 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, для skRectanglewidth и 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 или проверки внутри шаблона; при несовместимости типа компилятор сообщает об ошибке на этапе инстанцирования.

Дальше: управляющие конструкции · функции и макросы · простые приложения.