Em Verse, uma classe é um modelo para criar objetos com comportamentos e propriedades semelhantes. É um tipo composto, o que significa que são dados agrupados de outros tipos e funções que podem operar nesses dados.
As classes são hierárquicas, ou seja, uma classe pode herdar informações de seu pai (superclasse) e compartilhar suas informações com seus filhos (subclasses). As classes podem ser um tipo personalizado definido pelo usuário. Compare com instância.
Por exemplo, digamos que você que ter vários gatos no seu jogo. Um gato tem nome e idade, além de poder miar. A classe do seu gato pode ficar assim:
cat := class:
Name : string
var Age : int = 0
Sound : string
Meow() : void = DisplayMessage(Sound)As definições de variáveis aninhadas dentro da classe definem os campos da classe. As funções definidas dentro de uma classe também podem ser chamadas de métodos. Campos e métodos são chamados de membros das classes. No exemplo acima, Sound é um campo e Meow é um método de cat.
Os campos de uma classe podem ou não ter um valor padrão, ou podem apenas definir um tipo que limita os valores que o campo pode ter. No exemplo acima, "Name" não tem um valor padrão, mas "Age" tem. O valor pode ser especificado usando uma expressão que tenha os efeitos <converges>. A expressão de valor padrão pode não usar o identificador "Self", sobre o qual você aprenderá a seguir.
Por exemplo, digamos que você quer que seus gatos possam inclinar suas cabeças. Você pode inicializar uma rotação inicial HeadTilt no código a seguir usando o método IdentityRotation(), porque ele tem o especificador <converges> e será concluído sem efeitos colaterais.
cat := class:
...
# A valid expression
HeadTilt:rotation = IdentityRotation()Como criar uma classe
Com uma classe que define o que é um gato e o que esse gato pode fazer, você pode criar uma instância da classe a partir de um arquétipo. Um arquétipo define os valores dos campos da classe. Por exemplo, vamos criar um gato velho chamado Percy a partir da classe cat.
OldCat := cat{Name := ”Percy”, Age := 20, Sound:= ”Rrrr”}Neste exemplo, o arquétipo é a parte entre { and }. Não é preciso definir valores para todos os campos da classe, mas pelo menos definir valores para todos os campos que não tiverem um valor padrão. Se algum campo for omitido, a instância criada terá o valor padrão para esse campo.
Neste caso, o campo Age da classe cat (gato) tem um valor padrão atribuído (0). Já que o campo tem um valor padrão, não é necessário fornecer um valor para ele ao construir uma instância da classe. O campo é uma variável, o que significa que, embora você possa fornecer um valor no momento da construção, o valor dessa variável pode ser alterado após a construção.
Por outro lado, o campo Name de cat não é uma variável mutável, por isso é imutável por padrão. Isso significa que você pode fornecer um valor padrão a ele no momento da construção, mas depois, o valor não pode mudar, ou seja, é imutável.
Como uma classe em Verse é um modelo, você pode criar quantas instâncias quiser da classe cat. Vamos criar um gatinho chamado Flash:
Kitten := cat{Name := ”Flash”, Age := 1, Sound := ”Mew”}Como acessar campos
Agora que já temos algumas instâncias de cat, você poderá acessar o campo Name de cada gato com OldCat.Name ou Kitten.Name, além de chamar o método Meow de cada gato com OldCat.Meow() ou Kitten.Meow().
Ambos os gatos têm campos de mesmo nome, mas com valores diferentes. Por exemplo, OldCat.Meow() e Kitten.Meow() se comportam de maneira diferente porque seus campos Sound têm valores diferentes.
Self
Self é um identificador especial em Verse que pode ser usado em um método de classe para fazer referência à instância da classe na qual o método foi chamado. Você pode referenciar outros campos da instância na qual o método foi chamado sem usar Self, mas se quiser referenciar a instância como um todo, é preciso usar Self.
Por exemplo, se DisplayMessage precisar de um argumento para associar uma mensagem a um pet:
DisplayMessage(Pet:pet, Message:string) : void = …
cat := class:
…
Meow() : void = DisplayMessage(Self, Sound)Se você quisesse inicializar uma versão mais alta dos miados dos seus gatos, poderia pensar em usar a variável Sound que já foi configurada. Mas isso não funcionará no código a seguir, porque LoudSound não pode fazer referência ao membro da instância Sound, pois expressões de valor padrão não podem usar o identificador 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)Subclasses e herança
Classes podem herdar de uma superclasse, o que inclui todos os campos da superclasse na classe que herda. Essas classes são conhecidas por serem uma subclasse da superclasse. Por exemplo:
pet := class:
Name : string
var Age : int = 0
cat := class(pet):
Sound : string
Meow() : void = DisplayMessage(Self, Sound)
dog := class(pet):
Trick : string
Aqui, o uso de class(pet) ao definir cat e dog declara que eles herdam da classe pet. Em outras palavras, são subclasses de pet.
Há várias vantagens nisso:
Já que cães e gatos têm nomes e idades, esses campos precisam ser definidos apenas uma vez, na classe
pet. Tanto o campocatquantodogherdarão esses campos.A classe
petpode ser usada como um tipo para fazer referência a uma instância de qualquer subclasse depet. Por exemplo, se você quiser escrever uma função que apenas exige o nome de um pet, pode escrever a função uma vez para cães e gatos, bem como qualquer outra subclasse depetque possa ser acrescentada futuramente:VerseIncreaseAge(Pet : pet) : void= set Pet.Age += 1
Consulte mais informações na página Subclasse.
Substituições
Ao definir uma subclasse, você poderá substituir campos definidos na subclasse para tornar seu tipo mais específico, ou alterar seu valor padrão. Para isso, é preciso escrever a definição do campo na subclasse outra vez, mas com o especificador <override> em seu nome. Por exemplo, você pode adicionar um campo Lives (vidas) ao pet com um valor padrão de 1, bem como substituir o valor padrão para que seja 9 para gatos:
pet := class:
…
Lives : int = 1
cat := class(pet):
…
Lives<override> : int = 9Chamadas de métodos
Ao acessar um campo de uma instância de classe, você acessa o valor dessa instância para o campo. Para métodos, o campo é uma função, e sua substituição altera o valor do campo para uma nova função. A chamada de um método chama o valor do campo. Isso significa que o método chamado é determinado pela instância. Considere o exemplo a seguir:
pet := class:
…
OnHearName() : void = {}
cat := class(pet):
…
OnHearName<override>() : void = Meow()
dog := class(pet):
…
Se você escrever CallFor(Percy), o método OnHearName será chamado conforme definido por cat. Se você escrever CallFor(Fido), em que Fido é uma instância da classe dog, então o método OnHearName será chamado conforme definido por dog.
Especificadores de visibilidade
Você pode adicionar especificadores de visibilidade aos campos e métodos da classe para controlar quem tem acesso a eles. Por exemplo, você pode adicionar o especificador private ao campo Sound para que apenas a classe proprietária tenha acesso ao campo privado.
cat := class:
…
Sound<private> : string
MrSnuffles := cat{Sound := "Purr"}
MrSnuffles.Sound # Error: cannot access a private fieldConfira a seguir todos os especificadores de visibilidade que podem ser usados com classes:
public: acesso irrestrito.
internal: acesso limitado ao módulo atual. Esta é a visibilidade padrão.
protected: acesso limitado à classe atual e a quaisquer subclasses.
private: acesso limitado à classe atual.
Especificadores de acesso
É possível adicionar especificadores de acesso a uma classe para controlar quem pode criá-las. O que é útil, por exemplo, se você quiser garantir que uma instância de uma classe só possa ser criada em um determinado escopo.
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 constructorA chamada do construtor para a classe cat fora de seu módulo pets falhará porque a palavra-chave class está marcada como "internal". Isso é válido mesmo que o próprio identificador de classe esteja marcado como "public", o que significa que cat pode ser referenciado por código fora do módulo pets.
Confira a seguir todos os especificadores de acesso que você pode usar com a palavra-chave "class":
public: acesso irrestrito. Este é o acesso padrão.
internal: acesso limitado ao módulo atual.
Especificador concreto
Quando uma classe tem o especificador concrete, é possível construí-la com um arquétipo vazio, como cat{}. O que significa que cada campo da classe deve ter um valor padrão. Além disso, todas as subclasses de uma classe concreta devem ser concretas.
Por exemplo:
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 = 0Uma classe concrete só poderá herdar diretamente de uma classe abstract se ambas as classes estiverem definidas no mesmo módulo. Entretanto, a transitividade não é mantida: uma classe concrete pode herdar diretamente de uma segunda classe concrete em outro módulo, no qual essa segunda classe concrete herda diretamente de uma classe abstract nesse módulo.
Especificador unique
O especificador unique pode ser aplicado a uma classe para torná-la uma classe única. Para construir uma instância de uma classe única, o Verse aloca uma identidade exclusiva para a instância resultante. Assim, é possível comparar instâncias de classes exclusivas em termos de igualdade ao comparar suas identidades. Classes sem o especificador unique não têm essa identidade, por isso, só podem ser comparadas em termos de igualdade baseadas nos valores dos seus campos.
Isso significa que classes únicas podem ser comparadas com os operadores = e <>, e são subtipos do tipo comparable.
Por exemplo:
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 equalEspecificador final
Você poderá usar o especificador final apenas em classes e campos de classes.
Quando uma classe tem o especificador final, não é possível criar uma subclasse da classe. No exemplo a seguir, não é possível usar a classe pet como uma superclasse, pois a classe tem o especificador final.
pet := class<final>():
…
cat := class(pet): # Error: cannot subclass a “final” class
…Quando um campo tem o especificador final, não é possível substituir o campo em uma subclasse. No exemplo a seguir, a classe do "cat" não pode substituir o campo Owner, pois o campo tem o especificador final.
pet := class():
Owner<final> : string = “Andy”
cat := class(pet):
Owner<override> : string = “Sid” # Error: cannot override “final” fieldQuando um método tiver o especificador final, não é possível substituir o método em uma subclasse. No exemplo a seguir, a classe "cat" não pode substituir o método GetName(), pois o método tem o especificador final.
pet := class():
Name : string
GetName<final>() : string = Name
cat := class(pet):
…
GetName<override>() : string = # Error: cannot override “final” method
…Expressões block no corpo de uma classe
Você pode usar expressões block no corpo de uma classe. Ao criar uma instância da classe, as expressões block são executadas na ordem em que são definidas. Funções chamadas em expressões block no corpo da classe não podem ter o efeito NoRollback.
Por exemplo: vamos adicionar duas expressões block ao corpo da classe cat e adicionar o especificador do efeito transacts ao método Meow(), pois o efeito padrão para os métodos tem o efeito NoRollback.
cat := class():
Name : string
Age : int
Sound : string
Meow()<transacts> : void =
DisplayOnScreen(Sound)
block:
Self.Meow()
Quando a instância da classe cat, OldCat, é criada, as duas expressões block são executadas: o gato primeiro dirá "Rrrr", depois "Garfield" será impresso no Log de Saída.
Interfaces
Interfaces são uma forma limitada de classes que só pode conter métodos que não têm um valor. Classes podem herdar apenas de uma única outra classe, mas podem herdar de qualquer número de interfaces.
Tipo persistente
Uma classe é persistente quando:
Definido com o especificador persistente.
Definida com o especificador final, porque classes persistentes não podem ter subclasses.
Não é unique.
Não tem uma superclasse.
Não é parametric.
Contém apenas membros que também são persistentes.
Não tem membros variáveis.
Quando uma classe é persistente, você pode usá-la nas variáveis weak_map com escopo de módulo e fazer com que seus valores persistam entre as sessões de jogo. Consulte mais detalhes sobre a persistência em Verse em Como usar dados persistentes em Verse.
O exemplo em Verse a seguir mostra como você pode definir um perfil de jogador personalizado em uma classe que pode ser armazenada, atualizada e acessada posteriormente por um jogador. A classe player_profile_data as armazena informações de um jogador, como EXP ganho, ranque e as tarefas concluídas.
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{}