# Классы

Класс - это набор данных и функций для обработки этих данных. Определение класса имеет следующий синтаксис

'class' [<имя-класса>] ['extends' <базовый-класс>]
[`строка с документацией`]
'{'
   [<метод-1>] [<метод-2>] ... [<метод-N>]
   [<аксессор-1> [<аксессор-2>] ... [<аксессор-N>]]
'}'

где метод - определение метода

<имя-метода> '(' [<имя-параметра-1> [',' <имя-параметра-2>] ... [',']] ')'
[`строка с документацией`]
'{'
   <тело-метода>
'}'

аксессор - определение геттера или сеттера

('get' | 'получить') <имя-свойства>
[`строка с документацией`]
'{'
   <тело-геттера>
'}'

('set' | 'установить') <имя-свойства> '(' <имя-параметра> ')'
[`строка с документацией`]
'{'
   <тело-сеттера>
'}'

а <имя-класса> - идентификатор. Если <имя-класса> не указано, такой класс называется анонимным, иначе - именованным. Для именованного класса <базовый-класс> должен быть идентификатором, т.е. именованные классы могут наследоваться только от других именованных классов. Для анонимного класса <базовый-класс> может быть произвольным выражением, результатом вычисления которого является класс (иначе возникает исключение). Определение именованного класса является инструкцией, а анонимного - выражением.

Пример:

# именованный класс
class D extends S
{
   constructor(x)
   {
      this.x = x
   }

   method()
   {
      return this.x * this.x
   }
}

# анонимный класс
var C = class extends fn()
{
}

func fn()
{
   return class {}
}

Имена классов попадают в собственное пространство имен. Если определить два или более именованных класса с одинаковыми именами, то все они будут объединены в один класс. При объединении, если в нескольких классах есть одинаковые члены (конструкторы, геттеры/сеттеры или методы), приоритет имеют члены того класса, который был определен последним. Пример:

:[a]
class C
{
   constructor()
   {
      this._x = 1
   }

   constructor(x)
   {
      this._x = x
   }

   get x()
   {
      return this._x
   }

   set x(v)
   {
      this._x = v
   }

   get y()
   {
      return this._y
   }

   method()
   {
      return this.x + this.y
   }

   method(z)
   {
      return this.method() + z
   }
}

:[b]
class C
{
   constructor()
   {
      this._x = 2
   }

   constructor(x, y)
   {
      this._x = x
      this._y = y
   }

   get x()
   {
      return this.x + 2
   }

   set y(v)
   {
      this.y = v + 2
   }

   method()
   {
      return this.x + this.y + 2
   }

   method(z, w)
   {
      return this.method(z) + w
   }
}

эквивалентно одному определению

:[a, b]
class C
{
   # переопреден, из второго класса
   constructor()
   {
      this._x = 2
   }

   # из первого класса
   constructor(x)
   {
      this._x = x
   }

   # из второго класса
   constructor(x, y)
   {
      this._x = x
      this._y = y
   }

   # переопреден, из второго класса
   get x()
   {
      return this.x + 2
   }

   # из первого класса
   set x(v)
   {
      this._x = v
   }

   # из первого класса
   get y()
   {
      return this._y
   }

   # из второго класса
   set y(v)
   {
      this.y = v + 2
   }

   # переопределен, из второго класса
   method()
   {
      return this.x + this.y + 2
   }

   # из первого класса
   method(z)
   {
      return this.method() + z
   }

   # из второго класса
   method(z, w)
   {
      return this.method(z) + w
   }
}

Выражение <базовый-класс> для анонимного класса вычисляется каждый раз, когда вычисляется определение класса:

func f()
{
   # функция g будет вызываться каждый раз при вызове f
   return class extends g() {}
}

Синоним ключевого слова class - класс.

# Создание экземпляра

Создать экземпляр класса можно с помощью оператора new (новый):

class C
{
   constructor(x)
   {
      this.x = x           # инициализация поля x
   }
}

var ci = new C("a")        # в ci хранится экземпляр класса C

Экземпляры классов имеют тип Instance. Экземпляры могут хранить произвольный набор полей (пар имя-значение), к которым можно обращаться с помощью операторов . и []. Таким образом экземпляры класса похожи на Object, но вместо свойств и методов Object они содержат свойства и методы, определенные в их классе.

