El lenguaje de programación Gnome Vala

<<TableOfContents: la ejecución fallada [cannot import name AS_IS] (ve también el log)>>

Introducción

Este documento es un resumen de las características del lenguaje de programación Vala. Diseñado para el sistema GObject de GNOME.

Ficheros fuente

No hay restricciones en la escritura del código fuente. Esto significa que se pueden crear tantas clases como se quieran dentro del mismo fichero.

En algunos lenguajes de programación hay que cumplir unas reglas: en java, por ejemplo, el nombre del fichero con el código fuente se tiene que corresponder con el nombre de la clase que se está definiendo; y los nombres de los directorios se tienen que corresponder con los nombres de los paquetes que se están implementando.

El enfoque quizás sea parecido al que se sigue en el lenguaje de programación C donde todas las funciones de una misma librería se agrupan en un fichero objeto.

Por este motivo, para crear un paquete, es necesario compilar a un fichero binario pasando al compilador todos los ficheros fuente necesarios. Posteriormente es posible incluir ese paquete, al compilar otro fichero, indicándoselo al compilador mediante el parámetro --pkg.

Operadores

Operadores (separados por comas)

Descripción

=

Operador de asignación: asigna un valor a una variable.

+, -, /, *, %

Operadores aritméticos: suma, resta, división, multiplicación y módulo.

+=, -=, /=, *=, %=

Operadores aritméticos en los que a la izquierda debe haber un operando al que asignar el resultado.

++, --

Incremento y decremento del valor de una variable respectivamente.

|, ^, &, ~

Operadores de comparación a nivel de bit: or, or exclusivo, and y not.

|=, &=, ^=

Modificadores de bits en los que a la izquierda debe haber un operando al que asignar el resultado: or, or exclusivo, and y not.

<<, >>

Modificadores de bits: el resultado de mover hacia la izquierda y hacia la derecha, respectivamente, los bits de una variable. Tantas posiciones como indique el operando de la derecha.

<, >, ==

Operadores lógicos de comparación: menor, mayor e igualdad, respectivamente.

!, &&,

Operadores lógicos: not, and y or, respectivamente.

(expresión)?(valor en verdadero):(valor en falso)

Operador condicional ternario. Evalúa una condición y devuelve el resultado según si la expresión es verdadera o falsa.

??

Operador de prevención de nulo. Establece un valor predeterminado en caso de que una referencia sea nula. Por ejemplo: x = a??b Es equivalente a x = (a != null) ? a : b.

as

Operador de casting de tipo dinámico. Comprueba, en tiempo de ejecución, que el tipo sea correcto. En caso contrario se asigna un valor nulo. Por ejemplo:Button b = widget as Button es equivalente a Button b = (widget is Button) ? (Button) widget : null;.

=>

Expresión lambda para funciones anónimas.

while, do while, for, foreach, switch

Operadores de control.

Tipos de datos

Se diferencian tres tipos de datos:

A continuación los tipos de datos soportados en el lenguaje.

Tipos por valor

Los siguientes tipos son equivalentes a los soportados por compiladores de lenguaje C. Estos tipos pueden tener tamaños diferentes según la arquitectura del computador en el que se vaya a compilar:

En cualquier caso, es posible determinar el tamaño mínimo y máximo de un tipo númerico utilizando los atributos .MIN y .MAX respectivamente: short.MIN.

