5.21. Типы данных
Типы данных
Переменные: именованные контейнеры для данных
Переменная в Nim — это именованная область памяти, предназначенная для хранения значения определённого типа. Каждая переменная имеет имя, тип и значение. Имя служит человекочитаемым идентификатором, тип определяет допустимые операции и объём занимаемой памяти, а значение представляет собой конкретные данные, которые могут изменяться в ходе выполнения программы.
Объявление переменной в Nim выполняется с помощью ключевого слова var. Например:
var age: int = 25
Здесь age — имя переменной, int — её тип (целое число), а 25 — начальное значение. Nim поддерживает вывод типа, поэтому в большинстве случаев указание типа можно опустить:
var name = "Alice"
Компилятор автоматически определит, что name имеет тип string, поскольку присваиваемое значение является строковым литералом. Однако явное указание типа повышает читаемость кода и помогает избежать неожиданностей при рефакторинге или работе с полиморфными выражениями.
Переменные в Nim являются изменяемыми по умолчанию. Это означает, что после объявления их значение можно переназначать:
var counter = 0
counter = counter + 1
Если требуется создать неизменяемую переменную, используется ключевое слово let:
let pi = 3.14159
Попытка изменить значение переменной, объявленной через let, приведёт к ошибке компиляции. Это способствует написанию более безопасного и предсказуемого кода, особенно в функциональном стиле.
Для констант, известных на этапе компиляции и не подлежащих изменению ни при каких обстоятельствах, применяется ключевое слов const:
const MaxUsers = 1000
Константы вычисляются во время компиляции и не занимают место в памяти во время выполнения программы. Они подходят для определения фиксированных параметров, таких как максимальные размеры буферов, порты по умолчанию или математические константы.
Система типов в 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 реализованы как динамические массивы и поддерживают конкатенацию, срезы, поиск подстрок и другие операции.
Составные типы
Составные типы позволяют объединять несколько значений в одну структуру.
-
Массивы (
array): фиксированной длины, известной на этапе компиляции. Все элементы имеют одинаковый тип. Например,array[5, int]— массив из пяти целых чисел. -
Последовательности (
seq): динамические массивы, размер которых может меняться во время выполнения. Они аналогичны спискам в Python или векторам в C++. Последовательности создаются с помощью процедур@[]илиnewSeq. -
Кортежи (
tuple): упорядоченные наборы значений разных типов с фиксированной структурой. Каждый элемент кортежа имеет позицию, но не обязательно имя. Кортежи удобны для возврата нескольких значений из функции. -
Объекты (
object): основной способ создания пользовательских структур данных. Объекты поддерживают наследование, вариативность (case objects) и инкапсуляцию. Они используются для моделирования сложных сущностей, таких как пользователи, документы или сетевые пакеты. -
Указатели и ссылки: Nim поддерживает явные указатели (
ptr) и трассируемые ссылки (ref). Указатели не отслеживаются сборщиком мусора и требуют ручного управления памятью, тогда какrefавтоматически управляются GC и безопасны для использования в большинстве сценариев.
Пользовательские типы
Одной из сильных сторон Nim является возможность создавать собственные типы на основе существующих. Это достигается с помощью ключевого слова type:
type
UserId = int
Username = string
User = object
id: UserId
name: Username
isActive: bool
Здесь UserId и Username — это псевдонимы (distinct types можно создать отдельно), а User — полноценный объект. Такой подход повышает семантическую выразительность кода: вместо того чтобы передавать просто int, разработчик работает с понятным UserId, что снижает вероятность ошибок.
Nim также поддерживает различимые типы (distinct), которые создают новый тип, несовместимый с исходным, даже если они имеют одинаковое внутреннее представление. Это мощный инструмент для предотвращения смешивания, например, метров и футов, или идентификаторов разных сущностей.
Безопасность типов и проверка на этапе компиляции
Nim обеспечивает строгую проверку типов на этапе компиляции. Любая операция, нарушающая правила типовой совместимости, приводит к ошибке ещё до запуска программы. Например, нельзя сложить строку и число, присвоить bool переменной типа int или вызвать метод, не определённый для данного типа.
Эта строгость сочетается с гибкостью благодаря механизму преобразования типов. Явное преобразование выполняется с помощью вызова типа как функции:
let x: int = 42
let y: float = float(x)
Неявные преобразования ограничены и происходят только в безопасных контекстах, таких как расширение диапазона (например, int8 → int32). Это предотвращает скрытые потери точности или неожиданные побочные эффекты.
Особенности управления памятью
Хотя Nim предоставляет высокоуровневые абстракции, он не скрывает детали управления памятью. Разработчик может выбирать между автоматическим управлением (через сборщик мусора) и ручным (через указатели и процедуры вроде alloc/dealloc). По умолчанию Nim использует трассирующий сборщик мусора, который работает эффективно даже в многопоточных приложениях.
Для переменных, содержащих ссылки на объекты (ref), память освобождается автоматически, когда на них больше нет активных ссылок. Это позволяет писать безопасный код без риска утечек памяти.
Целочисленные типы
Целочисленные типы в Nim делятся на знаковые и беззнаковые, с фиксированной разрядностью. Наиболее часто используемый тип — int, который адаптируется под разрядность целевой платформы. Это делает его удобным для индексации массивов, работы с размерами и системными вызовами.
Простой пример:
var count: int = 100
echo count # Вывод: 100
Сложный пример:
Предположим, требуется вычислить количество дней между двумя датами, представленными как метки времени (Unix timestamp). Результат должен быть строго положительным целым числом:
import times
let
start = initDateTime(2025, mJan, 1, 0, 0, 0, utc())
end = initDateTime(2026, mDec, 31, 23, 59, 59, utc())
let durationSeconds = end.toTime() - start.toTime()
let daysBetween: int = int(durationSeconds / 86400) # 86400 секунд в сутках
echo "Дней между датами: ", daysBetween
Здесь результат арифметической операции над временем преобразуется в int — тип, подходящий для дальнейших вычислений или отображения.
Для случаев, где важна точная разрядность (например, при работе с сетевыми протоколами или бинарными форматами), используются фиксированные типы:
var packetId: uint32 = 0x1A2B3C4D
var flags: uint8 = 0b00101100
Вещественные типы
Типы float32 и float64 предназначены для представления чисел с плавающей запятой. По умолчанию литералы вида 3.14 имеют тип float64.
Простой пример:
var price: float = 19.99
echo price # Вывод: 19.99
Сложный пример:
Вычисление среднего арифметического значений из последовательности, полученной в результате математической трансформации:
import math
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
Этот пример демонстрирует работу с функциональными примитивами (map, foldl) и корректное использование float для накопления результата.
Логический тип
Тип bool принимает значения true и false. Он является результатом всех логических и сравнительных операций.
Простой пример:
var isReady: bool = true
echo isReady # Вывод: true
Сложный пример:
Проверка сложного условия, например, валидности пользователя по нескольким критериям:
type
User = object
name: string
age: int
email: string
isActive: bool
proc isValidUser(u: User): bool =
u.name.len > 0 and
u.age >= 18 and
u.email.contains('@') and
u.isActive
let user = User(name: "Алексей", age: 25, email: "alex@example.com", isActive: true)
echo "Пользователь валиден: ", isValidUser(user)
Здесь логическое выражение объединяет несколько условий, и результат присваивается переменной типа bool.
Символьный и строковый типы
Тип char представляет один байт в кодировке UTF-8 и ограничен ASCII-символами. Для полноценной работы с Unicode используется тип Rune.
Простой пример:
var letter: char = 'A'
echo letter # Вывод: A
Сложный пример:
Подсчёт количества гласных в строке с учётом Unicode:
import 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)
Здесь строка преобразуется в последовательность Rune, что позволяет корректно обрабатывать многоязычный текст.
Строки в Nim изменяемы, что отличает их от многих других языков:
var message = "Привет"
message.add(", друг!")
echo message # Вывод: Привет, друг!
Массивы и последовательности
Массивы (array) имеют фиксированную длину, известную на этапе компиляции. Последовательности (seq) — динамические и могут расти.
Простой пример (массив):
var rgb: array[3, int] = [255, 128, 0]
echo rgb[0] # Вывод: 255
Сложный пример (последовательность):
Фильтрация и преобразование данных:
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]
Этот пример показывает цепочку функциональных операций над seq[int].
Кортежи
Кортежи позволяют группировать значения разных типов без необходимости определять новый тип.
Простой пример:
let point = (x: 10, y: 20)
echo point.x # Вывод: 10
Сложный пример:
Возврат нескольких значений из процедуры:
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
Кортежи здесь обеспечивают читаемый и компактный способ передачи структурированных данных.
Объекты
Объекты — основа пользовательских типов в Nim. Они поддерживают наследование и вариативность.
Простой пример:
type
Person = object
name: string
age: int
var p = Person(name: "Иван", age: 30)
echo p.name
Сложный пример (вариантный объект):
Моделирование геометрических фигур:
type
ShapeKind = enum
skCircle, skRectangle
Shape = object
kind: ShapeKind
case kind
of skCircle:
radius: float
of skRectangle:
width, height: float
proc area(s: Shape): float =
case s.kind
of skCircle:
return 3.14159 * s.radius * s.radius
of skRectangle:
return s.width * s.height
let circle = Shape(kind: skCircle, radius: 5.0)
let rect = Shape(kind: skRectangle, width: 4.0, height: 6.0)
echo "Площадь круга: ", area(circle)
echo "Площадь прямоугольника: ", area(rect)
Этот пример демонстрирует мощь вариантных объектов для безопасного моделирования полиморфных данных.