После создания экземпляра класса оператор new автоматически вызывает конструктор класса (метод с именем constructor или конструктор), в котором можно выполнить инициализацию полей нового экземпляра класса. В конструктор передаются все аргументы, которые были переданы в оператор new. Если ни одна из перегрузок конструктора не может принять переданные в new аргументы, возникает исключение. Конструктор может отсутствовать, в этом случае он не вызывается.

Возвращать значение из конструктора класса запрещено:

class C
{
   constructor()
   {
      return            # OK
      return 1          # ошибка компиляции, возвращать значение из
                        # конструктора запрещено
   }
}

Ключевое слово new, за которым идет тело класса, т.е. new <тело-класса>, эквивалентно выражению new (class <тело-класса>)(), например:

var s = new
{
   constructor()
   {
      this.x = 1
   }

   method(v)
   {
      return this.x + 1
   }
}

s.method(1)             # возвращает 2

Такая форма записи удобна при создании синглтонов.

# Поля

Поля экземпляра представляют собой набор пар имя-значение, который принадлежит конкретному экземпляру класса. Присвоение полю значения создает новую пару или обновляет значение в существующей паре. При чтении поля возвращается значение, которое было ранее в него записано.

class C
{
   constructor()
   {
      this.x = 1        # записывает в поле x значение 1
   }
}

var ci = new C()
ci.x                    # считывает значение поля x, т.е. 1

# Методы

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

class C
{
   # определение метода с именем method
   method()
   {
      return 1
   }
}

Вызывать метод на экземпляре класса можно с помощью оператора . или []:

var ci = new C()
ci.method()             # возвращает 1

Внутри метода можно обратиться к экземпляру класса, на котором он был вызван, с помощью специальной переменной this (этот).

class C
{
   constructor(x)
   {
      this.x = x
   }

   method()
   {
      return this.x * this.x
   }
}

var ci = new C(2)
ci.method()             # возвращает 4

В отличие от обычных переменных, this видима только непосредственно внутри метода, и не видна в вызванных им функциях:

func f()
{
   # локальная переменная y из method видна, а this - нет
   # выполнение выражения приведет к исключению
   return this.x + y
}

class C
{
   method()
   {
      this.x = 1
      var y = 2
      return f()
   }
}

Если вызвать метод без экземпляра класса, то обращение к this приведет к исключению:

class C
{
   method()
   {
      return this.x
   }
}

var ci = new C()
var m = ci.method

# исключение, this отсутствует
m()

Так же как и именованные функции, методы класса (в т.ч. и конструкторы) могут перегружать друг друга:

class C
{
   method()
   {
      return 1
   }

   method(x)
   {
      return x
   }
}

var ci = new C()
ci.method()                # вернет 1
ci.method(2)               # вернет 2

# Свойства

Свойство - это виртуальное поле класса. В отличие от обычных полей, которые хранятся непосредственно в виде значений экземпляре класса, свойства представляют собой пару специальных методов - геттер и сеттер (аксессоры). При чтении свойства неявно вызывается его геттер, значением свойства становится возвращенное геттером значение; для записи свойства вызывается сеттер. Таким образом, свойства класса позволяют исполнять произвольный код при обращении к ним:

class C
{
   # геттер свойства x
   get x()
   {
      return this._x
   }

   # сеттер свойства x
   # запрещает присваивать этому полю отрицательные значения
   set x(v)
   {
      if (v < 0)
         throw "Negative value"

      this._x = v
   }
}

var ci = new C()
ci.x = 10               # вызывается сеттер x, присваивает полю this._x значение 10
var t = ci.x            # вызывается геттер x, возвращает значение this._x, т.е. 10
ci.x = -10              # исключение "Negative value"

Так же как и внутри обычных методов, внутри аксессоров можно обращаться к this.

Методы-геттеры помечаются ключевым словом get (получить). Геттеры не могут иметь параметров. Выполнять return без выражения внутри геттера запрещено.

class C
{
   # ошибка компиляции, геттер не может принимать параметры
   get x(v)
   {
      return 1
   }

   get y()
   {
      # ошибка компиляции, геттер должен возвращать значение
      return
   }
}

Методы-сеттеры помечаются ключевым словом set (установить). Сеттер должен иметь один параметр. Возвращать значение из сеттера при помощи return с выражением запрещено.

class C
{
   # ошибка компиляции, сеттер должен принимать один параметр
   set x()
   {
   }

