Найти в Дзене
Блокнот математика

Объектность в Вим

Друзья, Вим версии 9 вышел, я скачал 9.1, есть уже 9.2, может и дальше. Объектная система в скриптовом языке Вим планировалась, уход из жизни автора Вим Брама Муленара внёс неопредённость... но версия 9 вышла и ООП в ней есть. Нужна ли она и насколько нужна — вопрос, но знать о её существовании полезно.

Брам Муленаар. 1961-2023.
Брам Муленаар. 1961-2023.

Я кратко опишу планируемую объектную систему, потому что на ней можно удобно показать важные концепции. Она лишена всех "так исторически сложилось" и "для обратной совместимости оставили".

Объект — это некоторое состояние. Оно описывается набором переменных, имеющих значения. Объект предоставляет функции (методы), которые это состояние меняют или используют. Объект имеет интерфейс: набор переменных с типом и функций с заданными параметрами и возвращаемым значением.

Пример. Мы можем создать объект "рациональное число", описываемое двумя целыми (причем второе строго положительно). И есть методы для сложения, проверки на равенство и пр. Метод "знает" свой объект.

Классы

Объект относится к классу и порождается им. Все объекты одного класса имеют один и тот же интерфейс. Класс предоставляет метод new(), который называется конструктор и возвращает объект. Конструктор принадлежит классу и вызывается через его имя: MyClass.new().

Классом будет "рациональное число" как идея. И можно создавать конкретные переменные типа "рациональное число": объекты. Создавать их и будет new().

Классу могут принадлежать и переменные: они общие для всех объектов.

Например, мы можем задать точность для перевода рационального числа в десятичную форму. Она одна на всех. Но мы можем иметь два класса разных рациональных чисел, отличающиеся этой точностью.

Интерфейсы

Объект может реализовывать интерфейсы, которые описывают, что объект может делать. Объекты разных классов, но реализующие один интерфейс, могут использоваться одинаково. Интерфейс можно описать отдельно, как список того, что должен уметь класс, либо указать, что данный класс описывает интерфейс, и другие классы, реализующие интерфейс, должны уметь то же (плюс что-то ещё), что и данный класс.

Интерфейсом может быть, скажем "поле", и тогда объект должен кое-что уметь делать: складываться с другим объектом того же класса и умножаться, иметь обратный и противоположный, и кое-что еще.
Пример фривольный, но поучтельный: класс "жена" расширяет класс "женщина" и реализует интерфейсы "любовница", "экономка" и "кухарка". Какие методы прописаны в этих интерфейсах — можете додумать сами.

Наследование

Классы могут быть наследованы от (одного) другого класса, что создает дерево наследования. Множественного наследования нет, но его функции реализуют интерфейсы. Синтаксис наследования приведён чуть ниже.

Вот пример простого класса из документации:

class TextPosition
           this.lnum: number
           this.col: number

def new(lnum: number, col: number)
              this.lnum = lnum
              this.col = col
           enddef

def SetLnum(lnum: number)
              this.lnum = lnum
           enddef

def SetCol(col: number)
              this.col = col
           enddef

def SetPosition(lnum: number, col: number)
              this.lnum = lnum
              this.col = col
           enddef
endclass

Здесь всё понятно: класс, в нем два поля-переменных, четыре функции-метода. Можно создавать объекты:

var pos = TextPosition.new(1, 1)

Важно, что поля защищены: их можно читать извне объекта, но нельзя изменять. Например,

echo $'The text position is ({pos.lnum}, {pos.col})'

Ко всем переменным объектов доступ через this. Это позволяет легко отыскивать эти обращения в коде. Ну и если this нету, то это точно не переменная объекта.

Можно явно объявить поле публичным:

public this.lnum: number

Тогда его можно и читать, и изменять извне.

Можно сделать поле и приватным: тогда его нельзя ни читать, ни (тем более) изменять извне. Для этого имя должно начинаться с подчеркивания:

this._lnum: number

Почему решили так асимметрично поступить — публичные через особое ключевое слово, а приватные через формат имени? Ну, если вы вдруг решили сделать поле публичным, вам надо только добавить слово public, имя поля не изменится и вам не надо менять его во всех вхождениях (а это может быть где угодно, поле же было доступно для чтения извне). Если же вы решили сделать поле приватным, вам так или иначе придется убрать все случаи доступа извне. И наоборот: если вы решили убрать приватность, то изменить надо имена только внутри класса, вне оно не могло упоминаться. А вот если вы решили убрать публичность, то случаи чтения поля откуда угодно отследить трудно.

Методы тоже могут быть приватными, по той же схеме: имя функции начинается с _.

Конструктор допускает упрощенный синтаксис:

def new(this.lnum, this.col)
enddef

Указав явно имена полей, вы не просто задаете аргументы функции, но и предписываете, каким полям их присвоить. То есть, упрощаете самую распространенную роль конструктора: задать начальные значения полей. Причём если конструктора вообще нет, то он будет создан именно в таком виде: его аргументами будут все поля в порядке из объявления, то есть можно очень просто их задать:

class AutoNew

var name: string

var age: number

endclass

Тогда автоматически созданный конструктор new имеет два необязательных аргумента (при отсутствии присвоит полю значение v:none) и позволяет создать объект с заданными значениями полей.

Конструкторов может быть и больше одного, но имя каждого должно начинаться с new. Если конструктора new нет, он будет создан, даже если имеются другие конструкторы.

Некоторые поля могут относиться к классу в целом: их значения едины для всех экземпляров. Их мы уже упоминали, и они задаются ключевым словом static. Они тоже могут быть публичными или приватными. Методы тоже могут относиться к классу: тогда им доступны только переменные класса. Конструкторы в описателе static не нуждаются, так как они изначально относятся к классу. Такие переменные и методы не наследуются и наследник может определить свои с теми же именами.

Класс может быть абстрактным: в этом случае объекты/экземпляры на его основе не создаются, но он служит основой для наследования. Например, вектор, матрицы и тензоры все могут наследоваться от абстрактного объекта "числовой многомерный объект", который декларирует метод "сложить с таким же", метод "умножить", имеет массив элементов и какую-то размерность. При этом информации недостаточно, чтобы экземпляры класса создавать, да и нужды нет.

Можно снабдить переменные класса или объекта ключевым словом final либо const и тогда они неизменны: константы. Задавать их может только конструктор (при создании объекта), а потом они уже не могут меняться. Однако, как часто бывает в Вим, final-неизменность касается сущности, а не её частей. Так, если массив из двух элементов объявлен как final, то присвоить этой переменной массив нельзя, а вот менять элементы — можно. Если же переменная (скаляр, массив, словарь) const, то однажды задав в конструкторе, ее больше нельзя менять, ни целиком, ни по частям.

Наследование осуществляется через слово extends: "расширяет". В документации приведен пример:

abstract class Shape

var color

var thickness

abstract def Draw()

endclass

class Square extends Shape

var size: number

def new(this.size)

enddef

endclass

Всё понятно? Абстрактный класс задаёт идею формы, имеющей цвет и толщину линии, которой нарисован. И метод "нарисовать". Класс "квадрат" расширяет идею формы и объекты этого класса могут быть созданы (и нарисованы). Квадрат имеет характеристику: размер. Можно создать другой класс, треугольник. И так далее. В нашем примере не хватает реализации метода Draw(), который рисует квадрат. Он обязан присутствовать, если абстрактный метод объявлен.

Осознанно не поддерживается перегрузка конструкторов, методов и функций.