В Verse класс — это шаблон для создания объектов с похожим поведением и свойствами. Это составной тип, что означает, что он объединяет данные других типов и функции, которые могут работать с этими данными.
Классы являются иерархическими, то есть класс может наследовать информацию от родительского класса (суперкласса) и передавать эту информацию своим дочерним элементам (подклассам). Классы могут представлять собой настраиваемые типы, определяемые пользователем. Не путать с экземпляром.
Представьте, что вы хотите, чтобы в вашей игре было несколько кошек. У кошки есть кличка и возраст, и она умеет мяукать. Ваш класс кошек может выглядеть так:
cat := class:
Name : string
var Age : int = 0
Sound : string
Meow() : void = DisplayMessage(Sound)Определения переменных, которые вложены в класс, определяют поля класса. Функции, определённые внутри класса, также называются методами. Поля и методы называются составляющими класса. В примере выше Sound является полем, а Meow — методом cat.
Поля класса могут как иметь, так и не иметь значение по умолчанию или могут определять только тип, ограничивающий значения, которые может содержать поле. В приведённом выше примере переменная Name не имеет значения по умолчанию, а Age имеет. Значение можно указать с помощью выражения, содержащего эффекты <converges>. Выражение со значением по умолчанию не должно использовать идентификатор Self, описанный ниже.
Разберём пример, когда нужно, чтобы кошки могли наклонять голову. Вы можете выполнить инициализацию начального поворота HeadTilt в следующем коде, используя метод IdentityRotation(), так как в нём имеется спецификатор <converges> и он гарантированно завершается без побочных эффектов.
cat := class:
...
# A valid expression
HeadTilt:rotation = IdentityRotation()Создание класса
Имея класс, определяющий, что такое кошка и что она может делать, вы можете создать экземпляр класса из архетипа. Архетип определяет значения полей класса. Например, создадим старого кота по имени Перси из класса cat:
OldCat := cat{Name := ”Percy”, Age := 20, Sound:= ”Rrrr”}В этом примере архетип — это часть между { and }. Он не обязательно определяет значения всех полей класса, но должен, по крайней мере, определять значения всех полей, которые не имеют значений по умолчанию. Если какое-либо поле опущено, то созданный экземпляр будет иметь значение этого поля по умолчанию.
В нашем примере полю Age класса cat присвоено значение по умолчанию (0). Поскольку поле имеет значение по умолчанию, нет необходимости указывать его значение при создании экземпляра класса. Поле является переменной, то есть, несмотря на то, что вы можете указать значение переменной во время создания, его можно изменить и после создания.
Напротив, поле Name в cat не является изменяемой переменной и поэтому по умолчанию не может быть изменено. Это означает, что можно задать его значение по умолчанию во время создания, но после создания его нельзя будет изменить — это неизменяемое поле.
Поскольку класс в Verse является шаблоном, можно создать столько экземпляров класса cat, сколько требуется. Давайте создадим котёнка по имени Флэш:
Kitten := cat{Name := ”Flash”, Age := 1, Sound := ”Mew”}Обращение к полям
Теперь, когда у нас есть несколько экземпляров cat, можно обратиться к полю Name каждой кошки с помощью OldCat.Name или Kitten.Name и вызвать метод Meow каждой кошки с помощью OldCat.Meow() или Kitten.Meow().
Обе кошки имеют поля с одинаковыми именами, но эти поля содержат разные значения. Например, OldCat.Meow() и Kitten.Meow() возвращают разный результат, потому что их поля Sound содержат разные значения.
Self
Self — это специальный идентификатор в языке Verse, который можно использовать в методе класса для ссылки на экземпляр класса, для которого вызывается метод. Можно ссылаться на другие поля экземпляра, из которого был вызван метод, без использования Self, но если нужно сослаться на экземпляр в целом, то следует использовать Self.
Например, если DisplayMessage требуется аргумент, задающий питомца, с которым нужно связать сообщение:
DisplayMessage(Pet:pet, Message:string) : void = …
cat := class:
…
Meow() : void = DisplayMessage(Self, Sound)Если вам потребуется инициализировать более громкую версию мяуканья кошек, вы, скорее всего, заходите использовать уже настроенную переменную Sound. Однако в следующем коде это не сработает, поскольку LoudSound не может ссылаться на составляющую элемент экземпляра Sound из-за того, что выражения со значениями по умолчанию не используют идентификатор Self.
cat := class:
...
Sound : string
Meow() : void = DisplayMessage(Self, Sound)
# The following will fail since default class values
# can't reference Self
LoudSound : string = "Loud " + Self.Sound
LoudMeow() : void = DisplayMessage(Self, LoudSound)Подклассы и наследование
Классы могут наследоваться от суперкласса, который включает все поля суперкласса в наследующий класс. Такие классы называются подклассами суперкласса. Пример:
pet := class:
Name : string
var Age : int = 0
cat := class(pet):
Sound : string
Meow() : void = DisplayMessage(Self, Sound)
dog := class(pet):
Trick : string
Здесь использование class(pet) при определении cat и dog объявляет, что они наследуются от класса pet. Иначе говоря, они являются подклассами pet.
Это даёт несколько преимуществ:
Поскольку и у кошек, и у собак есть клички и возраст, эти поля должны быть определены только один раз, в классе
pet. Поля классовcatиdogнаследуют эти поля.Класс
petможно использовать в качестве типа для ссылки на экземпляр любого подклассаpet. Например, если создаётся функция, которой нужна только кличка питомца, то можно написать её один раз для кошек и собак, а также для любых других подклассовpet, которые могут быть добавлены в будущем:VerseIncreaseAge(Pet : pet) : void= set Pet.Age += 1
Дополнительную информацию см. на странице Подкласс.
Переопределения
При определении подкласса можно переопределить поля, определённые в суперклассе, чтобы сделать их тип более конкретным, или изменить их значения по умолчанию. Для этого нужно снова написать определение поля в подклассе, но уже со спецификатором <override> для его имени. Например, можно добавить поле Lives к pet со значением по умолчанию 1 и переопределить значение по умолчанию на 9 для кошек:
pet := class:
…
Lives : int = 1
cat := class(pet):
…
Lives<override> : int = 9Вызов методов
При обращении к полю экземпляра класса вы получаете доступ к значению поля этого экземпляра. Для методов поле является функцией и его переопределение заменяет значение поля новой функцией. При вызове метода вызывается значение поля. Это означает, что вызываемый метод определяется экземпляром. Рассмотрим следующий пример:
pet := class:
…
OnHearName() : void = {}
cat := class(pet):
…
OnHearName<override>() : void = Meow()
dog := class(pet):
…
Если написать CallFor(Percy), то будет вызван метод OnHearName, определённый в cat. Если написать CallFor(Fido), где Fido является экземпляром класса dog, то будет вызван метод OnHearName, определённый в dog.
Спецификаторы видимости
Можно добавить спецификаторы видимости к полям и методам класса, чтобы определить, какая часть кода может к ним обращаться. Например, можно добавить спецификатор private к полю Sound, чтобы к этому частному полю мог обращаться только класс-владелец.
cat := class:
…
Sound<private> : string
MrSnuffles := cat{Sound := "Purr"}
MrSnuffles.Sound # Error: cannot access a private fieldНиже перечислены все спецификаторы видимости, которые можно использовать с классами:
public: обращение не ограничено.
internal: обращение ограничено текущим модулем. Это видимость по умолчанию.
protected: обращение ограничено текущим классом и любыми подклассами.
private: обращение ограничено текущим классом.
Спецификаторы доступа
Вы можете добавлять спецификаторы доступа к классу, чтобы определять, где можно создавать экземпляры. Это пригодится, к примеру, когда необходимо, чтобы экземпляр класса создавался только в определённой области видимости.
pets := module:
cat<public> := class<internal>:
Sound<public> : string = "Meow"
GetCatSound(InCat:pets.cat):string =
return InCat.Sound # Valid: References the cat class but does not call its constructor
MakeCat():void =
MyNewCat := pets.cat{} # Error: Invalid access of internal class constructorВызов конструктора для класса cat за пределами модуля pets приведёт к ошибке, поскольку ключевое слово class имеет спецификатор доступа internal. Ошибка возникнет несмотря на то, что идентификатор класса имеет спецификатор доступа public, т. е. к классу cat можно обращаться в коде за пределами модуля pets.
Ниже перечислены все спецификаторы доступа, которые можно использовать с ключевым словом class:
public: обращение не ограничено. Это доступ по умолчанию.
internal: обращение ограничено текущим модулем.
Спецификатор concrete
Когда класс имеет спецификатор concrete, его можно создать с пустым архетипом, таким как cat{}. Это означает, что каждое поле класса должно иметь значение по умолчанию. Кроме того, каждый подкласс класса concrete сам должен иметь спецификатор concrete.
Пример:
class1 := class<concrete>:
Property : int = 0
# Error: Property isn't initialized
class2 := class<concrete>:
Property : int
# Error: class3 must also have the <concrete> specifier since it inherits from class1
class3 := class(class1):
Property : int = 0Класс concrete может наследоваться непосредственно от класса abstract, только если оба класса определены в одном модуле. Однако это условие не транзитивно — класс concrete может наследоваться непосредственно от второго класса concrete в другом модуле, где этот второй класс concrete наследуется непосредственно от класса abstract в своём модуле.
Спецификатор unique
Спецификатор unique можно применить к классу, чтобы сделать его уникальным. Чтобы создать экземпляр уникального класса, язык Verse выделяет уникальный идентификатор для полученного экземпляра. Это позволяет определить, одинаковы экземпляры уникальных классов или нет, сравнивая их идентификаторы. Классы без спецификатора unique не имеют таких идентификаторов, поэтому их можно оценивать на эквивалентность только на основе значений их полей.
Это означает, что уникальные классы можно сравнивать с помощью операторов = и <> и они являются подтипами типа comparable.
Пример:
unique_class := class<unique>:
Field : int
Main()<decides> : void =
X := unique_class{Field := 1}
X = X # X is equal to itself
Y := unique_class{Field := 1}
X <> Y # X and Y are unique and therefore not equalСпецификатор final
Спецификатор final можно использовать только для классов и полей классов.
Когда у класса есть спецификатор final, невозможно создать подкласс этого класса. В следующем примере невозможно использовать класс pet в качестве суперкласса, потому что он имеет спецификатор final.
pet := class<final>():
…
cat := class(pet): # Error: cannot subclass a “final” class
…Когда поле имеет спецификатор final, переопределить это поле в подклассе невозможно. В следующем примере класс cat не может переопределить поле Owner, потому что оно имеет спецификатор final.
pet := class():
Owner<final> : string = “Andy”
cat := class(pet):
Owner<override> : string = “Sid” # Error: cannot override “final” fieldКогда метод имеет спецификатор `final`, невозможно переопределить метод в подклассе. В следующем примере класс cat не может переопределить метод GetName(), потому что он имеет спецификатор final.
pet := class():
Name : string
GetName<final>() : string = Name
cat := class(pet):
…
GetName<override>() : string = # Error: cannot override “final” method
…Блочные выражения в теле класса
Вы можете использовать блочные выражения в теле класса. При создании экземпляра класса выражения block выполняются в том порядке, в котором они определены. Функции, вызываемые в выражениях block в теле класса, не могут иметь эффекта NoRollback (без отката).
В качестве примера добавим два выражения block в тело класса cat и добавим спецификатор эффекта transacts к методу Meow(), потому что по умолчанию для методов действует эффект NoRollback.
cat := class():
Name : string
Age : int
Sound : string
Meow()<transacts> : void =
DisplayOnScreen(Sound)
block:
Self.Meow()
Когда создаётся экземпляр класса cat, OldCat, выполняются два блочных выражения: сначала кот говорит «Рррр», а затем в журнале выходных данных выводится «Гарфилд».
Интерфейсы
Интерфейсы — это ограниченная форма классов, которые могут содержать только методы, не имеющие значения. Классы могут наследоваться только от одного (другого) класса, но от любого количества интерфейсов.
Тип persistable
Класс является сохраняемым в следующих случаях:
Определяется с помощью спецификатора persistable.
Определяется с помощью спецификатора final, потому что сохраняемые классы не могут иметь подклассы.
Не unique.
Не имеет суперкласса.
Не параметрический.
Содержит только составляющие, которые также являются сохраняемыми.
У него нет переменных составляющих.
«Класс является сохраняемым» означает, что вы можете использовать их в переменных weak_map, входящих в область видимости модуля, и их значения будут сохраняться в течение всех игровых сеансов. Подробнее о сохраняемых элементах в Verse см. в статье Использование сохраняемых данных в Verse.
В следующем примере Verse показано, как можно определить собственный профиль игрока в классе, который можно будет хранить, обновлять и открываться игроком позднее. Класс player_profile_data хранит информацию для игрока, такую как полученный им опыт, его рейтинг и выполненные им задания.
player_profile_data := class<final><persistable>:
Version:int = 1
Class:player_class = player_class.Villager
XP:int = 0
Rank:int = 0
CompletedQuestCount:int = 0
QuestHistory:[]string = array{}