   set y(v)
   {
      # ошибка компиляции, сеттер не может возвращать значение
      return v
   }
}

Аксессоры нельзя перегружать. Добавление нескольких геттеров или сеттеров для одного и того же свойства в классе вызовет ошибку компиляции.

Если для свойства задан только геттер, то оно будет предназначено только для чтения, присвоение этому свойству вызовет исключение:

class C
{
   get x()
   {
      return 1
   }
}

var ci = new C()
ci.x                 # вернет 1
ci.x = 2             # исключение, свойство x только для чтения

Если для свойства задан только сеттер, то оно будет предназначено только для записи, чтение этого свойства вызовет исключение:

class C
{
   set x(v)
   {
      this._x = v
   }
}

var ci = new C()
ci.x = 1             # присвоит 1 полю this._x
var t = ci.x         # исключение, свойство x только для записи

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

Класс можно унаследовать от другого класса, указав базовый класс после ключевого слова extends (расширяет):

# класс D наследуется от S
class D extends S
{
}

Унаследованный класс получает все методы и свойства базового класса, и может добавить к ним свои методы и свойства.

Если унаcледованный класс определяет метод с такими же именем и сигнатурой, что и в базовом классе, то этот метод перекрывает родительский метод. Если же имена методов в базовом и унаследованном классе совпадают, но их сигнатуры отличаются, то методы перегружают друг друга.

Аксессоры свойств не могут быть перегружены, поэтому свойства унаследованного класса всегда перекрывают свойства базового класса. Кроме того, поскольку свойства являются парой аксессоров, перекрытие любого из аксессоров в унаследованном классе добавляет новое свойство с собственными аксессорами. Например, если в базовом классе определены и геттер и сеттер для свойства x, а в унаследованном - только геттер, то сеттер из базового класса не будет унаследован (он будет перекрыт несуществующим сеттером нового свойства).

class S
{
   method()
   {
      return "s"
   }

   method(x)
   {
      return x
   }

   get x()
   {
      return this._x
   }

   set x(v)
   {
      this._x = v
   }
}

class D extends S
{
   # перекрывает method без параметров из класса S
   method()
   {
      return "d"
   }

   # добавляет перегрузку к method
   method(x, y)
   {
      return x + y
   }

   # перекрывает сеттер x базового класса
   # также скрывает геттер x базового класса
   set x(v)
   {
      this._x = v + 1
   }
}

var di = new D()

di.method()             # вернет "d"
di.method(1, 2)         # вернет 3
di.x = 1                # присвоит полю this._x значение 2
var t = di.x            # исключение, в классе D свойство x не имеет геттера

Вызвать любой метод или аксессор базового класса из метода или аксессора унаследованного класса можно с помощью специальной переменной super (базовый):

class S
{
   method()
   {
      return 1
   }

   get x()
   {
      return 1
   }
}

class D extends S
{
   method()
   {
      # вызывает method класса S
      return super.method() + 1
   }

   get x()
   {
      # читает значение свойства x класса S
      return super.x + 1
   }
}

var di = new D()
di.method()             # вернет 2
di.x                    # вернет 2

Переменная super (так же как и this) видна только непосредственно внутри методов класса.

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

class S
{
   constructor(x)
   {
      this.x = x
   }
}

class D extends S
{
   constructor()
   {
      super("a")
   }
}

var di = new D()
di.x                    # строка "a"

Если в унаследованном классе конструктор отсутствует, то компилятор автоматически добавляет к нему конструктор без параметров, который вызывает конструктор без параметров базового класса:

class S
{
   constructor()
   {
      this.x = "a"
   }
}

class D extends S
{
   # неявно добавляется constructor() { super() }
}

var di = new D()
di.x                    # строка "a"

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

# Интроспекция

Класс имеет тип Class, свойство класса имеет тип Property.

Получить ссылку на именованный класс можно по его имени:

class C {}
C.name            # возвращает имя класса, строку "C"

Получить класс по его экземпляру можно с помощью оператора classof:

class C
{
}

var ci = new C()
classof ci              # вернет класс C

Оператор instanceof позволяет проверить, принадлежит ли экземпляр к указанному или унаследованному от него классу:

class S
{
}

class D extends S
{
}

var di = new D()
di instanceof S         # true
di instanceof D         # true

Далее перечислены методы и свойства Class и Property.

# Class

# Методы

# constructor(): Func (конструктор)

