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 clase principal (superclase) y compartir su información con sus clases secundarias (subclases). Las clases pueden ser un tipo personalizado definido por el usuario. Compáralo con instancia.
Por ejemplo, digamos que quieres tener varios gatos en tu juego. Un gato tiene un nombre y una edad, y puede maullar. Tu clase de gato podría verse de la siguiente manera:
cat := class:
Name : string
var Age : int = 0
Sound : string
Meow() : void = DisplayMessage(Sound)Las definiciones de variables anidadas dentro de la clase definen los campos de la clase. Las funciones definidas dentro de una clase también pueden denominarse 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 puede tener el campo. En el ejemplo anterior, Name no tiene un valor predeterminado, pero Age sí. El valor puede especificarse mediante una expresión que tenga los <converges> efectos. La expresión de valor por defecto podría no utilizar el identificador Self, del cual aprenderás más a continuación.
Por ejemplo, digamos que quieres que tus gatos puedan inclinar sus cabezas. Puedes inicializar una rotación inicial de HeadTilt en el siguiente código mediante el método IdentityRotation() porque tiene el <converges> especificador y se garantiza que se completará sin efectos secundarios.
cat := class:
...
# A valid expression
HeadTilt:rotation = IdentityRotation()Construcción de una clase
Con una clase que define 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, creemos 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 (0). Dado que el campo tiene un valor predeterminado, no es necesario ofrecer un valor para él cuando se construye una instancia de la clase. El campo es una variable, lo que significa que, aunque se brinde un valor en el momento de la construcción, el valor de esa variable se puede cambiar después de la construcción.
Por el contrario, el campo Name de cat no es una variable mutable, por lo que es inmutable de forma predeterminada. 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: es inmutable.
Como una clase en Verse es una plantilla, puedes crear tantas instancias como quieras a partir de la clase cat. Creemos 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 diferente porque sus campos Sound tienen valores diferentes.
Self
Self es un identificador especial en Verse que puede usarse en un método de clase para referirse a la instancia de la clase a la que se llamó el método. Puedes referirte a otros campos de la instancia a la que se llamó el método sin usar Self, pero si deseas referirte a la instancia en su totalidad, debes usar Self.
Por ejemplo, si DisplayMessage requiere un argumento para asociar un mensaje con una mascota:
DisplayMessage(Pet:pet, Message:string) : void = …
cat := class:
…
Meow() : void = DisplayMessage(Self, Sound)Si quieres inicializar una versión más ruidosa del maullido de tus gatos, podrías pensar en valerte de la variable Sound que ya estableciste. Sin embargo, esto no funcionará en el código que se muestra a continuación. La razón es que LoudSound no puede referenciar al miembro de instancia Sound porque las expresiones de valor por defecto 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 heredera. Se dice que tales 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. En otras palabras, son subclases de pet.
Esto tiene algunas ventajas:
Dado que tanto los gatos como los perros tienen nombres y edades, esos campos solo tienen que definirse una vez, en la clase
de mascota. Tanto el campo delgatocomo el delperroheredarán esos campos.La clase
petse puede utilizar como un tipo para hacer referencia a una instancia de cualquier subclase depet. Por ejemplo, si deseas escribir una función que solo necesita el nombre de una mascota, puedes escribir dicha función una vez para los gatos y los perros, y para cualquier otra subclasepetque introduzcas en el futuro:VerseIncreaseAge(Pet : pet) : void= set Pet.Age += 1
Para más información, consulta la página Subclase.
Anulaciones
Al definir una subclase, puedes anular los campos definidos en la superclase para hacer que su tipo sea más específico o cambiar su valor predeterminado. Para hacerlo, debes escribir de nuevo la definición del campo en tu subclase, pero con el especificador <override> en su nombre. Por ejemplo, puedes añadir un campo Lives a pet con un valor predeterminado de 1 y anular el valor predeterminado para que los gatos tengan un valor de 9:
pet := class:
…
Lives : int = 1
cat := class(pet):
…
Lives<override> : int = 9Llamadas a métodos
Al acceder 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 que se llama está determinado por la instancia. Veamos el siguiente ejemplo:
pet := class:
…
OnHearName() : void = {}
cat := class(pet):
…
OnHearName<override>() : void = Meow()
dog := class(pet):
…
Si escribes CallFor(Percy), llamará al método OnHearName tal como lo define cat. Si escribes CallFor(Fido) donde Fido es una instancia de la clase dog, entonces llamará al método OnHearName como lo define dog.
Especificadores de visibilidad
Puedes agregar 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 dicho 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 puedes usar 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 cualquier subclase.
private: Acceso limitado a la clase actual.
Especificadores de acceso
Puedes agregar especificadores de acceso a una clase para controlar quién puede construirla. Esto es útil, por ejemplo, si deseas asegurarte de que la instancia de una clase solo pueda construirse 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 constructorSi se llama al constructor de la clase cat fuera de su módulo pets, se producirán errores porque la palabra clave class está marcada como interna. Esto es cierto a pesar de que el identificador de clase en sí está marcado como público, lo que significa que se puede hacer referencia a gato mediante código fuera del módulo de mascotas.
A continuación se indican todos los especificadores de acceso que se pueden utilizar con la palabra clave clase:
public: Acceso sin restricciones. Este es el acceso predeterminado.
interno: acceso limitado al módulo actual.
Especificador concreto
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 concreta debe ser, a su vez, concreta.
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 concreta solo puede heredar directamente de una clase abstracta si ambas clases están definidas en el mismo módulo. Sin embargo, no se cumple transitivamente: una clase concrete puede heredar directamente de una segunda clase concrete en otro módulo, donde esa segunda clase concrete hereda directamente de una clase abstract en su módulo.
Especificador único
El especificador unique se puede aplicar 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 la igualdad de las instancias de clases únicas mediante la comparación de sus identidades. Las clases sin el especificador unique no tienen dicha identidad, por lo que su igualdad solo se puede comparar en función de los valores de sus campos.
Esto significa que las clases unique pueden compararse con los operadores = y <>, y 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 usar el especificador final en clases y campos de clases.
Cuando una clase tiene el especificador final, no puedes crear una subclase de la clase. En el siguiente ejemplo, no puedes utilizar la clase pet como una 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 propietario 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
Se pueden utilizar expresiones de bloque en un cuerpo de clase. Cuando creas una instancia de la clase, las expresiones de bloque se ejecutan en el orden en que están definidas. Las funciones llamadas en expresiones de bloque en el cuerpo de la clase no pueden tener el efecto NoRollback.
Como ejemplo, agreguemos dos expresiones block al cuerpo de la clase cat y agreguemos el 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 de bloque: el gato primero dirá “Rrrr”; entonces “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 un valor. Las clases solo pueden heredar de otra clase, pero pueden heredar de cualquier cantidad de interfaces.
Tipo persistente
Una clase es persistente cuando:
Se define con el especificador persistente.
Se define con el especificador final, porque las clases persistentes no pueden tener subclases.
No es único.
No tiene una superclase.
No paramétrico.
Solo contiene miembros que también son persistentes.
No tiene miembros de variable.
Cuando una clase es persistente, quiere decir que puedes usarlas en tus variables weak_map del módulo y hacer que sus valores persistan a lo largo de las sesiones de juego. Para obtener más información sobre la persistencia en Verse, consulta Cómo usar datos persistentes en Verse.
En el siguiente ejemplo de Verse, se muestra cómo puedes definir un perfil de jugador personalizado en una clase que puedes almacenar y actualizar, y a la que también puedes acceder, para un jugador. La clase player_profile_data guarda información de un jugador, como los PE obtenidos, su clasificación y las misiones que completó.
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{}