A continuación los tipos específicos de Vala (y de cualquier lenguaje de alto nivel moderno como Java o C#):

Cadenas de caracteres (Strings)

Señalar que, además del tipo de dato string mencionado anteriormente, también existen tipos de strings más complejos:

Además, es posible partir una cadena de caracteres utilizando la expresión [inicio:fin]. Donde inicio es la posición (incluyente) del primer carácter (empezando en 0) y fin es la posición (excluyente) del último carácter:

string saludo = "hola mundo";
string s1 = saludo[5:11]; // "mundo"

Se puede acceder a un carácter en concreto, teniendo en cuenta que se está utilizando, de forma predeterminada, la codificación UTF8:

unichar c = saludo[7];

Sin embargo, las cadenas de caracteres en Vala son inmutables. Esto significa que no se puede modificar directamente un carácter individual de una cadena. No son simples Arrays. La cadena tiene que ser procesada internamente ya que en UTF8 cada carácter puede ocupar diferentes cantidades de bytes.

Finalmente, decir que la mayoría de los tipos básicos disponen de métodos de conversión a, y desde, string:

bool b = "false".to_bool();
string s1 = true.to_string();
int i = "-52".to_int();
string s2 = 21.to_string();

Arrays

Se pueden definir Arrays de cualquier tipo. Ejemplo de Array unidimensional:

in[] a = new int[10];
int[] b = {2,5,6,1};

Ejemplo de Array multidimensional:

int[,] c = new int[3,4];
int[,] d = {{1,1},{2,2}};

Sólo si el array es local o privado, es posible añadir elementos dinámicamente. Y su tamaño se va incrementando en potencias de 2. Esto se hace con el operador +=:

int[] e;
e += 12;
e += 5;

El atributo length muestra el número de elementos del array. Que no tiene porque corresponderse con su tamaño interno.

Es posible establecer el tamaño fijo del array en su declaración. Y en este caso no hace falta instanciar el tipo con el operador new():

int f[10];

Por lo visto, todavía no están soportados los arrays multidimensionales irregulares. Donde cada dimensión es de un tamaño diferente:

int[5][3];

Tipos dinámicos

También se pueden utilizar tipos dinámicos declarando las variables con var:

var a = 32;
var b = "hola";

Esto es muy útil para reducir la redundancia en el código. En java, por ejemplo, con la introducción de los tipos genéricos, tendríamos el siguiente código para la instanciación de una clase:

MiAlgo<string, MiOtro<string, int>> algo = new MiAlgo<string, MiOtro<string, int>>();

La misma instanciación podría haber quedado resuelta, utilizando tipos dinámicos, de la siguiente manera (tal como se hace en Groovy):

var algo = new MiAlgo<string, MiOtro<string, int>>();

Información de tipos en tiempo de ejecución

Es posible comprobar el tipo de una instancia, en tiempo de ejecución, a través de la palabra reservada is. Por ejemplo:

bool b = instancia is Clase;

También es posible obtener información del tipo, en aquellas clases que hereden de GLib.Object, mediante el método get_type():

Type tipo = instancia.get_type();
stdout.println ("%s\n", tipo.name());

Cosa que también se puede hacer, directamente, con el operador typeof:

Type tipo = typeof(instancia);

Y crear una instancia pasando como parámetro la información del tipo:

Clase instancia = (Clase) Object.new(tipo);

Funciones

Los nombres de las funciones en Vala (también llamados métodos) siguen la convención de utilizar letras minúsculas y un guión bajo para separar palabras. Se utiliza el guión bajo en vez de la notación en camello porque es más coherente con los nombres de las funciones de las librerías de Vala y GObject.

No existe la sobreescritura de métodos. Esto significa que no pueden existir dos métodos con el mismo nombre aunque éstos tengan distintos parámetros. El motivo es que las funciones escritas en Vala tambén deberían ser usables por programadores de C.

Es posible establecer un valor predeterminado para el argumento de un método. Así se puede simular el uso, común en otros lenguajes como Java, de métodos con el mismo nombre y número creciente de argumentos.

Por ejemplo, en Vala:

void funcion(in x, string s="hola", double z = 0.5){...};

Serviría para hacer lo que en Java sería:

void funcion(int x, string s, double z){...};
void funcion(int x, string s) { funcion(x, s, 0.5}; }
void funcion(int x) { funcion(x, "hola"); }

Vala hace una comprobación básica de valores nulos en los argumentos y devoluciones de los métodos. Por este motivo hay que indicar qué argumentos pueden tener valor nulo. En el siguiente ejemplo, tanto el valor de retorno como los argumentos texto y objeto pueden tener un valor nulo:

string? nombre_funcion (string? texto, Clase? objeto, int entero)

Delegados

Existen tipos delegados (punteros a funciones). Así se pude pasar una función como parámetro a otra función. Por ejemplo:

//definir un tipo delegado
delegate void tipo_delegado(int a);

void funcion_1 (int a){...}
void funcion_2 (tipo_delegado d, int a){ d(a); }

[...]

funcion2(funcion1, 5);

Funciones anónimas

También existen funciones anónimas (closures). Las funciones anónimas son aquellas que no tienen nombre. Por lo tanto no se definen sino que se escriben directamente en el lugar en el que van a ser utilizadas.

En el siguiente ejemplo se asigna una función anónima a un tipo delegado utilizando una expresión lambda:

delegate void tipo_delegado(string saludo);
tipo_delegado d1 = (saludo) => { stdout.printf(saludo) }

O directamente como parámetro de un método:

//definición de funciones (una con parámetro delegado)
void funcion_1(tipo_delegado d, int a){ d(a); }
void funcion_2 (int a) { stdout.printf("%d\n", a);}

[...]

//pasando la función como parámetro delegado en otra función
f1(f2, 5); 

//utilizando una función anónima en el parámetro delegado.
f1( (a)=>{stdout.printf("hola %i\n", a}, 5 ); 

Una expresión lambda se lleva a cabo a través del operador =>, que significa "apunta a" o "va a". Hay que fijarse en que no se está estableciendo ni el tipo del parámetro ni el tipo de retorno. Éstos se deducen de la definición del tipo delegado que se está utilizando.

Parámetros

En Vala, una función (o también llamado método) tiene una forma predeterminada de recibir los parámetros:

Sin embargo, es posible cambiar este comportamiento a través de los siguientes modificadores:

A continuación un ejemplo de definición de métodos:

void funcion_1 (int a, out int b, ref int c) {...}
void funcion_2 (Object o, out Object p, ref Object q) {...}

Utilizar estos modificadores en tipos por valor hace que en las funciones sean tratados como tipos de referencia. Por ejemplo, según los métodos anteriormente definidos:

int a = 1;
int b;
int c = 3;

funcion_1(a, out b, ref c);

Ahora un ejemplo con parámetros por referencia.

Object o = new Object();
Object p;
Object q = new Object();

funcion_2(o, out p, ref q);

Espacios de nombres

Los espacios de nombre son como los paquetes de java. Y se definen así:

namespace NombreQueSeLeQuieraDar 
{
        ...
}

En este caso sí se están utilizando nombres con notación en camello. Y la primera letra suele ser mayúscula (al contrario que en Java pero igual que en C#).

Otra forma de decir que una clase pertenece a un espacio de nombres (o paquete) es hacerlo utilizando un punto en su declaración:

class EspacioNombres1.EspacioNombres2.clase {...}

El espacio de nombres GLib es importado predeterminadamente. Cualquier cosa que no se ponga en un espacio de nombres concreto se ubicará en el espacio de nombres anónimo global.

Por supuesto, se pueden anidar espacios de nombres. Pero cualquier código externo al mismo debe utilizar el nombre completo de cualquier cosa que pretenda utilizar. Por ejemplo, para utilizar una función de una clase perteneciente a otro espacio de nombres:

NombreDelEspacioDeNombres.clase.funcion(...);

También es posible declarar el espacio de nombres y así poder utilizar, posteriormente, cualquier cosa del mismo. A continuación un ejemplo de declaración del espacio de nombres:

using nombreDelEspacioDeNombres;

Estructuras

Una estructura o tipo compuesto puede contener, de forma limitada, métodos de ejecución. Y los datos son privados. Esto significa que hay que indicar, de forma explícita, qué datos son públicos.

Por ejemplo, una definición de estrcutura o tipo compuesto:

struct NombreEstructura
{
        public int a;
        public float b;
}

Que puede ser utilizada así:

NombreEstructura estructura = NombreEsctructura()
{
        a = 5,
        b = 0.5;
}

Las estructuras, a diferencia de las clases, se comportan como un tipo de datos básico: son manejadas por valor. Esto significa que en cada asignación se produce una copia de todos sus datos.

Clases

En Vala se diferencian tres tipos de clases:

Para definir una clase se utiliza el modificador class. También se puede definir una relación de herencia con otra clase escribiendo su nombre detrás; separado de dos puntos (:).

A continuación un ejemplo de definición de clase:

class NombreClase : NombreSuperclase, NombreInterfaz
{ ... }

Las clases compactas se definen sin heredar de Glib.Object y utilizando la anotación Compact.

[compact] class NombreClaseCompacta
{ ... }

Modificadores de acceso

Una clase puede ser definida como pública o privada. De forma predeterminada es privada pero si se utiliza el modificador de acceso public, es posible que pueda ser instanciada fuera del fichero en el que se ha definido. En código C equivale a declarar su interfaz en el fichero de cabecera.

Al mismo tiempo, cada atributo y método de una clase, también puede ser definido utilizando los diferentes modificadores de acceso:

Constructores

Por la misma razón por la que no existe sobreescritura de funciones/métodos en Vala, tampoco es posible definir más de un constructor para una clase. Aunque esta limitación puede ser superada utilizando constructores con nombres distintos. Se sabe que la función es un constructor porque en su definición no se ha indicado lo que devuelve.

Por ejemplo:

public class Boton : Object 
{
        public Boton(){...}
        public Boton.con_etiqueta(string etiqueta){...}
}

Y la instanciación:

new Boton();
new Boton.con_etiqueta("pincha");

Es posible ejecutar el constructor de la clase padre a través de la palabra reservada base:

public class Superclase : Glib.Object
{
        public Superclase(int datos){...}
}

public class Subclase : Superclase
{
        public Subclase(){ base(10);}
}

Destructores

Aunque Vala gestiona la memoria por ti, es posible definir un destructor si estamos gestionando la memoria de forma manual (utilizando punteros). El destructor se define igual que el constructor pero utilizando el modificador ~ delante del nombre:

public class Clase : Object
{
        ~Clase()
        {
                stdout.printf("destruyendo...");
        }
}

Y como Vala no utiliza un recolector de basura (al contrario que Java), se puede utilizar un destructor para liberar recursos como conexiones, flujos, etc. La recolección de basura en Java es impredecible y, por lo tanto, impide utilizar destructores para este cometido (en el caso de java la sobreescritura de finally() en Object).

Propiedades

En programación orientada es una práctica habitual ocultar los detalles de implementación a los usuarios de una clase. Por ejemplo: hacer privados los atributos y permitir su uso a través de métodos de acceso. En Vala, como en C#, existe lo que se llama propiedades; que no es más que una forma más eficaz de implementar los típicos métodos de acceso de Java. Por ejemplo:

class Persona : Object
{
        //metiendo un guión bajo como prefijo evitamos 
        //conflictos con la propiedad
        private int _edad = 31;

        //implementación de propiedad de clase
        public int edad
        {
                get { return _edad; }
                set { _edad = value; }
        };
}

Y un ejemplo de utilización:

Persona david = new Persona();
david.edad = david.edad +1;
david.edad ++;

Y un ejemplo de implementación más corta de propiedad de clase:

Class Persona : Object
{
        //propiedad con getter y setter estándar 
        //y valor predeterminado
        public int edad { get; set; default = 32;}

        //propiedad de sólo lectura
        public int peso { get; private set; default 80;}
}

Señales

La librería GLib define señales para las clases que hereden de Object. Las señales son similares a los listeners de Java. Son una forma de establecer funciones externas que deben ejecutarse ante determinados eventos. Una señal es definida como un miembro de la clase. Y tiene el mismo aspecto que un método abstracto (sin implementación). Los manejadores de señales pueden entonces ser registrados utilizando el método connect(). En el siguiente ejemplo se utiliza un método anónimo como manejador de una señal de la clase:

public class Test : GLib.Object
{
        public signal void sig_1(int a);

        public static void main(string[] args)
        {
                //nueva instancia de la clase
                Test t1 = new Test();
        
                //establecer el manejador de la señal
                t1.sig_1.connect( 
                                (t,a) =>
                                {
                                        stdout.printf("%d\n", a);
                                }
                        )

                        //emitir la señal
                        t1.sig_1(5);
                )
        }
}

En este ejemplo, la razón por la que la función anónima recibe dos argumentos es que cuando se emite la señal, se pasa el objeto que lo ha emitido como primer parámetro de la función manejadora. El segundo argumento es el que indica la definición de la señal. No es necesario especificar los tipos de los parámetros en la función anónima porque Vala los deduce directemante de la señal que se está manejando.

Cuando se emite la señal se ejecutan todos los métodos conectados a la misma.

Anotaciones

Las anotaciones se utilizan para indicar al compilador cómo debe funcionar el código en la plataforma para la que se vaya a compilar. En general, no se deben utilizar anotaciones a menos que se esté escribiendo un enlace para una biblioteca externa (binding).

Un ejemplo de anotación (entre corchetes):

[Compact] [Inmutable] public class nombreClase {...}

A través de anotaciones es posible establecer a las propiedades un nombre y una descripción. Éstos se utilizarán, en tiempo de ejecución, por programas como Glade.

Todas las clases que heredan de Glib.Object disponen de una señal que se llama notify, y que es emitida cada vez que se modifica una propiedad. A continuación un ejemplo de cómo registrar una función anónima como manejador de esta señal. Donde s es el origen de la señal y p la propiedad (de tipo ParamSpec):

objeto.notify.connect(
        (s,p) =>
        {
                stdout.printf("La propiedad %s ha sido modificada\n", p.name);
        }
);

Es posible deshabilitar la notificación de cambios en las propiedades utilizando la anotación CCode:

public class MiObjeto : Glib.Object
{
        //propiedad sin notificación
        [CCode (notify = false)]
        public int sin_notificacion (get; set;);
        
        //propiedad con notificación
        public int con_notificacion (get; set;);
}

Polimorfismo

Una instancia de una clase que reimplemente alguno de los métodos de otra clase de la que esté heredando, puede utilizar ambas implementaciones: la suya propia (clase hija) y, a través de un casting, la de la clase padre (la clase de la que hereda); cuya implementación y comportamiento puede ser completamente diferente.

Por ejemplo:

class Superclase : GLib.Object
{
        public void metodo_1()
        {
                stdout.printf("metodo_1 en Superclase\n");
        }
}

class Subclase : Superclase
{
        public void metodo_1()
        {
                stdout.printf("metodo_1 en Subclase\n");
        }
}

[...]

Subclase o1 = new Subclase();
o1.metodo_1();          // <-- metodo_1 en Subclase
Superclase o2 = o1;
o2.metodo_1();          // <-- metodo_1 en Superclase

Observar que, a pesar de tratarse del mismo objeto (la misma instancia), en un caso se ejecuta un método y en otro caso se ejecuta otro.

Declarando que la función es virtual, en la clase padre, a través de la palabra reservada virtual, hacemos posible sobreescribirla en una subclase. Esto significa que si, en una subclase, sobreescribimos la función virtual, a través de la palabra reservada override, siempre se utilizará esta implementación independientemente de que se haga, o no, un casting a la clase padre.

Continuando con el ejemplo:

class Superclase : GLib.Object
{
        public virtual void metodo_1()
        {
                stdout.printf("Superclase.metodo_1()\n");
        }
}
class Subclase : Superclase
{
        public override void metodo_1()
        {
                stdout.printf("Subclase.metodo_1()\n");
        }
}

[...]

Subclase o1 = new Subclase();
o1.metodo_1();          // <-- Subclase.metodo_1()
Superclase o2 = o1;
o2.metodo_1();          // <-- Subclase.metodo_1()

Una función virtual pura necesita, siempre, ser implementada por una clase. Las clases que contienen métodos virtuales puros son precísamente las clases abstractas.

Si se ha sobreescrito un método virtual, quizás para ampliar su funcionalidad, todavía es posible llamar explícitamente a la implementación de la clase padre a través de la palabra reservada base.

Por ejemplo:

public override void funcion()
{
        base.funcion();
        
        //implementación extra
        [...]
}

Clases abstractas

Una clase abstracta es aquella que tiene métodos que deben ser implementados por todas aquellas clases que hereden de la misma.

Tanto la clase como los métodos abstractos deben ser declarados con el modificador abstract. Y en la implementación hay que indicar que se está sobreescribiendo un método abstracto (un método virtual puro) con la palabra reservada override. De lo contrario sería imposible utilizar la implementación del método cuando una instancia haga un casting al tipo de la clase abstracta.

Por ejemplo:

public abstract class Animal : Object 
{
        public void comer(){...}
        public abstract void saluda();
}

public class Pato : Animal
{
        public override void saluda()
        {
                stdout.println("¡cuack!\n");
        }
}

Una propiedad de una clase también puede ser abstracta.

Interfaces

Una interfaz es como una clase con la particularidad de que, como no contiene código, no puede ser instanciada. Se utiliza para definir las características que deben cumplir un cierto tipo de clases: nombres de métodos, parámetros, propiedades, etc.

Por lo tanto, aunque una interfaz no puede ser directamente instanciada, como sí lo puede ser una clase que la implemente, al final es como si se hubiese instanciado la propia interfaz.

Todos los métodos de las interfaces con funciones virtuales puras. Y, por lo tanto, deben declararse utilizando el modificador abstract.

La sintaxis utilizada en la definición de una clase para indicar que se está implementando una interfaz es la misma que la utilizada para indicar que se está extendiendo (heredando de) otra clase.

Las relaciones de herencia entre interfaces se llevan a cabo estableciendo requisitos que deben cumplir todas las clases que vayan a llevar a cabo la implementación.

Así, si tenemos la siguiente definición de interfaz:

public interface ITest : GLib.Object 
{
        public abstract int dato_1 {get; set;}
        public abstract void funcion_1();
}

Una posible implementación de la misma podría ser la siguiente clase:

public class Test : GLib.Object, ITest
{
        public int dato_1 {get; set;}
        public void funcion_1(){...}
}

Observar que, tal y como exije la interfaz, se está heredando de GLib.Object.

Como se ha podido observar, parece ser que como se trata de la implementación de una interfaz, y no de la extensión de una clase, no es necesario utilizar el modificador override.

Mixins

Hay una diferencia entre las interfaces de Vala y las de Java ó C#: en las interfaces de Vala se pueden implementar métodos. De ahí que todos los métodos de una interfaz deban tener el modificador abstract (los que no dispongan de implementación). Por este motivo, las interfaces de Vala pueden actuar de mixins.

Un mixin es una clase (en este caso una interfaz) que ofrece cierta funcionalidad pensada para ser heredada.

Es un enfoque de la herencia un tanto particular: no es que se defina una clase más o menos genérica y, posteriormente, se lleven a cabo extensiones que sirvan para cubrir casos específicos: animal -> perro.

El enfoque del mixin es el opuesto: se define una clase a la que, desde el principio, se le están añadiendo las características que van a ser portadas a todas las clases se decidan tenerlas como herencia: perro <- mamífero, cuadrúpedo, etc. En este último ejemplo, las clases mamífero y cuadrúpedo son mixins.

Genéricos

Sirven para abstraer una implementación de los tipos con los que ésta vaya a trabajar. La implementación de genéricos en Vala es similar a la de Java. Por ejemplo:

public class Envoltorio<G> : GLib.Object
{
        private G datos;
        
        public void set_datos(G datos)
        {
                this.datos = datos;
        }
        public G get_datos()
        {
                return this.datos;
        }
}

En el ejemplo anterior, la clase envoltorio puede trabajar con cualquier tipo de datos. Así, para utilizarla, por ejemplo con cadenas de caracteres:

Envoltorio envoltorio = new Envoltorio<string>();
envoltorio.set_datos("prueba");
string datos = envoltorio.get_datos();

Aserciones y diseño por contrato

Se trata de que un programador pueda comprobar las presunciones en tiempo de ejecución. De ahí lo del diseño por contrato.

En vala existen aserciones (assert), precondiciones (requires) y postcondiciones (ensures). Por ejemplo:

double funcion(int x, double d)
        requires (x >0 && < 10);
        requires (d >= 0.0 && d <= 1.0)
        ensures (result >= 0.0 && result <= 10.0)
{
        double a = d*x;

        //ejemplo de aserción
        assert(a != 1000);

        return a;
}

Donde result es una variable especial que representa el valor de retorno de la función.

Las comprobaciones se hacen en tiempo de ejecución. Si las comprobaciones fallan se termina la ejecución con un mensaje de error apropiado..

Se puede estar tentado de utilizar este sistema para comprobar que los datos de entrada no sean nulos. Sin embargo, hay que recordad que esta comprobación ya la hace Vala, automáticamente, para todos aquellos parámetros que no han sido señalados con el modificador ?.

Finalmente, señalar que también existen métodos especiales dentro del espacio de nombres de GLib:

Excepciones

GLib tiene un sistema de excepciones llamado GError que Vala traduce a una forma parecida a las de los lenguajes modernos de programación. Hay que tener claro cuándo se debe utilizar este sistema de gestión de errores: no es apropiado, por ejemplo, para comprobar que el valor introducido es mayor que cero. En este caso es mejor utilizar aserciones (assert), precondiciones (requires) y postcondiciones (ensures).

Los errores de Vala deben ser capturados en algún punto. Sin embargo, al contrario de por ejemplo el lenguaje Java, si un error no ha sido capturado, el compilador sólo lanza una advertencia (warning). Sin parar la compilación.

La forma de definir tipos de errores está determinada por la implementación en GLib. Cada error está compuesto de un dominio, un código de error y un mensaje. El dominio define la naturaleza del problema (como las subclases de Exception en Java), y el código de error declara la variedad exacta del problema encontrado.

Por ejemplo, una definición podría ser la siguiente:

errordomain IOError
{
        FILE_NOT_FOUND
}

Y un ejemplo de utilización, practicamente similar a la sintaxis de Java:

public class Test : GLib.Object
{
        public static void lanzador() throws IOError
        {
                throw new IOError.FILE_NOT_FOUND("no se ha encontrado el fichero");
        }

        public static void main (string[] args)
        {
                try
                {
                        lanzador();
                }catch (IOError ioe){
                        stderr.printf("¡error!");
                }finally{
                        ...
                }
        }
}

Hilos

Se pueden crear hilos de ejecución a través del método GLib.Thread.create pero para ello es necesario compilarlo de forma apropiada. Hay que utilizar el parámetro --thread. Así, además de incluir las librerías nos aseguramos de que el sistema de hilos sea inicializado cuando se necesite:

valac --thread -o test-hilo hilos.vala

Es posible comprobar en tiempo de ejecución si existe soporte para ejecución de hilos a través del método GLib.Thread.supported.

A través del método estático GLib.Thread.yield se le dice al sistema que se cede el procesador a cualquier hilo que esté en espera de ser ejecutado. Sin embargo, el sistema puede decidir cuándo se ejecuta cada hilo, y no existe ninguna garantía de que el nuevo hilo termine de ejecutarse antes de que se retome el hilo principal y se termine la ejecución del programa.

Sólo a través del método, también estático, GLib.Thread.join nos podemos asegurar de que no se continuará hasta que no termine la ejecución del hilo seleccionado, pero éste ha debido crearse para que pueda ser encajable (joinable). Por ejemplo:

void* funcion()
{
        stdout.printf("Corriendo hilo.\n");
        return null;
}

int main(string[] args)
{
        //comprobar soporte de hilos en el sistema
        if(!Thread.supported())
        {
                stderr.printf("No se puede ejecutar sin soporte de hilos en el sistema.\n");
                return 1;
        }
        
        try
        {
                //la booleana del segundo parámetro indica que el
                //nuevo hilo es encajable: encaja en la ejecución actual
                unowned Thread hilo = Thread.create(funcion, true);

                hilo.join();

                //O (creo) de otra forma:
                //Thread.join(hilo);
        }catch (ThreadError e){
                return 1;
        }
}

Por otro lado, no es suficiente con crear un hilo de ejecución. Además, es necesario crear un bucle de eventos o, de lo contrario, el programa terminará, independientemente de que se hayan creado otros hilos de ejecución, cuando termine el hilo principal (el creado por el método main).

Control de recursos

Cuando está siendo ejecutado más de un hilo, puede darse la situación de que se pretenda acceder a los mismos datos al mismo tiempo. Para controlar esta situación se puede utilizar la palabra reservada lock como en el siguiente ejemplo:

public class Test : GLib.Object 
{
        private int a { get; set; }

        public void accion_1()
        {
                lock(a)
                {
                        int tmp = a;
                        tmp++;
                        a = tmp;
                }
        }

        public void accion_2()
        {
                lock(a)
                {
                        int tmp = a;
                        tmp --;
                        a = tmp;
                }
        }
}

Donde si no se hubiesen utilizado los bloqueos, los cambios de la variable "a" habrían sido aleatorios. Sin embargo, en el ejemplo, Vala se asegura de que, si un hilo está bloqueando la variable, cualquier otro hilo que pretenda hacer el mismo bloqueo necesitará esperar. En Vala sólo es posible establecer bloqueos sobre miembros del mismo objeto que está siendo ejecutando. Todos los bloqueos deben ser internos a una clase.

Bucle principal de eventos

GLib incluye un sistema para ejecutar un bucle de eventos. De esta manera es posible escribir un programa que espera y responde a los eventos que se puedan producir. Es el mismo modelo utilizado por GTK+ para permitir la interacción del usuario.

En el siguiente ejemplo se crea un bucle de eventos que será utilizado por un un generador de ventos: un temporizador que producirá un evento cada 2000 milisegundos. El manejador de eventos registrado en el temporizador se encargará de parar el bucle y terminar la ejecución del programa:

public static void main(string[] args)
{
        var bucle = new MainLoop (null, false);
        var temporizador = new TimeoutSource(2000);

        temporizador.set_callback(
                ()=>
                {
                        stdout.printf("¡tiempo!\n");
                        bucle.quit();
                        
                        return false;
                }
        );

        temporizador.attach(bucle.get_context());
        
        bucle.run();
}

Cuando se está usando GTK+ se crea automáticamente un bucle principal de eventos que será iniciado cuando se lleva a cabo la llamada Gtk.main(). De esta manera es posible comenzar a recibir eventos de la iterfaz gráfica de usuario. A continuación otro ejemplo, equivalente al ejemplo anterior, usando GTK+. Seguimos registrando un manejador de ventos pero, en esta ocasión, utilizando los métodos GTK+ para controlar el bucle principal.

void main(string[] args)
{
        Gkt.init(ref args);
        var temporizador = new TimeoutSource(2000);
        
        temporizador.set_callback(
                ()=>
                {
                        stdout.print("¡tiempo!\n");
                        Gtk.main_quit();
                        
                        return false;
                }
        );
        
        temporizador.attach(null);
        Gtk.main();
}

Referencias débiles

Cuando se crea una nueva instancia de una clase, Vala guarda en la instancia que existe una referencia apuntando a la misma. De esta manera, cada objeto sabe cuántas referencias tiene y cuándo puede ser eliminado de la memoria.

Sin embargo, las referencias débiles declaradas a través de las palabras reservadas weak ó unowned nunca son tenidas en cuenta. Por ejemplo, para el siguiente código:

class Test
{
        private Object o;

        public weak Object obtener_referencia_debil()
        {
                Test.o = new Object();
                return o;
        }
}

Se podría hacer la siguiente llamada:

var test = new Test()
weak Object o = test.obtener_referencia_debil();

Que no sería lo mismo que esta otra:

Object o = test.obtener_referencia_debil();

Y, según el concepto de propiedad, habría que tener en cuenta las siguientes situaciones:

Por otro lado, señalar que a diferencia de los métodos normales de una clase, las propiedades siempre devuelven referencias débiles. Porque se supone que el valor de esa propiedad pertenece al objeto que la posee. Esto significa que, en un método get de una prpiedad nunca se puede, ni devolver un objeto nuevo creado dentro del mismo método get, ni devolver un valor no-débil obtenido de una llamada a cualquier otro método:

public Object propiedad
{
        get
        {
                //MAL. Una propiedad devuelve una referencia débil.
                //El objeto creado será destruido en cuanto termine la
                //ejecución del método.
                return new Object();
        }
}

public string propiedad
{
        public string dame_string()
        {
                //se devuelve un valor duplicado del texto
                return "un string";
        }

        get
        {
                //MAL. Por la misma razón anterior
                return dame_string();
        }
}

Y un ejemplo de algo correcto sería:

public string propiedad
{
        public weak string dame_string()
        {
                //las cadenas de texto son siempre propiedad
                //de la clase en la que se definen.
                //por eso no importa que no sea asignada a
                //ninguna variable no-débil.
                return "un string";
        }

        get
        {
                //BIEN. porque el método de la llamada
                //devuelve una referencia débil.
                return dame_string();
        }
}

Es posible transferir la propiedad de una instancia a través de la palabra reservada owned como si de un casting se tratase. En el siguiente ejemplo la variable origen se quedará con un valor nulo, mientras que objeto tendrá una referencia no-débil a la instancia:

Object objeto = (owned) origen;

Punteros

Cuando se crea una instancia de una clase se recibe una referencia a la misma. Vala se ocupa de destruirla cuando ésta deja de tener referencias.

Sin embargo, si optamos por utilizar punteros, tenemos la posibilidad de llevar a cabo la gestión manual de la memoria.

Cuando pedimos un puntero a una instancia somos nosotros quienes tenemos la responsabilidad de eliminar la instancia una vez ésta deje de sernos útil. Esto no es necesario en la mayoría de los casos. Quizás sólo si se desea optimizar el funcionamiento de un programa o si se pretende trabajar con una librería que no implementa una cuenta de referencias para la gestión de memoria (probablemente una librería que no esté basada en GObject).

Para obtener un puntero a una instancia:

Object* o = new Objetct();

Para acceder a los atributos de la instancia:

o->funcion_1();
o->variable_1;

Y para eliminar la instancia de la memoria, a través de la palabra reservada "delete":

delete o;

La sintaxis de punteros es similar a la del lenguaje C: se utiliza el operador & para obtener la dirección de memoria de una variable y el operador * para obtener los datos que se ubican en la dirección de memoria a la que apunta el puntero.

int i = 42;
int* punt_i = &i;       //guardamos en un puntero la dirección de memoria de la variable.
int j = * punt_i;       //asignamos el dato guardado en la dirección señalada por el puntero

Gestión de memoria

La gestión de memoria en Vala es a través de conteo de referencias. Este sistema no funciona correctamente cuando se produce un ciclo de referencias ya que, en ese caso, los objetos involucrados siguen conservando, indefinidamente, una referencia. Por lo tanto no son eliminados de la memoria cuando cabría esperar. Para evitar este problema es fundamental saber cuándo utilizar referencias débiles.

En java, y otros lenguajes que utilizan el sistema de recolección de basura, no existe este problema. Puesto que el recolector es lo suficientemente inteligente como para detectar estos casos de ciclos de referencias.

A continuación un ejemplo de ciclo de referencias:

class Nodo : Object 
{
    public Nodo anterior;
    public Nodo siguiente;

    public Nodo (Nodo? anterior = null) 
    {
        this.anterior = siguiente; // ref
        if (anterior != null) 
            anterior.siguiente = this;  // ref
    }
}

void main () 
{
    var n1 = new Nodo ();    // ref
    var n2 = new Nodo (n1);  // ref

    // escribir el número de referencias de ambos objetos
    stdout.printf ("%u, %u\n", n1.ref_count, n2.ref_count);

}   // unref, unref

En el ejemplo anterior tendríamos la siguiente representación de las referencias de los objetos. Los objetos entre paréntesis, con el número de veces que son referenciados y las variables en la parte de abajo.

(2) --> (2)
 ^  <--  ^
 |       |
 |       |
 n1     n2

Dos objetos con su número de referencias: la variable n1 apunta al objeto de la izquierda (con dos referencias) y la variable n2 apunta al objeto de la derecha (con otras dos referencias). Una vez se ha terminado la ejecución del método main se termina el programa con lo que se recuperaría toda la memoria utilizada por el mismo. Sin embargo, si el programa continuase, al terminar la ejecución del método sólo se eliminarían las referencias n1 y n2. Los objetos no serían limpiados de la memoria puesto que seguirían conservando una referencia cada uno: tenemos un ciclo de referencias.

Utilizando una referencia débil habríamos evitado este problema:

 public weak Nodo anterior;
 public Nodo siguiente;

Así, la representación pasaría a ser la siguiente:

(1) --> (2)
 ^  <--  ^
 |       |
 |       |
 n1     n2

Donde, como la referencia anterior del objeto de la derecha es débil, en el objeto de la izquierda sólo cuenta la referencia hecha por la variable n1. Así, cuando se termine la ejecución del método, y se elimine la referencia hecha por n1, el objeto de la izquierda, al perder su única referencia, sería eliminado de la memoria. El objeto de la derecha, a su vez, al haber perdido las dos referencias (n2 y la del siguiente del objeto de la izquierda) también sería eliminado de la memoria.

El soporte de conteo de referencias viene implementado en la librería Glib. Esto significa que si definimos una clase compacta perderemos, automáticamente, el soporte de gestión de memoria a través de conteo de referencias.

Un objeto que no herede de Glib.Object contiene una única referencia maestra. Esto significa que en el momento en que se pierda la referencia maestra, el objeto es eliminado de la memoria. Nunca serán determinantes el resto de referencias que se le puedan hacer.

Cuando se llama a un método que devuelve una referencia débil sólo se puede hacer dos cosas: crear una copia del objeto original, con lo que tendríamos una referencia maestra a nuestra propia copia, o traspasar la referencia maestra a una variable utilizando el modificador owned. Por ejemplo:

[Compact]
Class algo{}

void main()
{
        Algo a = new Algo();
        unowned Algo b = a;
        c = (owned) a; // c es ahora la referencia maestra
}

Y la representación gráfica antes:

a --> (algo)
b

y después de transferir la referencia maestra:

a
b
c --> (algo)

Se entiende que las variables a y b también están referenciando a la instancia. Pero no cuentan para el sistema de gestión de memoria responsable de liberar su espacio en memoria.

Aclarar que los modificadores weak y unowned no son del todo iguales. Aunque hasta ahora se pueden utilizar de forma indistinta, se supone que weak debería ser utilizado con el fin de evitar problemas de ciclos de referencia mientras que unowned debería ser utilizado para la gestión de referencias maestras.

Se puede hacer una implementación de la gestión de memoria a mano indicando, a través de anotaciones, los métodos que deben ser utilizados cada vez que se haga o deshaga una referencia. A continuación un ejemplo de implementación:

[Compact]
[CCode (ref_function = "mas_referencia", unref_function = "menos_referencia")]
public class Algo
{
        public int num_referencias = 1;
        
        public unowned Algo mas_referencia()
        {
                this.num_referencias ++;
                return this;
        }
        
        public unowned Algo menos_referencia()
        {
                if (-- this.menos_referencia == 0)
                        delete (void*) this;
        }
        
        public void metodo(){}
}

void main()
{
        Algo algo = new Algo(); //asignar, referenciar
        algo.metodo();
        Algo otro = algo;
}// desreferenciar (algo), desreferenciar (otro) => liberar memoria

Como se puede comprobar en el método main, todo vuelve a funcionar automáticamente.

Y finalmente un ejemplo de implementación de una clase compacta, inmutable (su estado interno no puede cambiar), con un método de duplicación. De esta manera Vala puede utilizar el método para hacer asignaciones a referencias maestras:

[Compact]
[Inmutable]
[CCode (copy_function = "copiar")]
public class Algo 
{
        public Algo copiar()
        {
                return new Algo();
        }
        
        public void metodo(){}
}

void main()
{
        Algo algo = new Algo(); //asignar
        algo.metodo();
        Algo otro = algo; //copiar
} //desreferenciar (algo) => liberar memoria, desreferenciar (otro) => liberar memoria

Aunque, siguiendo con el ejemplo anterior, siempre se puede evitar la copia utilizando una referencia débil:

void main()
{
        Algo algo = new Algo();
        algo.método();
        unowned Algo otro = algo;
} //desreferenciar (algo) => liberar memoria

Créditos

Autor: David Hoces Pérez [david.hoces (EN) gmail.com].

Tutorial elaborado tomando como referencia http://live.gnome.org/Vala/Tutorial.

Este documento tiene una licencia Creative Commons de reconocimiento-compartirigual CC-BY-SA.

Documentacion/Desarrollo/Tutoriales/Vala (última edición 2010-07-15 20:52:32 efectuada por DavidHoces)