object: собственные типы данных

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

object — тип данных в Nim, который объединяет несколько именованных полей разных типов в одну сущность, например «ученик» с именем, возрастом и оценкой.

Зачем нужен собственный тип

Представьте, что вы храните данные об ученике: имя, возраст, средний балл. Можно завести три отдельные переменные — imya, vozrast, ball — но это неудобно и опасно: ничего не мешает случайно перепутать порядок при передаче в функцию, а если учеников много, придётся плодить переменные с номерами вроде imya1, imya2. В Python для такой задачи обычно заводят класс или используют словарь. В Nim есть свой инструмент — object, который позволяет объявить новый тип данных с именованными полями и работать с ним как с единым целым.

object в Nim — это не класс в привычном смысле ООП. У него нет встроенных методов внутри тела типа, нет наследования по умолчанию и нет конструктора, который вы обязаны писать. Это просто «чертёж» структуры данных: список полей и их типов. Функции, которые работают с этим типом, объявляются отдельно, вне описания самого object — подробнее об этом будет отдельный раздел учебника.

Объявление типа

Тип объявляется в блоке type, с указанием имени (по соглашению — с заглавной буквы) и списком полей с их типами.

type
  Uchenik = object
    imya: string
    vozrast: int
    ball: float

var petya = Uchenik(imya: "Petya", vozrast: 14, ball: 4.5)
echo petya.imya, " — ", petya.vozrast, " лет, балл ", petya.ball

Вывод:

Petya — 14 лет, балл 4.5

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

Строка type открывает блок объявления типов — в Nim типы, в отличие от обычных переменных, описываются в отдельном блоке, похожем на блок var для переменных. Дальше Uchenik = object говорит: «создаю новый тип с именем Uchenik, и это структура-объект». Три следующие строки с отступом — поля этого типа: imya типа string, vozrast типа int, ball типа float. Обратите внимание на синтаксис поля — это то же самое имя: тип, что вы уже видели при явной типизации обычных переменных.

Дальше идёт создание экземпляра: Uchenik(imya: "Petya", vozrast: 14, ball: 4.5) — это вызов типа как конструктора, куда поля передаются по имени, через двоеточие, в любом порядке (можно было бы написать vozrast раньше imya, и код остался бы рабочим). Результат сохраняется в переменную petya через var, потому что мы, возможно, захотим её позже изменить. Наконец, обращение к полю происходит через точку: petya.imya, petya.vozrast — синтаксис, знакомый каждому, кто хоть немного работал с объектами в Python или JavaScript.

Сравнение с классами Python

В Python похожая структура выглядела бы так:

class Uchenik:
    def __init__(self, imya, vozrast, ball):
        self.imya = imya
        self.vozrast = vozrast
        self.ball = ball

petya = Uchenik("Petya", 14, 4.5)

Разница сразу бросается в глаза: в Python нужно вручную написать метод __init__, который принимает аргументы и присваивает их полям self. В Nim это делать не нужно — конструктор с именованными полями создаётся автоматически, просто из описания типа. Это экономит время на простых структурах данных, но есть и обратная сторона: если полей много, вызов вида Uchenik(imya: ..., vozrast: ..., ball: ...) с явными именами полей — это стандартный и рекомендуемый стиль, в отличие от Python, где чаще пишут позиционные аргументы без имён.

Ещё одно отличие — производительность и хранение в памяти. Объекты Python — это словари под капотом (в упрощённом виде), гибкие, но с накладными расходами. object в Nim по умолчанию компилируется в компактный блок памяти фиксированного размера, как struct в языке C — отсюда и разговор о «скорости C» в названии этого учебника. Поля объекта лежат в памяти подряд, без лишней обёртки, и обращение к ним практически бесплатно по времени выполнения.

Изменение полей

Поля объекта, объявленного через var, можно менять после создания — так же, как обычную переменную:

type
  Uchenik = object
    imya: string
    ball: float

var masha = Uchenik(imya: "Masha", ball: 3.5)
masha.ball = 5.0
echo masha.imya, " исправила балл на ", masha.ball

Вывод:

Masha исправила балл на 5.0

Если бы masha была объявлена через let (неизменяемая переменная), строка masha.ball = 5.0 вызвала бы ошибку компиляции — компилятор Nim запрещает менять поля объекта, привязанного к let, точно так же, как запрещает переприсваивать саму переменную.

Мини-пример: карточка книги

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

type
  Kniga = object
    nazvanie: string
    avtor: string
    stranits: int

let knigi = [
  Kniga(nazvanie: "Dune", avtor: "Herbert", stranits: 412),
  Kniga(nazvanie: "1984", avtor: "Orwell", stranits: 328)
]

for k in knigi:
  echo k.nazvanie, " (", k.avtor, "), ", k.stranits, " стр."

Вывод:

Dune (Herbert), 412 стр.
1984 (Orwell), 328 стр.

Здесь мы создали массив из двух объектов типа Kniga и прошлись по нему циклом for, обращаясь к полям каждого элемента через точку. Именно так на практике чаще всего и используют object — не поодиночке, а коллекциями, что естественным образом подводит нас к следующему уроку про seq.

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

Первая ошибка — забыть, что object объявляется внутри блока type, а не как обычная переменная. Написать var Uchenik = object ... вместо type Uchenik = object ... — частая опечатка у тех, кто только начал.

Вторая ошибка — попытаться создать экземпляр без имён полей, по одной лишь позиции, как в Python (Uchenik("Petya", 14, 4.5)). Такой синтаксис в Nim тоже допустим при определённых условиях, но начинающих он часто путает с полем не на своём месте — поэтому именованный синтаксис с двоеточиями надёжнее и на первых порах его стоит использовать всегда.

Третья ошибка — пытаться изменить поле объекта, объявленного через let. Компилятор Nim строго следит за неизменяемостью, и ошибка «attempt to modify readonly variable» — частый сюрприз для тех, кто привык к более расслабленному Python.

Итоги

  • object позволяет объявить собственный тип данных с именованными полями — аналог класса-структуры без методов внутри тела типа.
  • Экземпляр создаётся вызовом типа как конструктора с полями по имени: Тип(поле: значение, ...).
  • В отличие от объектов Python, object Nim компилируется в компактный блок памяти без накладных расходов — источник «скорости C».
  • Поля можно менять только у объекта, привязанного к var — у let-объекта поля защищены от изменения так же, как и сама переменная.
Проверьте себя
1. Как в Nim правильно объявить новый тип object с полями imya типа string и vozrast типа int?
Avar Chelovek = object imya: string, vozrast: int
Btype Chelovek = object; imya: string; vozrast: int (на отдельных строках с отступом внутри блока type)
Cdef Chelovek(object): imya, vozrast
Dobject Chelovek(imya: string, vozrast: int)
2. Что произойдёт, если попытаться изменить поле объекта, который был объявлен через let, а не var?
AПоле изменится без проблем, let влияет только на саму переменную, а не на её поля
BКомпилятор выдаст ошибку — поля let-объекта защищены от изменения так же, как и сама переменная
CNim автоматически превратит let в var
DПрограмма скомпилируется, но упадёт во время выполнения