En Verse, una clase es una plantilla para crear objetos con comportamientos y propiedades similares. Es un tipo compuesto, lo que significa que son datos agrupados de otros tipos y funciones que pueden operar con esos datos.
Las clases son jerárquicas, lo que significa que una clase puede heredar información de su padre (superclase) y compartir la información con sus hijos (subclases). Las clases pueden ser un tipo personalizado definido por el usuario. Comparable con instancia.
Por ejemplo, digamos que quieres tener varios gatos en tu juego. Un gato tiene un nombre y una edad, y pueden maullar. Tu clase de gato podría ser así:
cat := class:
Name : string
var Age : int = 0
Sound : string
Meow() : void = DisplayMessage(Sound)Las definiciones de variables que se anidan dentro de la clase definen campos de la clase. Las funciones definidas dentro de una clase también se pueden denominar métodos. Los campos y los métodos se denominan miembros de clase. En el ejemplo anterior, Sound es un campo y Meow es un método de cat.
Los campos de una clase pueden tener o no un valor predeterminado, o pueden definir solo un tipo que limita los valores que el campo puede tener. En el ejemplo anterior, `Name` no tiene un valor predeterminado, mientras que `Age` sí. El valor puede especificarse utilizando una expresión que tenga los efectos <converges>. La expresión del valor predeterminado puede no utilizar el identificador Self, el cual te explicamos a continuación.
Por ejemplo, imagina que quieres que tus gatos puedan inclinar la cabeza. Puedes inicializar una rotación inicial HeadTilt en el siguiente código usando el método IdentityRotation() porque tiene el especificador <converges> y se garantiza que se completará sin efectos secundarios.
cat := class:
...
# A valid expression
HeadTilt:rotation = IdentityRotation()Cómo construir una clase
Con una clase que defina lo que es un gato y lo que puede hacer, puedes construir una instancia de la clase a partir de un arquetipo. Un arquetipo define los valores de los campos de una clase. Por ejemplo, hagamos un gato viejo llamado Percy a partir de la clase cat:
OldCat := cat{Name := ”Percy”, Age := 20, Sound:= ”Rrrr”}En este ejemplo, el arquetipo es la parte entre { and }. No es necesario definir valores para todos los campos de la clase, pero al menos debes definir valores para todos los campos que no tienen un valor predeterminado. Si se omite algún campo, la instancia construida tendrá el valor predeterminado de ese campo.
En este caso, el campo Age de la clase cat tiene un valor predeterminado asignado de 0. Dado que el campo tiene un valor predeterminado, no es necesario proporcionar un valor para él cuando se construye una instancia de la clase. El campo es una variable, lo que significa que, aunque se proporcione un valor en el momento de la construcción, el valor de esa variable se puede cambiar después de la construcción.
En cambio, el campo Name de cat no es una variable mutable, por lo que es inmutable por defecto. Esto significa que puedes proporcionarle un valor predeterminado en el momento de la construcción, pero después de la construcción, no puede cambiar, ya que es inmutable.
Como una clase en Verse es una plantilla, puedes crear tantas instancias como quieras a partir de la clase cat. Hagamos un gatito que se llame Flash:
Kitten := cat{Name := ”Flash”, Age := 1, Sound := ”Mew”}Cómo acceder a los campos
Ahora que tienes algunas instancias de cat, puedes acceder al campo Name de cada gato con OldCat.Name o Kitten.Name, y llamar al método Meow de cada gato con OldCat.Meow() o Kitten.Meow().
Ambos gatos tienen los mismos campos con nombre, pero esos campos tienen valores diferentes. Por ejemplo, OldCat.Meow() y Kitten.Meow() se comportan de manera distinta porque sus campos Sound tienen valores diferentes.
Self
Self es un identificador especial en Verse que puede utilizarse en un método de clase para referirse a la instancia de la clase sobre la que se ha llamado al método. Puedes referirte a otros campos de la instancia a la que se ha llamado al método sin utilizar Self, pero si quieres referirte a la instancia en su conjunto, debes utilizar Self.
Por ejemplo, si DisplayMessage requiere un argumento para saber a qué mascota asociar un mensaje:
DisplayMessage(Pet:pet, Message:string) : void = …
cat := class:
…
Meow() : void = DisplayMessage(Self, Sound)Si quisieras inicializar una versión más ruidosa del maullido de tu gato, podrías basarte en la variable Sound que ya has configurado. Sin embargo, esto no funcionará en el siguiente código, porque LoudSound no puede hacer referencia al miembro de instancia Sound, ya que las expresiones de valor predeterminado no pueden utilizar el 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)Subclases y herencia
Las clases pueden heredar de una superclase, que incluye todos los campos de la superclase en la clase que hereda. Se dice que estas clases son una subclase de la superclase. Por ejemplo:
pet := class:
Name : string
var Age : int = 0
cat := class(pet):
Sound : string
Meow() : void = DisplayMessage(Self, Sound)
dog := class(pet):
Trick : string
Aquí, el uso de class(pet) al definir cat y dog declara que heredan de la clase pet. Dicho de otra forma, hay subclases de pet.
Esto tiene varias ventajas:
Como tanto los gatos como los perros tienen nombre y edad, esos campos solo tienen que definirse una vez, en la clase
pet. Tanto el campocatcomo el dedogheredarán dichos campos.La clase
petpuede utilizarse como tipo para referirse a una instancia de cualquier subclase depet. Por ejemplo, si quieres escribir una función que solo necesite el nombre de una mascota, puedes escribir dicha función una vez tanto para los gatos como para los perros, y para cualquier otra subclasepetque vayas a incluir en el futuro:VerseIncreaseAge(Pet : pet) : void= set Pet.Age += 1
Para obtener más información, consulta la página Subclase.
Anulaciones
Cuando defines una subclase, puedes anular los campos definidos en la superclase para hacer su tipo más específico, o cambiar su valor por defecto. Para ello, debes escribir de nuevo la definición del campo en la subclase, pero con el especificador <override> en su nombre. Por ejemplo, podemos añadir un campo Lives a pet con un valor predeterminado de 1, y anular el valor predeterminado de los gatos para que sea 9:
pet := class:
…
Lives : int = 1
cat := class(pet):
…
Lives<override> : int = 9Llamadas a método
Cuando accedes a un campo de una instancia de clase, accedes al valor de esa instancia para el campo. En el caso de los métodos, el campo es una función, y al anularlo se sustituye el valor del campo por una nueva función. Al llamar a un método se llama al valor del campo. Esto significa que el método llamado viene determinado por la instancia. Ten en cuenta los siguientes ejemplos:
pet := class:
…
OnHearName() : void = {}
cat := class(pet):
…
OnHearName<override>() : void = Meow()
dog := class(pet):
…
Si escribes CallFor(Percy), llamará al método OnHearName definido por cat. Si escribes CallFor(Fido), en el que Fido es una instancia de la clase dog, entonces llamará al método OnHearName definido por dog.
Especificadores de visibilidad
Puedes añadir especificadores de visibilidad a los campos y métodos de la clase para controlar quién tiene acceso a ellos. Por ejemplo, puedes añadir el especificador private al campo Sound para que solo la clase propietaria pueda acceder a ese campo privado.
cat := class:
…
Sound<private> : string
MrSnuffles := cat{Sound := "Purr"}
MrSnuffles.Sound # Error: cannot access a private fieldLos siguientes son todos los especificadores de visibilidad que se pueden utilizar con las clases:
public: acceso sin restricciones.
internal: acceso limitado al módulo actual. Esta es la visibilidad predeterminada.
protected: acceso limitado a la clase actual y todas sus subclases.
private: acceso limitado a la clase actual.
Especificadores de acceso
Puedes añadir especificadores de acceso a una clase para controlar quién puede construirlas. Esto es útil, por ejemplo, si quieres asegurarte de que una instancia de una clase solo se puede construir en un ámbito determinado.
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 constructorLlamar al constructor de la clase cat fuera de su módulo pets fallará porque la palabra clave class está marcada como interna. Esto es cierto a pesar de que el propio identificador de la clase está marcado como público, lo que significa que cat puede ser referenciado por código fuera del módulo pets.
A continuación, se indican todos los especificadores de acceso que se pueden utilizar con la palabra clave class:
public: acceso sin restricciones. Este es el acceso por defecto.
internal: acceso limitado al módulo actual.
Especificador `concrete`
Cuando una clase tiene el especificador concrete, es posible construirla con un arquetipo vacío, como cat{}. Esto significa que cada campo de la clase debe tener un valor predeterminado. Además, toda subclase de una clase `concrete` debe ser a su vez `concrete`.
Por ejemplo:
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 = 0Una clase concrete solo puede heredar directamente de una clase abstracta si ambas clases están definidas en el mismo módulo. Sin embargo, no se mantiene de forma transitiva: una clase concrete puede heredar directamente de una segunda clase concrete en otro módulo, en la que esa segunda clase concrete hereda directamente de una clase abstract en su módulo.
Especificador `unique`
El especificador unique puede aplicarse a una clase para convertirla en una clase única. Para construir una instancia de una clase única, Verse asigna una identidad única para la instancia resultante. Esto permite comparar instancias de clases únicas para la igualdad mediante la comparación de sus identidades. Las clases que no tengan el especificador unique no tienen esta identidad, así que solo se pueden comparar para determinar su igualdad en función de los valores de sus campos.
Esto significa que las clases `unique` pueden compararse con los operadores = y <>, y que son subtipos del tipo comparable.
Por ejemplo:
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`
Solo puedes utilizar el especificador final en clases y campos de clases.
Cuando una clase tiene el especificador final, no se puede crear una subclase de la clase. En el siguiente ejemplo, no se puede utilizar la clase pet como superclase, porque la clase tiene el especificador final.
pet := class<final>():
…
cat := class(pet): # Error: cannot subclass a “final” class
…Cuando un campo tiene el especificador final, no se puede anular el campo en una subclase. En el siguiente ejemplo, la clase cat no puede anular el campo Owner, porque el campo tiene el especificador final.
pet := class():
Owner<final> : string = “Andy”
cat := class(pet):
Owner<override> : string = “Sid” # Error: cannot override “final” fieldCuando un método tiene el especificador `final`, no se puede anular el método en una subclase. En el siguiente ejemplo, la clase cat no puede anular el método GetName(), porque el método tiene el especificador `final`.
pet := class():
Name : string
GetName<final>() : string = Name
cat := class(pet):
…
GetName<override>() : string = # Error: cannot override “final” method
…Expresiones de bloque en el cuerpo de una clase
Puedes utilizar expresiones de bloque en el cuerpo de una clase. Cuando se crea una instancia de la clase, las expresiones block se ejecutan en el orden en que están definidas. Las funciones llamadas en expresiones block en el cuerpo de la clase no pueden tener el efecto NoRollback.
Como ejemplo, añadamos dos expresiones block al cuerpo de la clase cat y agreguemos el especificador de especificador de efecto transacts al método Meow() porque el efecto predeterminado para los métodos tiene el efecto NoRollback.
cat := class():
Name : string
Age : int
Sound : string
Meow()<transacts> : void =
DisplayOnScreen(Sound)
block:
Self.Meow()
Cuando se crea la instancia de la clase cat, OldCat, se ejecutan las dos expresiones block: el gato dirá primero «Rrrr» y, a continuación, «Garfield» se imprimirá en el registro de salida.
Interfaces
Las interfaces son una forma limitada de clases que solo pueden contener métodos que no tienen valor. Las clases solo pueden heredar de una única clase, pero pueden hacerlo de muchas interfaces.
Tipo persistente
Una clase es persistente cuando:
Se define con el especificador `persistable`.
Se define con el especificador final, porque las clases de persistencia no pueden tener subclases.
No es único.
No tiene superclase.
No es paramétrico.
Solo contiene miembros que también son persistentes.
No tiene miembros variables.
Cuando una clase es persistente, significa que puedes usarla en las variables weak_map de tu ámbito de módulo y hacer que sus valores se mantengan entre sesiones de juego. Para obtener más información sobre la persistencia en Verse, consulta la sección Cómo utilizar datos persistentes en Verse.
El siguiente ejemplo de Verse muestra cómo se puede definir un perfil de jugador personalizado en una clase que se puede almacenar, actualizar y a la que se puede acceder posteriormente para un jugador. La clase player_profile_data almacena información de un jugador, como los PE que ha ganado, su rango y las misiones que ha completado.
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{}