Возвращает ссылку на конструктор класса или nil, если класс не имеет конструктора:

class A {}
class B { constructor() {} }

A.constructor()                        # nil
B.constructor().name                   # "<constructor>"

# method(name: String): Func (метод)

Возвращает ссылку на собственный метод класса с именем name, и nil, если такого метода нет:

class C
{
   ownMethod() {}
}

C.method("ownMethod").name             # "ownMethod"
C.method("no-such-method")             # nil

# property(name: String): Property (свойство)

Возвращает ссылку на собственное свойство класса с именем name, и nil, если такого свойства нет:

class C
{
   get x() { return 1 }
}

C.property("x").name                   # "x"
C.property("no-such-property")         # nil

# methods(): Iterator (методы)

Возвращает итератор, который проходит по всем собственным методам класса в лексикографическом порядке:

class C
{
   a() {}
   b() {}
}

var r = @[]
forall (m in C.methods())
   # m содержит ссылку на метод (Func)
   r.pushBack(m.name)

return r                               # @["a", "b"]

# properties(): Iterator (свойства)

Возвращает итератор, который проходит по всем собственным свойствам класса в лексикографическом порядке:

class C
{
   get a() { return 1 }
   get b() { return 2 }
}

var r = @[]
forall (p in C.properties())
   # m содержит ссылку на свойство (Property)
   r.pushBack(m.name)

return r                               # @["a", "b"]

# Свойства

# name: String (имя)

Возвращает имя для именованного класса и "<class>" для анонимного:

class N {}
var A = class {}

N.name                                 # "N"
A.name                                 # "<class>"

# superClass: Class (базовыйКласс)

Возвращает ссылку на базовый класс, если таковой есть, иначе nil.

class S {}
class D extend S {}

D.superClass                           # вернет S
S.superClass                           # nil

# docstring: String (докстрока)

Возвращает докстроку класса:

class C "class docs"
{
    ...
}

C.docstring;   # "class docs"

# documentation: String (документация)

Возвращает документацию для класса и всех его конструкторов, методов и свойств:

class C
`
   class docs

   lorem ipsum
`
{
   constructor() `ctor docs` {}

   method() `method docs` {}

   get x() `get docs` {}

   set x(v) `set docs` {}
}

C.documentation   # C
                  #   <path-to-script>:<line>:<column>
                  #
                  #   class docs
                  #
                  #   lorem ipsum
                  #
                  #   <constructor>()
                  #     <path-to-script>:<line>:<column>
                  #     ctor docs
                  #   method()
                  #     <path-to-script>:<line>:<column>
                  #     method docs
                  #   get x()
                  #     <path-to-script>:<line>:<column>
                  #     get docs
                  #   set x(v)
                  #     <path-to-script>:<line>:<column>
                  #     set docs

# annotations: Array[Annotation] (аннотации)

Возвращает список аннотаций класса. Если у класса нет аннотаций, возвращается пустой массив. Массив является копией списка аннотаций, его изменение не влияет на аннотации самого класса.

class W {}

:[test]
class Z {}

W.annotations     # пустой массив
Z.annotations     # массив с одной аннотацией test

# Property

# Свойства

# name: String (имя)

Возвращает имя свойства.

class C
{
   get x() { return 1 }
}

C.property("x").name                   # "x"

# hasGetter: Boolean (естьГеттер)

Возвращает true, если у свойства есть геттер, и false в противном случае.

class C
{
   get x() { return 1 }
   set y(v) {}
}

C.property("x").hasGetter              # true
C.property("y").hasGetter              # false

# hasSetter: Boolean (естьСеттер)

Возвращает true, если у свойства есть сеттер, и false в противном случае.

class C
{
   get x() { return 1 }
   set y(v) {}
}

C.property("x").hasSetter              # false
C.property("y").hasSetter              # true

# getter: Overload (геттер)

Возвращает геттер данного свойства, если он есть, и nil в противном случае.

class C
{
   get x() { return 1 }
   set y(v) {}
}

C.property("x").getter                 # возвращает геттер свойства x
C.property("y").getter                 # возвращает nil

# setter: Overload (сеттер)

Возвращает сеттер данного свойства, если он есть, и nil в противном случае.

class C
{
   get x() { return 1 }
   set y(v) {}
}

C.property("y").setter                 # возвращает cеттер свойства y
C.property("x").setter                 # возвращает nil