Programación en Vala/Programación orientada a objetos en Vala
Se podría definir brevemente la programación orientada a objetos como un paradigma de programación en el que los programas están definidos como objetos que interaccionan entre ellos para llevar a cabo una tarea. Para aquellas personas que no tengan una idea clara de en que consiste la programación orientada objetos se recomienda leer el enlace anterior.
El lenguaje de programación Vala no obliga a usar el paradigma de programación orientada a objetos (de hecho se puede decir que Vala es un lenguaje multiparadigma, es decir, soporta varios paradigmas de programación), sin embargo, algunas de sus características sólo están accesibles mediante la programación orientada a objetos.
Una definición de clase indica qué datos compone cada objeto dentro de un programa, que otros objetos referencia, y que métodos se pueden ejecutar en ese objeto. La definición puede incluir un nombre de otra clase de la cual esta clase heredaría. Una instancia de una clase es a su vez una instancia de todas las clases antecesoras (clases padre, abuelo, etc; de esa clase), y hereda todos sus datos y métodos, aunque no sea posible acceder a ellos por si misma. Una clase puede también implementar cualquier número de interfaces, las cuales son un conjunto de definiciones de métodos que deben ser implementadas en aquellas clases que las implementan (cuando se habla de que una clase implementa una interfaz nos estamos refiriendo a que esa clase ha sido definida usando la interfaz y eso obliga a que la clase tenga al menos los mismos métodos que la interfaz). Una instancia de la clase es también una instancia de cada interfaz que implemente esa clase o cualquier clase antecesora suya.
Las clases en Vala puede tener miembros estáticos (usando la palabra reservada static). Este modificador permite definir a los datos o métodos como pertenecientes a la clase como un conjunto, en lugar de específicos de una instancia suya. Este tipo de miembros de la clase se pueden acceder sin necesidad de definir una instancia suya.
Código básico
[editar]Una definición simple de una clase podría ser como el código siguiente:
Código de definición de una clase
public class Clase : GLib.Object {
/* Campos */
public int dato1 = 0;
private int dato2;
/* Constructor */
public Clase() {
this.dato2 = 5;
}
/* Metodos */
public int metodo_1() {
stdout.printf("Dato privado: %d", this.dato2);
return this.dato2;
}
}
Este código definirá un nuevo tipo (el cual es registrado de forma automática junto a los demás tipos de la biblioteca gobject) que contiene tres miembros. Hay dos miembros de tipo dato, los enteros definidos al principio de la clase, y un método llamada metodo_1, el cual devuelve un valor entero. La definición de la clase indica que la misma es una subclase de GLib.Object, y por lo tanto sus instancias lo son al mismo tiempo del tipo Object, y contienen todos los miembros que este tipo primitivo tiene. El hecho de que esta clase herede de Object también significa que hay unas funcionalidades especiales de Vala que pueden usarse para acceder fácilmente a algunas de las funcionalidades de Object.
Esta clase ha sido definida mediante la palabra reservada public. Esto implica que puede ser referenciada directamente por código que se encuentre definido en otro archivo distinto donde está definida esta clase.
Los miembros de la clase pueden ser descritos también como públicos o privados (public o private). El miembro dato1 es público, por lo que es visible para cualquier usuario de la clase y puede ser modificado sin intermediarios, es decir, sin control alguno. El segundo dato es privado, y por lo tanto sólo es accesible por código que pertenezca a la clase. El lenguaje Vala dispone de cuatro modificadores de acceso que aparecen descritos en la siguiente tabla:
Modificador | Descripción |
public | Sin ningún tipo de restricción. Si una clase es definida mediante este modificador podrá ser definida por código definido en un archivo distinto al de la definición de la clase. Los miembros definidos así en una clase puede ser accedidos sin ningún tipo de restricción al respecto. |
private | El acceso está limitado al código interno de la clase o estructura donde ha sido definido. Este es el acceso por defecto si no se especifica otro. |
protected | El acceso está limitado al código interno de la clase o a cualquier clase que herede de esta clase. |
internal | El acceso está restringido a clases que hayan sido definidas dentro del mismo paquete que la clase (o miembro de la clase) así definido. |
El método identificado como constructor (el método debe ser público y llamarse igual que la clase) inicializa nuevas instancias de la clase. Este método puede tomar cero o más argumentos y no devuelve ningún valor.
La parte final de la definición de la clase de arriba es la definición de los métodos de la misma. El método se llama metodo_1, y devuelve un entero. Puesto que el método definido no es estático (static), sólo puede ser llamado sobre una instancia de la clase, y puede por lo tanto acceder a miembros del objeto. Esto se puede realizar mediante la referencia al propio objeto this, la cual siempre apunta a la instancia en la cual el método se está ejecutando. A menos que sea motivo de ambigüedad el identificador this puede ser omitido si se desea.
Para usar instancias de la clase que hemos definido anteriormente podemos usar código como el siguiente:
Clase c = new Clase(); // Inicialización del objeto
c.dato1 = 5; // Acceso al miembro dato público
c.metodo_1(); // Ejecución del método público
Constructores
[editar]El lenguaje Vala permite dos esquemas de construcción: el esquema de construcción tipo Java/C# y el esquema de construcción tipo GObject (el cual será descrito al final del presente capítulo). A continuación nos centraremos sobre el primero de los tipos.
Vala no permite la sobrecarga de constructores (no permite crear más de un constructor por clase), por las mismas razones que no permite sobrecarga de operadores, lo cual significa que una clase no puede tener múltiples constructores con el mismo nombre. Sin embargo, esto no mayor problema, ya que Vala permite la definición de los constructores con nombre, es decir, constructores que no se llaman igual que la clase. Si se quieren ofrecer diversos constructores al desarrollador se puede obrar como sigue:
class Clase() : GLib.Object {
public Clase() {
}
public Clase.constructor_con_cadena(string cadena) {
}
public Clase.constructor_con_entero(int entero) {
}
}
Para crear un objeto con estos constructores es análogo a lo anterior:
Clase c1 = new Clase();
Clase c2 = new Clase.constructor_con_cadena("Hola");
Clase c3 = new Clase.constructor_con_entero(7);
Se puede también usar constructores dentro de otros constructores para ahorrar esfuerzo, si parte de la inicialización ya la tenemos definida. Así un ejemplo sería:
public class Point : Object {
public double x;
public double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public Point.rectangular(double x, double y) {
this(x, y);
}
public Point.polar(double radius, double angle) {
this.rectangular(radius * Math.cos(angle), radius * Math.sin(angle));
}
}
void main() {
var p1 = new Point.rectangular(5.7, 1.2);
var p2 = new Point.polar(5.7, 1.2);
}
Destructores
[editar]Aunque Vala dispone de un sistema de gestión de la memoria puede que necesite crear destructores si se decide gestionar la memoria de forma manual, usando punteros (que se verán más adelante) o si se tienen que liberar otro tipo de recursos al dejar de usar un tipo de objetos. La forma de definir un destructor es similar a cómo se hace en C++:
class Clase : Object {
~Clase() {
stdout.printf("Destruyendo el objeto\n");
}
}
Puesto que la gestión de memoria en Vala se hace mediante conteo de referencias y no mediante un recolector de basuras, los destructores son deterministas y puede ser usados para implementar el patrón de diseño RAII para la gestión de recursos (cerrar flujos de datos, conexiones a bases de datos, etc).
Señales
[editar]Una señal en Vala es equivalente a los eventos en otros lenguajes de programación (como C#) o a los listeners en otros lenguajes (como Java). Una definición corta de una señal podría ser una forma de ejecutar un número arbitrario de método idénticos (por ejemplo métodos con los mismos parámetros) aproximadamente mismo tiempo.
Una señal se define como un miembro interno de una clase, y parece un método sin cuerpo. Los manejadores de señales se añaden mediante el método connect(). Para obtener una idea más clara de cómo se definen y para que sirven las señales se añade el siguiente ejemplo:
class Foo : Object {
public signal void some_event (); // Definición de la señal
public void method () {
some_event (); // Se emite la señal
}
}
void callback_a () {
stdout.printf ("Método A\n");
}
void callback_b () {
stdout.printf ("Método B\n");
}
void main () {
var foo = new Foo ();
foo.some_event.connect (callback_a); // Se conectan los métodos a la señal
foo.some_event.connect (callback_b);
foo.method ();
}
En el código anterior define una clase llamada Foo que tiene una señal que se lanza en un método llamado method. Cuando se emita la señal se ejecutarán todos los métodos que hayan sido conectados a la misma. En el método principal main se crea una instancia de Foo y luego se conectan los dos métodos callback_a y callback_b a la señal. Por último se ejecuta el método method que emite la señal, ejecutándose a continuación tanto callback_a como callback_b.
Las señales sólo pueden ser definidas como públicas ya que todas las señales deben poder estar conectadas a y emitirse desde cualquier lugar del código.
Propiedades
[editar]Una buena práctica cuando se desarrolla en un lenguaje orientado a objetos es esconder los detalles de la implementación de los usuarios de la clase (principio de encapsulamiento), de manera que se pueda modificar el funcionamiento interno sin necesidad de romper la API pública. Una práctica es definir los miembros datos de una clase como privados y suministrar métodos de acceso a sus valores (estos métodos se conocen como geters y seters). Un ejemplo de esto sería el siguiente ejemplo:
class Person : Object {
private string name;
private int age;
Person(string name, int age) {
this.set_name(name);
this.set_age(age);
}
public string get_name() { return this.name; }
public void set_name(string name) { this.name = name; }
public int get_age() { return this.age; }
public void set_age(int age) { this.age = age; }
}
Esto es totalmente válido y funciona correctamente, sin embargo, en Vala podemos hacer algo más elegante. El problema principal de estos métodos es que son muy engorrosos para trabajar con ellos. Supongamos que queremos incrementar la edad de la persona en una unidad:
Person p = new Person("Pedro", 25);
// Pedro ha cumplido años hoy por lo que queremos aumentar su edad en 1
p.set_edad( p.get_edad() + 1 );
Y es en este preciso momento cuando las propiedades hacen acto de presencia para ayudar en esta tarea. Las propiedades son un mecanismo por el cual se puede acceder y modificar los valores sin necesidad de acceder a métodos como en el caso anterior. Así una propiedad tiene la siguiente estructura:
class Person : Object {
private string _name;
private int _age;
Person(string name, int age) {
// Accedemos a los valores mediante las propiedades
this.name = name;
this.age = age;
}
public string name {
get { return this._name; }
set { this._name = name; }
}
public string age {
get { return this._age; }
set { this._age = age; }
}
}
Una propiedad tiene un bloque set (acceso de escritura de los datos) y bloque get (acceso de lectura de los datos). El identificador value representa el nuevo valor que se debe asignar en el bloque set de la propiedad.
Ahora se puede acceder a la propiedad como si de un dato público se tratara. Pero detrás de las cámaras el código de los bloques get y set se ejecutará dependiendo del tipo de acceso que se esté realizando.
Person p = new Person("Peter", 25);
// Pedro ha cumplido años hoy por lo que queremos aumentar su edad en 1
p.age++;
Si sólo se pretende hacer la implementación estándar entonces se puede realizar una definición más corta que la anterior:
class Person : Object {
private string _name;
private int _age;
Person(string name, int age) {
// Accedemos a los valores mediante las propiedades
this.name = name;
this.age = age;
}
public string name { get; set; default="Peter" }
public string age { get; set; default=18 }
}
}
Si se quiere se puede hacer que la propiedad sea de sólo lectura mediante el modificador del bloque set private o dejando en blanco ese bloque:
public string name { get; private set; default="Peter" }
// Otra definicion posible
public string name { get; default="Peter" }
Además del nombre una propiedad puede tener una descripción breve llamada nick y/o una descripción larga llamada blurb. La forma de establecer estos atributos de las propiedades es mediante un atributo del código Property:
[Property(nick = "Person's name", blurb = "This is a string representing the person's name")]
public string name { get; set; default="Peter" }
Este tipo de información puede ser accedida en tiempo de ejecución, lo cual permite que programas como Glade hagan uso de este tipo de información, presentando por ejemplo descripciones de las propiedades de los widgets de GTK+.
Toda clase derivada de GLib.Object tiene por defecto una propiedad llamada notify. Esta señal se emite cuando una propiedad de algún objeto se modifica. Así que se puede conectar un método a esta señal si se está interesado en los cambios en general:
// Esto se conoce como una expresión lambda
obj.notify.connect((s, p) => {
stdout.printf("Property '%s' has changed!\n", p.name);
});
Para definir la función se ha usado una expresión lambda, en la que s es el origen de la señal, p es la información de la propiedad para la propiedad que ha sido modificada. Si sólo se está interesado en las notificaciones de una propiedad simple se puede usar la siguiente sintaxis:
alice.notify["age"].connect((s, p) => {
stdout.printf("age has changed\n");
});
Nótese que este caso se debe usar la representación del nombre de la propiedad dónde los guiones bajos se reemplazan por guiones: nombre_propiedad pasa a ser nombre-propiedad en esta representación, la cual es el convenio de nombres de propiedades usado por GObject.
Las notificaciones se pueden deshabilitar mediante el atributo de código CCode justo antes de la declaración de la propiedad:
public class MyObject : Object {
[CCode(notify = false)]
// En esta propiedad no se emite la señal
public int without_notification { get; set; }
// Aquí si se emite la señal.
public int with_notification { get; set; }
}
Existe otro tipo de propiedades que se llaman propiedades de constructor que se describen más adelante en la sección Esquema de construcción tipo GObject.
Herencia
[editar]En Vala una clase puede derivarse (y heredar) de cero o una clase. En la práctica la mayoría de las veces heredará de una clase, puesto que no hay herencía implícita como en otros lenguajes de programación.
Cuando se define una clase nueva que hereda de otra, se crea una relación entre las clases dónde las instancias de la subclase definida son a su vez intancias de la clase padre. Esto significa que la operación sobre instancias de las clase padre también se pueden aplicar sobre las instancias de la clase hija. De hecho, dónde se debe usar una instancia de la clase padre, se podrá sustituir por una instacia de la clase hija.
Cuande se escribe la definición de una clase es posible definir de forma exacta quien va a poder acceder a los métodos y a los datos del objetos. El siguiente ejemplo demostrará la variedad de estas opciones:
class SuperClass : GLib.Object {
private int data;
public SuperClass(int data) {
this.data = data;
}
protected void protected_method() {
}
public static void public_static_method() {
}
}
class SubClass : SuperClass {
public SubClass() {
base(10);
}
}
El miembro dato data de la clase SuperClass es una instancia de datos de esa clase. Habrá un miembro de ese tipo en cada una de las instancias de la clase padre SuperClass, y puesto que ha sido declarada como privada (private) sólo puede ser accedida por el código que forma parte de la definición de la clase padre.
El método protected_method es un método de SuperClass. Este método al haber sido definido como protegido (protected) podrá ser ejecutado por la clase SuperClass y por todas las clases descendientes de ésta.
El método public_static_method tiene dos modificadores. El modificador static significa que puede ser ejecutado sin necesidad de crear una instancia de la clase o de alguna clase hija de ésta. Como resultado de esto, este método no tendrá acceso a la referencia cuando se ejecute. El modificado public significa que el método puede ser llamado desde cualquier parte del código, sin importar la relación que tenga con la clase padre o sus derivadas.
Dadas estas definicioes, cualquier instancia de la clase SubClass tendrá los tres miembros de la clase padre SuperClass, pero sólo podrá acceder a los miembros no privados (private). El código externo a la clase hija sólo podrá acceder a los miembros que hayan sido definidos como públicos (public).
Con el identificador base se puede enlazar con el contructor de la clase base, en caso de que sea necesario inicializar algunos datos a los que sólo tenemos acceso mediante el constructor de la clase padre.
Clases abstractas
[editar]Hay otro modificador para los métodos, denomidado abstract. Este modificador permite definir un método que no tiene implementación en la clase dónde se define. En lugar de implementarse en la clase dónde es definido, se obliga a que sean las clases hijas de esa las que definan ese método antes de que puede ser llamado. Esto permite definir operaciones que se pueden ejecutar en todas las instancias de un tipo, mientras que nos aseguramos de que todos los tipos definen sus propias implementaciones de esa funcionalidad. Una clase que contiene métodos abstractos debe ser definida como una clase abstracta también. El resultado de esto es que no se permitirá crear instancias de esa clase. Ejemplo:
public abstract class Animal : Object {
public void eat() {
stdout.printf("*chomp chomp*\n");
}
public abstract void say_hello();
}
public class Tiger : Animal {
public override void say_hello() {
stdout.printf("*roar*\n");
}
}
public class Duck : Animal {
public override void say_hello() {
stdout.printf("*quack*\n");
}
}
La implementación de un método abstracto debe ser marcada con el modificador override. Como se ve en el ejemplo se define una clase abstracta denomida Animal. Esta clase define dos métodos, uno de ellos normal que denomina eat y define como come un animal (se supone que todos comen igual); el otro método say_hello define como un animal dice hola, y se define como abstracto para que en cada clase que defina un animal tenga una implementación distinta. Así tenemos una clase Tiger que representa a un tigre y dice hola escribiendo "*roar*", y por otra parte tenemos la clase Duck que hereda también de Animal y que representa a un pato y que implementa say_hello escribiendo "*quack*".
Como se puede adivinar este mecanismo es muy útil para definir una interfaz común para una familia de clases que heredan de la clase abstracta y que no puede ser instanciada.
Interfaces/Mixins
[editar]Una clase Vala puede implementar un número arbitrario de interfaces. Cada una de ellas es un tipo, parecido a una clase, aunque no puede ser instanciada. Cuando una clase implementa una interfaz, una instancia de una clase así declarada también es una instancia de la interfaz, y por lo tanto puede ser usada en sustitución dónde se espere una instancia de la interfaz.
El procedimiento para implementar una interfaz es el mismo que para heredar de una clase abstracta. Una interfaz simple puede ser como sigue:
public interface ITest : GLib.Object {
public abstract int data_1 { get; set; }
public abstract void method_1();
}
Este código define una interfaz llamada ITest el cual requiere la clase GLib.Object como padre y contiene dos miembros. El miembro data_1 es una propiedad que ha sido declarado como abstracto. Esto no quiere decir que la interfaz albergue una propiedad realmente, sino que obliga a cualquier clase que implemente esta interfaz a que contenga un miembro propiedad del mismo tipo y con el mismo nombre y que contenga asimismo los bloques get y set. Es necesario que se defina como abstracto puesto que una interfaz no puede tener ningún miembro dato definido. El segundo miembro de la interfaz se denomina method_1 y es un método. Este método a estar definido como abstracto debe ser definido por la clase que implemente esta interfaz. La forma más simple para una implementación completa de una interfaz es:
public class Test1 : GLib.Object, ITest {
public int data_1 { get; set; }
public void method_1() {
}
}
Y puede ser usado de la siguiente forma:
var t = new Test1();
t.method_1();
ITest i = t;
i.method_1();
En el código se puede apreciar que se define un objeto de tipo Test1 que implementa la interfaz ITes. Se define así una referencia de tipo ITets y la asignamos para que apunte al nuevo objeto de tipo Test1. Como hemos dicho se podrán acceder a los métodos que define la interfaz y que obliga a que sean implementados en la clase. Hay que recalcar que no se crea una instancia de la interfaz (el código var i = new ITest(); NO ES CORRECTO), sino que se crea una referencia de la interfaz que no se inicializa y se usa para asignarla al nuevo objeto creado.
Las interfaces en Vala no puede heredar de otras interfaces, pero puede declarar como prerrequisito a otras interfaces, lo cual en la práctica funciona de la misma forma. Por ejemplo, sería deseable que cualquier clase que implementara la interfaz List también debiera implementar la interfaz Collection. La sintaxis para realizar esto es la misma que la que permite definir las interfaces que implementa una clase:
public interface List : Collection { // Esto obligará a que la clase que implemente la interfaz "List" deba implementar la interfaz "Collection" también
}
Esta definición de List no puede ser implementada sin que se implemente la interfaz Collection también, y Vala obliga a que se especifique todas las interfaces que deben ser implementadas para aquellas clases que quieran implementar la interfaz List. El siguiente código muestra un ejemplo:
public class ListClass : GLib.Object, Collection, List {
}
Las interfaces de Vala pueden tener una clase como prerrequisito también, si el nombre de una clase se define en los prerrequisitos de una interfaz entonces, la interfaz sólo puede ser implementada en clases derivadas de la clase que se especificó. Esto es útil para asegurar que una instancia de una interfaz es también una clase derivada de la clase GLib.Object, y por lo tanto la interfaz puede ser usada, por ejemplo, como el tipo de una propiedad.
El hecho de que una interfaz no pueda ser heredada de otra interfaz es sólo una distinción técnica, ya que, en la práctica el sistema de Vala funciona igual al de otros lenguajes en este área, pero con la funcionalidad extra de los prerrequisitos de las clases.
Hay otra diferencia importante entre las interfaces en Vala y las interfaces Java/C#: las interfaces en Vala puede tener métodos que no sean abstractos. De hecho Vala permite la implementación de métodos en las interfaces. Debido a esto, las interfaces de Vala pueden actuar como Mixins. Esto es una forma de restricción de la herencia múltiple.
Polimorfismo
[editar]El polimorfismo describe la forma en la cual el mismo objeto puede ser usado como más de un tipo de cosa. Varias de las técnicas ya descritas aquí sugieren cómo es posible esto en Vala: una instancia de una clase puede ser usada como una instancia de una clase padre, o de cualquier implementación de las interfaces, sin cualquier conocimiento de su tipo actual. A continuación se muestra un ejemplo:
class SuperClass : GLib.Object {
public void method_1() {
stdout.printf("SuperClass.method_1()\n");
}
}
class SubClass : SuperClass {
public void method_1() {
stdout.printf("SubClass.method_1()\n");
}
}
Estas dos clases implementan un método llamado method_1, y la clase SubClass contiene por tanto dos métodos llamados method_1, uno heredado de SuperClass y otro propio. Cada uno de ellos se ejecuta como muestra el siguiente código:
SubClass o1 = new SubClass();
o1.method_1();
SuperClass o2 = o1;
o2.method_1();
Este código llamará a los dos métodos. La segunda línea contiene la inicialización de la clase SubClass y llamará a la versión de esa clase del método. La cuarta línea contiene la inicialización de una instancia o2 de la clase SuperClass y ejecutará la versión del método de esta clase.
El problema que muestra el código, es que cualquiera que sea el código si contiene una instancia de la clase SuperClass, ésta ejecutará el método que se define en esa clase, independientemente de a que objeto apunte esa referencia. Para cambiar ese comportamiento es usar los métodos virtuales. El siguiente código muestra una versión modificada del ejemplo anterior usando los métodos virtuales:
class SuperClass : GLib.Object {
public virtual void method_1() {
stdout.printf("SuperClass.method_1()\n");
}
}
class SubClass : SuperClass {
public override void method_1() {
stdout.printf("SubClass.method_1()\n");
}
}
Cuando este código se use de la misma forma que el anterior, el método method_1 de la clase SubClass será el ejecutado, en lugar del método de la clase padre. Esto es debido a que le hemos indicado al sistema que el método method_1 es un método virtual, lo cual significa que si es redefinido en una clase hija, será esa nueva versión la se ejecutará siempre en instancias de esa clase hija, independientemente del tipo y conocimientos de la instancia que lo ejecute.
Esta distinción puede que sea familiar a los programadores de algunos lenguajes, como C++, pero es totalmente diferente al estilo del lenguaje Java, en dónde se previene del uso de los métodos virtuales.
El lector puede que se haya dado cuenta de que cuando un método se declara como abstracto (abstract) debe ser virtual también. De lo contrario, no sería posible ejecutar ese método dada una instancia del tipo aparente en el cual fue definido. Cuando se implementa un método abstracto en una subclase, se puede elegir declararlo como sobreescribible (overriden), estableciendo la naturaleza virtual de ese método, y permitiendo a los subtipos que usen la misma implementación si lo desean.
Es posible implementar los métodos de una interfaz de tal forma que las subclases puedan cambiar la implementación. El proceso en este caso es declarar la implementación inicial como virtual, y de ese modo las subclases pueden sobrescribir si así lo necesitan.
Cuando se escribe una clase, es muy común querer usar la funcionalidad definida en la clase de la cual hereda. Esto es complicado en los lugares dónde se usa el nombre del método más de una vez en el árbol de herencia de las clases. Para este cometido Vala tiene la palabra reservada base. El caso más común para usar este mecanismo es cuando se ha sobrescrito la funcionalidad de un método virtual para suministrar funcionalidad extra, pero aun se necesita la funcionalidad del método de la clase padre. El siguiente ejemplo muestra como actuar en este caso:
public override void method_name() {
base.method_name();
extra_task();
}
El código muestra como se ejecuta el método de la clase padre mediante la palabra reservada base que hace referencia a la clase padre. Después de ejecutar dicho método sólo tendríamos que ejecutar la funcionalidad extra que necesitemos.
Información de tipos en tiempo de ejecución
[editar]Puesto las clases en Vala se registran en el sistema en tiempo de ejecución, cada instancia contiene información de su tipo que se puede consultar de forma dinámica mediante el operador is:
bool b = object is AlgunTipo;
Se puede obtener el información acerca del tipo de una instancia usando el método definido get_type(). Este método está definido en GLib.Object por lo que la clase del objeto desde la cual lo llamamos debe heredar de esta clase base.
Type type = object.get_type(); // Se obtiene el tipo y se almacena en una instancia del tipo "Type"
stdout.printf("%s\n", type.name()); // Se muestra el nombre del tipo
Con el operador typeof() se puede acceder a la información del tipo de forma directa. A partir de la información del tipo obtenida se puede crear nuevas instancias con Object.new(). Ejemplo:
Type type = typeof(Foo); // Se obtiene la información del tipo de la instancia "Foo"
Foo foo = (Foo) Object.new(type); // Se crea un nuevo objeto del tipo "type" y se asigna a la referencia foo
En este caso el constructor que se utiliza para inicializar los objetos de esta forma será el descrito en la sección de los constructores estilo GObject.
Conversión de tipos dinámica
[editar]Para la conversión dinámica entre tipos una variable se convierte mediante una expresión posfija as y el tipo al que queremos convertir la referencia. Vala dispone de comprobación de tipos dinámica para asegurarse de que la conversión es factible. En caso de que la conversión sea imposible de realizar se devuelve null. Sin embargo, esto requiere que se especifique tanto el tipo de la clase origen como el destino. Ejemplo:
Button b = widget as Button;
Si por alguna razón la clase de la instancia widget no es de tipo Button o una de las clase hijas de ésta o no implementa la interfaz Button, b será null. Esta conversión es equivalente a la siguiente expresión:
Button b = (widget is Button) ? (Button) widget : null;
Tipos genéricos
[editar]Vala incluye un sistema de tipos genérico, mediante el cual una instancia particular de una clase se puede restringir con un tipo particular o un conjunto de tipos elegidos en la construcción de la clase. Esta restricción se usa normalmente para asegurarse que los datos almacenados en un objeto deben ser de un tipo particular, por ejemplo para implementar una lista de objetos de un determinado tipo. En este ejemplo, Vala se asegura que sólo los objetos del tipo indicado puedan ser añadidos a la lista, y todos los objetos que sean devueltos serán también de ese tipo (o convertidos a ese tipo).
En Vala, los tipos genéricos (generics) se gestionan cuando el programa se encuentra en ejecución. Cuando se define una clase que puede ser restringida a un tipo de dato, existirá sólo una clase, cuyas instancias serán personalizadas individualmente. Esto es diferente a C++, dónde se crea una nueva clase para cada tipo requerido. Vala es similar al sistema usado por Java en este sentido. Esto tiene varias consecuencias, la más importante de ellas es que los miembros estáticos (static) de un tipo se comparten como un todo, sin importar las restricciones que se encuentren en cada instancia; y eso dado una clase o una subclase, un tipo genérico refinado por una subclase puede ser usado como un tipo genérico refinado por la clase. Ejemplo:
public class Wrapper<G> : GLib.Object {
private G data;
public void set_data(G data) {
this.data = data;
}
public G get_data() {
return this.data;
}
}
La clase Wrapper debe ser restringida con un tipo para inicializarla, en este caso el tipo se identifica como G, y de esta manera las instancias de esta clase almacenarán un objeto del tipo G, y tendrán métodos para obtener o modificar ese objeto. La razón de este ejemplo específico es explicar que actualmente una clase de tipo genérico no puede usar propiedades es su restricción del tipo, por lo que debe usar métodos para obtener y modificar este dato.
Para inicializar esta clase, se debe elegir un tipo de dato, por ejemplo el tipo cadena (en Vala no existe restricción en el tipo de dtos que puede usarse en una clase genérica). Para crear una instancia de la clase:
var wrapper = new Wrapper<string>();
wrapper.set_data("test");
var data = wrapper.get_data();
Como puede verse, cuando se obtiene los datos de la clase Wrapper, se asignan a un identificador con un tipo no explícito (que se define mediante la palabra reservada var). Esto es posible por que Vala sabe que tipo de objetos se encuentran en cada instancia de Wrapper, y por lo tanto puede realizar este trabajo por usted. El hecho de que Vala no cree varias clases para una definición genérica significa que se puede realizar un código como el que sigue:
class TestClass : GLib.Object {
}
void accept_object_wrapper(Wrapper<Glib.Object> w) {
}
...
var test_wrapper = new Wrapper<TestClass>();
accept_object_wrapper(test_wrapper);
...
Puesto que todas las instancias de TestClass son también objetos GLib.Object, el método accept_object_wrapper aceptará el objeto que le sea pasado, y lo tratará como si fuera una instancia de un objeto GLib.Object.
Esquema de construcción tipo GObject
[editar]Como se comentó anteriormente en este mismo capítulo, Vala ofrece un estilo de construcción alternativo que es diferente al anteriormente descrito, pero muy cercano a como se realiza la construcción de objetos con la biblioteca GObject. El método que el programador prefiere depende de si se procede del estilo de programación de GObject o de Java/C#. El estilo de construcción de objetos GObject introduce un nuevo elemento de sintaxis: las propiedades del constructor, una llamada especial a Object(...) y un bloque de constructor. Vamos a ver como funciona este tipo de constructor:
public class Person : Object {
/* Propiedades del constructor */
public string name { get; construct; }
public int age { get; construct set; }
public Person(string name) {
Object(name: name);
}
public Person.with_age(string name, int years) {
Object(name: name, age: years);
}
construct {
// Todo lo que tenga que hacer adicionalmente el constructor
stdout.printf("Welcome %s\n", this.name);
}
}
Con este esquema de construcción cada método constructor contiene una llamada a GObject(...) para establecer las propiedades del constructor. La llamada a GObject(...) toma un número variable de parámetros con nombre en la forma propiedad : valor. Estas propiedades deben ser declaradas como una propiedad del constructor. Se establecerán dichas propiedades a los valores dados después de que todos los bloques de constructores desde el original GLib.Object hacia abajo hasta el de la clase actual se hayan ejecutado.
Se garantiza la ejecución del bloque del constructor cada vez que una clase es creada, incluso si es creada como un subtipo de la misma. No tiene ni un parámetro ni un valor de retorno. Dentro de este bloque se puede realizar llamadas a otros métodos y establecer el valor de miembros dato si es necesario.
Las propiedades de construcción se definen como propiedades con bloques get y set para obtener establecer sus valores, y por lo tanto con código arbitrario en la asignación. Si se necesita realizar la inicialización en una propiedad de un constructor, es posible escribir un bloque para esa propiedad, la cual se ejecutará inmediatamente después de la asignación, y antes de cualquier código de construcción.
Si una propiedad de constructor se declara sin bloque set se denomina como una propiedad de construcción exclusiva, lo cual significa que sólo puede ser asignada en la fase de construcción, pero no después de esa fase. En el ejemplo de arriba la propiedad name es una propiedad de este tipo. A continuación un resumen de los distintos tipos de propiedades junto con la nomenclatura que se encuentra en la librerías de tipo GObject:
public int a { get; private set; } // Lectura
public int b { private get; set; } // Escritura
public int c { get; set; } // Lectura / Escritura
public int d { get; set construct; } // Lectura / Escritura / Construcción
public int e { get; construct; } // Lectura / Escritura / Sólo Construcción