Programación en Vala/Funcionalidades avanzadas del lenguaje
Aserciones y Diseño por contrato
editarCon las aserciones un programador puede comprobar que un predicado se cumple en tiempo de ejecución. La sintaxis es assert( condición ). Si la aserción falla el programa terminará su ejecución con un mensaje de error que mostrará tal circunstancia. Hay varios métodos de aserciones definidos en la GLib y que el programador podrá usar, entre los que se encuentran:
Aserción | Descripción |
assert_not_reached() | Esta aserción se ejecuta en el caso en el que se llegue a ejecutar la línea en la que se ha definido. |
return_if_fail(expresión_booleana) | Si la expresión booleana es falsa, un mensaje de error crítico se muestra y se finaliza la ejecución de la función. Esto sólo se puede usar en métodos que no devuelven ningún valor. |
return_if_reached(val) | Si se ejecuta devuelve el valor val y cancela la ejecución del método. |
warn_if_fail(expresión_booleana) | Muestra un mensaje de aviso si la expresión booleana resulta ser falsa. |
warn_if_reached() | Si se ejecuta la línea dónde se encuentra definido entonces se muestra un mensaje de error. |
Puede que el lector quiera usar las aserciones para comprobar si los argumentos de un método tienen valor o son null, sin embargo, esto no es necesario, ya que, Vala realiza esto de forma implícita sobre todos los parámetros que no están marcados con ? (el indicador de que pueden ser de valor null).
void method_name(Foo foo, Bar bar) {
/* No es necesario por que Vala lo hace por tí!:
return_if_fail(foo != null);
return_if_fail(bar != null);
*/
}
Vala soporta las funcionalidades básicas de el diseño por contrato. Un método puede tener precondiciones (requires) y postcondiciones (ensures) que deben ser satisfechos al principio o al final de un método respectivamente:
double method_name(int x, double d)
requires (x > 0 && x < 10)
requires (d >= 0.0 && d <= 1.0)
ensures (result >= 0.0 && result <= 10.0)
{
return d * x;
}
La variable result es una variable especial que representa el valor de retorno de un método.
Manejo de errores
editarGLib tiene un sistema para manejar las excepciones en tiempo de ejecución llamado GError. Vala traduce este sistema a una forma familiar a la forma en que lo hacen los lenguajes de programación modernos, pero esto no quiere decir que lo haga exactamente igual que Java o C#. Es importante considerar cuando usar este tipo de manejo de errores, GError ha sido diseñado para tratar con errores que no sean fatales, es decir, cosas que no son sabidas por el programa hasta que se ejecuta en un sistema, y no son fatales para la ejecución del programa. No se debería usar GError para reportar problemas que pueden ser previstos, como mostrar que se ha pasado un valor inválido al método. Si un método, por ejemplo, necesita un número mayor que como parámetro, debería realizarse la comprobación de los valores negativos usando para ello técnicas de programación de diseño por contrato como las precondiciones o las aserciones como se ha descrito en la sección anterior.
Los errores en Vala son denominadas como excepciones, las cuales significan que estos errores deben ser controlados en algún punto del código. Sin embargo, si no se controla una excepción el compilador de Vala informará de que no se ha hecho esta comprobación, mediante un aviso, pero no parará la compilación del código.
El proceso de una excepción (o error en la terminología Vala) se compone de:
- Declarar que un método puede lanzar una excepción:
void my_method() throws IOError {
// ...
}
- Lanzar el error en el lugar adecuado:
if (something_went_wrong) {
throw new IOError.FILE_NOT_FOUND("Requested file could not be found.");
}
- Controlar el error en un punto del código adecuado:
try {
my_method();
} catch (IOError e) {
stdout.printf("Error: %s\n", e.message);
}
- Comparar el código con el operador is:
IOChannel channel;
try {
channel = new IOChannel.file("/tmp/my_lock", "w");
} catch (FileError e) {
if(e is FileError.EXIST) {
throw e;
}
GLib.error("", e.message);
}
Todo esto es similar más o menos como se hace en otros lenguajes, pero definir los tipos de errores permitidos es algo único. Los errores o excepciones tienen tres componentes, conocidos como dominio, código y mensaje. Los mensajes se han visto anteriormente, y son cadenas de texto suministradas cuando el error es creado. Los dominios del error describen el tipo del problema, y se asimilan a la subclase de la excepción en Java o similares. En los ejemplos anteriores se describe un dominio de error llamado IOError. La tercera parte, el código es un refinamiento describiendo la variedad exacta del problema encontrado. Cada dominio de error tiene uno o más códigos de error, en el ejemplo se usa un código de error llamado FILE_NOT_FOUND.
La forma de definir esta información acerca de los tipos de error está relacionado con la implementación en la biblioteca GLib. Para entenderlo mejor se necesita una definición como la siguiente:
errordomain IOError {
FILE_NOT_FOUND
}
Cuando se controla un error, se da el dominio del error en el que se encuentra definido los errores a controlar, y si un código de error definido en ese dominio es lanzado, el código fuente que define este bloque se ejecuta con el valor del error lanzado. Del objeto error lanzado se puede extraer el código de error y el mensaje si se necesita. Si se quiere controlar errores de más de un dominio, simplemente se utilizan más bloques de control de errores. También existe un bloque opcional que se coloca después de los bloques de errores definidos por las sentencias try y catch, llamado finally. Este código será ejecutado siempre al final de la sección, independientemente de si se ha lanzado un error o si se ha ejecutado un bloque catch, incluso si el error no ha sido controlado y será lanzado otra vez. Esto permite, por ejemplo, que cualquier recurso reservado en el bloque try será liberado sin importar cualquier error lanzado. Un ejemplo completo de todas las funcionalidades descritas es el siguiente:
errordomain ErrorType1 {
CODE_1A
}
errordomain ErrorType2 {
CODE_2A
}
public class Test : GLib.Object {
public static void thrower() throws ErrorType1, ErrorType2 {
throw new ErrorType1.CODE_1A("Error");
}
public static void catcher() throws ErrorType2 {
try {
thrower();
} catch (ErrorType1 e) {
// Se controlan los errores definidos en el dominio de error ErrorType1
if (e is ErrorType1.CODE_1A) {
// Se controla específicamente el error CODE_1A perteneciente al dominio de errores ErrorType1
}
} finally {
// Bloque finally para realizar las acciones necesario de limpieza
}
}
public static int main(string[] args) {
try {
catcher();
} catch (ErrorType2 e) {
// Se controlan los errores definidos en el dominio de error ErrorType2
}
return 0;
}
}
Este ejemplo tiene dos dominios de errores, ambos puede ser lanzados por el método lanzador llamado thrower. El método llamado catcher sólo puede lanzar el segundo tipo de errores, y por lo tanto debe manejar el primer tipo si thrower lo lanza. Finalmente el método main controlará cualquier tipo de error desde catcher (de los dominios de errores que el método catcher ha definido que puede lanzar y que no controla el método).
Dirección de parámetros
editarUn método en Vala puede recibir cero o más parámetros. El comportamiento por defecto cuando se llama a un método es el siguiente:
- Cualquier parámetro que sea un tipo pasado por valor se copia a una dirección local al método cuando se ejecuta.
- Cualquier parámetro de tipo referencia no se copia, en lugar de esto se pasa una referencia al método que se ejecuta.
Este comportamiento se puede modificar utilizando para ello los modificadores de parámetros ref y out. Hay cuatro combinaciones posibles dependiendo de si el programador utiliza estos modificadores en la llamada al método o en la definición del mismo:
- out utilizado en la llamada al método: se pasará una variable no inicializada al método y se puede esperar que esté inicializada después de que el método finalice la ejecución.
- out utilizado en la definición del método: el parámetro se considera no inicializado y el programador tendrá que inicializarlo.
- ref utilizado en la llamada al método: la variable que se pasa al método tiene que ser inicializada y puede ser modificada dentro del método.
- ref utilizado en la definición del método: el parámetro se considera que está inicializado y puede ser modificado dentro del método.
Ejemplo:
void method_1(int a, out int b, ref int c) { ... }
void method_2(Object o, out Object p, ref Object q) { ... }
Estos métodos definidos en el ejemplo de arriba podrán ser llamados de las siguientes formas:
int a = 1;
int b;
int c = 3;
method_1(a, out b, ref c);
Object o = new Object();
Object p;
Object q = new Object();
method_2(o, out p, ref q);
El tratamiento que se les va a dar a las variables será el siguiente:
- La variable "a" es de tipo valor. El valor será copiado a una posición de memoria nueva y local al método, y por lo tanto los cambios realizados dentro del método no afectarán al valor original de la misma.
- La variable "b" es de tipo valor también, pero se ha pasado mediante el modificador out. En este caso, el valor no es copiado, en lugar de esto se pasa una referencia (puntero) al valor original, y por lo tanto cualquier cambio de la variable en el método se reflejará en el valor original.
- La variable "c" se trata de la misma forma que "b", con la única salvedad de que se ha se ha señalado claramente en el método.
- La variable "o" es de tipo referencia. El método recibe una referencia con el mismo objeto que cuando se realiza la llamada. El método puede cambiar el objeto, pero el cambio no será visible para el código que ha llamado al método.
- La variable "p" es de tipo referencia, pero pasada mediante el modificador out. Esto significa que el método recibirá un puntero a la referencia del objeto. El método puede reemplazar la referencia con otro referencia a otro objeto distinto, y cuando el método finalice su ejecución el código que ha llamado al método obtendrá un objeto totalmente distinto. Cuando se use este tipo de parámetro si el método no reasigna la referencia entonces se establece a null al finalizar la ejecución del método.
- La variable "q" es de nuevo del mismo tipo que la anterior. En este caso se maneja como "p" con una importante diferencia, ya que el método puede elegir modificarla o no la referencia, y puede acceder al objeto al que referencia. Vala se asegurará que la instancia "q" apunte a cualquier objeto, y por lo tanto no sea null cuando se entra en el método.
Colecciones
editarLa biblioteca de funciones Gee es una biblioteca de colecciones escrita en Vala. Las clases deberían resultar familiares a los desarrolladores que han usado bibliotecas como la JFC (Java's Foundation Classes). Gee está formado por un conjunto de interfaces y distintos tipos de datos que se implementan de distintas formas.
Si se desea usar Gee en una aplicación, es necesario instalar la biblioteca en el sistema de forma separada. Se puede obtener Gee desde el enlace. Para usar la biblioteca es necesario compilar el programa usando la opción --pkg gee-1.0.
Los tipos de datos más útiles son:
- Listas: Colecciones ordenadas de elementos, accesibles por un índice numérico.
- Conjuntos: Colecciones de elementos distintos desordenadas.
- Mapas (Diccionarios): Colecciones de elementos desordenados, accesibles por un índice de tipo arbitrario.
Todas las listas y conjuntos en la biblioteca implementan la interfaz Collection, y todos los mapas la interfaz Map. Las listas implementan asimismo la interfaz List mientras que los conjuntos implementan la interfaz Set. Estas interfaces comunes significan no sólo que todas las colecciones son de un tipo similar y que pueden ser usadas de manera intercambiable, sino que las colecciones nuevas pueden ser escritas usando las mismas interfaces, y por lo tanto usando código ya existente.
También es común a todas las colecciones la interfaz Iterable. Esto significa que cualquier objeto de esta categoría puede ser iterado mediante los métodos estándar de la interfaz, o directamente mediante la sintaxis de Vala, usando foreach.
Todas las clases e interfaces usan tipos genéricos. Esto significa que deben ser instanciadas con un tipo particular o conjunto de tipos que van a contener. El sistema se asegurará de que sólo los tipos especificados puedan añadirse a una colección, y que cuando los objetos se extraigan desde una colección se devuelvan con ese tipo.
ArrayList<G>
editarImplementa: Iterable <G>, Collection <G>, List <G>.
Es una lista ordenada de elementos del tipo G implementada por un vector que crece de forma dinámica. Este tipo es muy rápido para acceder a los datos, pero potencialmente lento cuando se insertan elementos en cualquier posición que no sea al final, o al insertar elementos cuando el vector ya está lleno (y hay por tanto que redimensionarlo).
Ejemplo:
using Gee;
static int main (string[] args) {
var list = new ArrayList<int> (); // Se crea un arraylist de enteros
list.add (1); // Se añade un elemento al final
list.add (2);
list.add (5);
list.add (4);
list.insert (2, 3); // Se inserta el elemento '2' en la posición '3'
list.remove_at (3); // Se elimina el elemento de la posición '3'
// Se recorre todo el vector
foreach (int i in list) {
stdout.printf ("%d\n", i);
}
list[2] = 10; // Se obtiene el mismo resultado que con '''list.set (2, 10)'''
stdout.printf ("%d\n", list[2]); // Se obtiene el mismo resultado que con '''list.get (2)'''
return 0;
}
Para compilar este programa se tiene que ejecutar algo similar a lo que aparece en la siguiente línea de comandos:
valac programa_gee.vala --pkg gee-1.0
Como se puede ver en el código de arriba lo primero que se hace es crear el objeto de tipo ArrayList de tipo entero lo cual se especifica entre los símbolos de menor y mayor. Después se usa el método add para añadir un elemento al final de la colección. Esta inserción es rápido en comparación con la inserción de un elemento en una posición determinada de la lista; algo que se consigue mediante el uso del método insert, al que se le pasa el elemento y la posición. Se cuenta con el método remove_at que se usa para eliminar un elemento en una posición determinada, indicando la posición como parámetro. Se ve como se puede recorrer toda la lista mediante el uso de la construcción foreach. Por último se como es posible acceder a la lista mediante el uso de la sintaxis de vector indicando la posición entre corchetes. Esta sintaxis se puede usar tanto para acceder como modificar el valor de una posición determinada. Para más información sobre las listas se puede consultar la API de Vala al respecto en [1].
HashMap<K,V>
editarImplementa: Iterable <Entry<K,V>>, Map <K,V>
Es un mapa en relación 1 a 1 de elementos de tipo K a elementos de tipo V. El mapeo se hace mediante el cálculo de un valor hash para cada llave, esto puede ser modificado mediante el uso de punteros a funciones para el cálculo de funciones de hash y funciones que comprueben la igualdad de las claves de una forma concreta.
Se puede pasar opcionalmente una función de hash y un comparador al constructor de la siguiente forma:
var map = new Gee.HashMap<Foo, Object>(foo_hash, foo_equal);
Ejemplo:
using Gee;
static int main (string[] args) {
var map = new HashMap<string, int> ();
map.set ("one", 1);
map.set ("two", 2);
map.set ("three", 3);
map["four"] = 4; // same as map.set ("four", 4)
map["five"] = 5;
stdout.printf("%d\n", map["four"]);
foreach (string key in map.keys) {
stdout.printf ("%d\n", map[key]); // same as map.get (key)
}
return 0;
}
Este programa se compila de la misma forma que el anterior. El programa define inicialmente un mapa map que tendrán claves de tipo cadena y valores de tipo entero. Estos mapas puede definir valores de tuplas mediante el método set de los objetos o mediante la sintaxis de corchetes. Así se puede usar también el método get para obtener un valor dado una clave o mediante la sintaxis de los corchetes. Para más información sobre este tipo de colecciones se puede consultar la API en [2].
HashSet<G>
editarImplementa: Iterable <G>, Collection <G>, Set <G>
Un conjunto de elementos del tipo G que no estén repetidos. Los elementos duplicados se detectan calculando un valor resumen (hash) para cada clave, esto puede modificarse pasando un método que calcule el resumen y un método que compruebe la igualdad de una forma específica. Esto se realiza de la misma forma que con un mapa de tipo HashMap.
Ejemplo:
using Gee;
static int main (string[] args) {
var my_set = new HashSet<string> ();
my_set.add ("one");
my_set.add ("two");
my_set.add ("three");
my_set.add ("two"); // No se podrá añadir por que ya está en el conjunto
foreach (string s in my_set) {
stdout.printf ("%s\n", s);
}
return 0;
}
En el ejemplo de arriba se añaden elementos al conjunto mediante el método add. Para recorrer todo el conjunto se puede usar la construcción foreach de Vala. Para acceder se usa el método contains que nos indica si un elemento se encuentra dentro del conjunto o no.
Vista de sólo lectura
editarSe puede obtener una vista de una colección de sólo lectura mediante la propiedad read_only_view, por ejemplo, mi_mapa.read_only_view. Esto devolverá una vista que tiene la misma interfaz que la colección que contiene, pero no permitirá ninguna modificación, o cualquier acceso a la colección que contiene.
Métodos con soporte de sintaxis
editarVala reconoce algunos métodos que tienen un nombre determinado y un conjunto de parámetros y suministra soporte para ellos. Por ejemplo, si una clase tiene un método llamado contains() los objetos de este tipo se pueden utilizar con el operador in. La siguiente tabla muestra unos métodos especiales que Vala reconoce:
Método | Operador | Descripción |
get(TIPO índice) | Acceso mediante objeto[índice] | Permite el acceso mediante un índice (del tipo definido) a un objeto indexado |
void set(TIPO1 índice, TIPO2 item) | Acceso mediante objeto[índice] = item | Permite la inserción de un item en (del tipo definido) a un objeto indexado |
get(TIPO1 índice1, TIPO2 índice2) | Acceso mediante objeto[índice1, índice2] | Permite el acceso mediante dos índices (cada uno de un tipo que puede ser distinto o no) a un objeto doblemente indexado (por ejemplo una matriz) |
void set(TIPO1 índice1, TIPO2 índice2, TIPO2 item) | Acceso mediante objeto[índice1, índice2] = item | Permite la inserción de un item en (del tipo definido) a un objeto doblemente indexado (como por ejemplo una matriz) |
slice(long start, long end) | Trocea objetos mediante objeto[start:end] | Permite trocear un objeto indexado mediante el operador por defecto, indicando un inicio del troceo start, y un final end |
bool contains(T needle) | Comprueba que un subconjunto esté dentro de un conjunto mediante bool b = needle in object | Permite comprobar si un subconjunto needle se encuentra presente dentro del conjunto object. En case de estar presente debe devolver un valor true, en caso contrario false |
string to_string(void) | Convierte el objeto en una cadena y permite su uso dentro de las plantillas de cadenas mediante @"$object" | Permite convertir un objeto en cadena y por lo tanto se podrá usar en diversas situaciones entre ellas en las plantillas de cadenas. |
Iterator iterator(void) | Permite recorrer un objeto mediante la estructura foreach" | Permite recorrer un objeto indexado de alguna forma mediante la estructura foreach del lenguaje. |
El siguiente ejemplo muestra algunos de los métodos especificados:
public class EvenNumbers {
public int get(int index) {
return index * 2; // Devuelve el número par que ocupa la posición index del conjunto de los números pares
}
public bool contains(int i) {
return i % 2 == 0; // Nos dice si el elemento i se encuentra dentro del conjunto de números pares
}
public string to_string() {
return "[This object enumerates even numbers]"; // La representación en cadena muestra ayuda de lo que representa la clase
}
public Iterator iterator() {
return new Iterator(this); // Permite que el conjunto de los números pares se recorra mediante foreach
}
public class Iterator {
private int index;
private EvenNumbers even;
public Iterator(EvenNumbers even) {
this.even = even; // Constructor del iterador
}
public bool next() {
return true; // El método nos indica si el conjunto tiene o no un número par o no
}
public int get() {
this.index++; // Devuelve el siguiente número par del iterador
return this.even[this.index - 1];
}
}
}
void main() {
var even = new EvenNumbers();
stdout.printf("%d\n", even[5]); // get()
if (4 in even) { // contains()
stdout.printf(@"$even\n"); // to_string()
}
foreach (int i in even) { // iterator()
stdout.printf("%d\n", i);
if (i == 20) break;
}
}
Multihilo
editarHilos en Vala
editarUn programa escrito en Vala puede tener más de un hilo en ejecución, permitiéndole hacer más de una cosa al mismo tiempo. Fuera del ámbito de Vala los hilos comparten un mismo procesador o no, dependiendo del entorno de ejecución.
Un hilo en Vala no se define en el tiempo de compilación, en lugar de eso se define una porción del código Vala para que se ejecute como un nuevo hilo. Esto se realiza mediante el método estático de la clase Thread de la biblioteca GLib, como puede verse en siguiente ejemplo:
void* thread_func() {
stdout.printf("Thread running.\n");
return null;
}
int main(string[] args) {
if (!Thread.supported()) { // Se comprueba si se soportan la ejecución con hilos
stderr.printf("No puede ser ejecutado sin hilos.\n");
return 1;
}
try {
Thread.create(thread_func, false);
} catch (ThreadError e) {
return 1;
}
return 0;
}
Este programa pedirá que un nuevo hilo se cree y ejecute. El código a ejecutar está contenido en el método thread_func. Nótese también la comprobación que se realiza al principio del método principal, un programa en Vala no podrá usar hilos a no ser que sea compilado de una forma adecuada, de esta forma si se compila de la forma habitual, mostrará un mensaje de error y parará la ejecución. La posibilidad de comprobar el soporte de hilos en tiempo de ejecución permite al programa ser construido para ser ejecutado con o sin hilos si se quiere. Para compilar con soporte de hilos, se debe ejecutar una línea de comandos similar a la siguiente:
$ valac --thread threading-sample.vala
Esto incluirá las bibliotecas necesarias e inicializará el sistema de hilos cuando sea posible. El programa se ejecutará ahora sin producirse fallos de violación de acceso, pero no se comportará como se espera. Sin un conjunto de bucles, el programa terminará cuando el hilo principal (el que ha sido creado con la función principal) finalice. Para controlar este comportamiento, se puede permitir cooperar a los hilos entre sí. Esto se puede realizar mediante los bucles de eventos y las colas asíncronas, pero en esta introducción a los hilos se mostrará las posibilidades básicas de los hilos.
Es posible para un hilo comunicarle al sistema que ahora no necesita ejecutarse, y por lo tanto sugerir que sea otro hilo el que debería ejecutarse en lugar del primero, esto se realiza mediante el método estático Thread.yield(). Si esta sentencia se coloca al final del método main definido arriba, el sistema de ejecución pausará la ejecución del hilo principal por un instante y comprobará si hay otros hilos que pueden ejecutarse, encontrando el hilo creado recientemente en un estado de pausa y a la espera de ejecución, y ejecutará el nuevo hilo hasta la finalización del mismo, y el programa tendrá el comportamiento esperado. Sin embargo, no hay garantía de que esto pase así. El sistema tiene potestad para decidir cuando ejecuta los hilos, y de esta forma podría no permitir que el nuevo hilo termine antes de que el hilo principal es reiniciado y el programa finalice.
Para esperar a que un hilo finalice de forma completa hay un método llamado join(). LLamando a este método en un objeto Thread causa que el hilo que ha realizado la llamada espere a que finalice el otro hilo antes de finalizar. También permite a un hilo recibir el valor de retorno de otro, si eso es útil. Para implementar la unión de hilos se realiza mediante un código similar al siguiente:
try {
unowned Thread thread = Thread.create(thread_func, true);
thread.join();
} catch (ThreadError e) {
return 1;
}
Esta vez, cuando se cree el hilo se le pasará true como último argumento. Esto marca el hilo como que puede ser unido (joinable). Se recuerda el valor devuelto desde la creación, una referencia sin propietario al objeto Thread (las referencias sin propietario se explicarán después y no son vitales para esta sección). Con esta referencia es posible unir el nuevo hilo al hilo principal. Con esta versión del programa se garantiza que el nuevo hilo creado se ejecutará al completo antes de que el primer hilo continúe y finalice la ejecución del programa.
Todos estos ejemplos tienen un problema potencial, en el cual el hilo creado no sabe en que contexto debería ejecutarse. En el lenguaje C se suministraría a la creación del hilo algunos datos más, en Vala en cambio se pasaría una instancia del método a Thread.create, en lugar de un método estático.
Control de recursos
editarCuando más de un hilo se esté ejecutando al mismo tiempo, existe la posibilidad de que los datos sean accedidos de forma simultanea. Esto puede hacer que el programa no sea determinista, ya que la salida depende de cuando el sistema decide cambiar la ejecución entre los hilos.
Para controlar esta situación, se puede usar la palabra reservada lock para asegurarse de que un bloque de código no será interrumpido por otros hilos que necesitan acceder al mismo dato. La mejor forma de mostrar esto es mediante un ejemplo como el siguiente:
public class Test : GLib.Object {
private int a { get; set; }
public void action_1() {
lock (a) {
int tmp = a;
tmp++;
a = tmp;
}
}
public void action_2() {
lock (a) {
int tmp = a;
tmp--;
a = tmp;
}
}
}
Esta clase define dos métodos, dónde ambos necesitan cambiar el valor de la variable a. Si no estuviera el bloque definido por la palabra reservada lock, podría ser posible para las instrucciones de esos métodos cambiaran el valor de a y el resultado de las operaciones sería aleatorio. Como se ha establecido los bloques lock Vala garantizará que si un hilo ha bloqueado la variable a, otro hilo que necesita la misma variable tenga que esperar su turno hasta que el primero finalice de manipularla.
En Vala sólo es posible bloquear miembros de un objeto que está ejecutando el código. Esto podría parecer una restricción muy grande, pero de hecho el uso estándar de esta técnica debería incluir clases que son responsables individualmente de controlar un recurso, y por lo tanto todos los bloqueos deben ser internos a la clase. Del mismo modo, en el ejemplo de arriba todos los accesos a la variable a se encapsulan en la clase.
El bucle principal
editarLa biblioteca GLib incluye un sistema para la ejecución de un bucle de eventos, en las clases alrededor del bucle principal MainLoop. El propósito de este sistema es permitir escribir programas que esperen a que sucedan eventos para responder a dichos eventos, en lugar de tener que estar comprobando las condiciones continuamente hasta que se cumplan. Este es el modelo que usa la biblioteca GTK+, para que el programa espere hasta que se produzca la interacción con el usuario sin necesidad de tener código ejecutándose en el momento que se produce la interacción.
El siguiente programa crea e inicia el bucle MainLoop, y enlaza un conjunto de eventos a dicho bucle. En este caso el código es un simple temporizador, el cual ejecutará el método después de 2000ms. El método de hecho parará el bucle principal, el cual hará que finalice el programa.
void main() {
var loop = new MainLoop();
var time = new TimeoutSource(2000);
time.set_callback(() => {
stdout.printf("Time!\n");
loop.quit();
return false;
});
time.attach(loop.get_context());
loop.run();
}
El código crea un nuevo bucle principal (MainLoop) y después inicializa un temporizador TimeoutSource. Utilizando un método anónimo se añade el evento del temporizador al bucle principal. Así cuando el temporizador llegue a 0 lanzará un evento que hará que se ejecute el método anónimo definido, el cual imprime por pantalla un mensaje y después sale de la ejecución del bucle principal y finaliza la ejecución del programa.
Cuando se usa GTK+, se crea un bucle principal automáticamente, y se ejecutará cuando se lance el método Gtk.main(). Esto marca el punto dónde el programa está listo para ejecutarse y empezar a aceptar eventos del usuario u otros eventos externos. El código en GTK+ es equivalente al ejemplo de arriba, y por lo tanto se puede añadir eventos de la misma forma, aunque se necesite usar los métodos GTK+ para controlar el bucle principal.
void main(string[] args) {
Gtk.init(ref args);
var time = new TimeoutSource(2000);
time.set_callback(() => {
stdout.printf("Time!\n");
Gtk.main_quit();
return false;
});
time.attach(null);
Gtk.main();
}
Un requisito común en el desarrollo de interfaces de usuario es ejecutar el código tan pronto como sea posible, pero sólo cuando no moleste al usuario. Para ello, se puede usar las instancias de tipo IdleSource. Estas mandan eventos al bucle principal del programa, pero las peticiones sólo serán tratadas cuando no hay nada más importante que hacer.
Para obtener más información acerca de la biblioteca de funciones GTK+ se puede consultar la documentación de GTK+ o la documentación de la API de Vala.
Métodos asíncronos
editarCon los métodos asíncronos es posible programar sin realizar bloqueos de ninguna clase. Desde la versión 0.7.6 del compilador de Vala, se suministra una sintaxis especial para la programación asíncrona.
Sintaxis y ejemplos
editarUn método asíncrono se define mediante el modificador async. Se puede llamar a un método asíncrono con la sintaxis nombre_metodo.begin() desde un método síncrono. Desde un método asíncrono se pueden llamar a otros métodos asíncronos utilizando la palabra reservada yield. Esto hará que el método llamador se suspenda hasta que otro método devuelva el valor de retorno (y finalice por tanto su ejecución). Todo esto se realiza implícitamente mediante llamadas con AsyncResult. Todo lo relacionado con los método asíncronos en Vala depende de la biblioteca GIO, por lo que se debe compilar los programas con la opción --pkg gio-2.0. A continuación se muestra un ejemplo de este tipo de métodos:
// Ejemplo con métodos asíncronos
async void list_dir() {
var dir = File.new_for_path (Environment.get_home_dir()); // Se obtiene un objeto fichero del directorio HOME
try {
var e = yield dir.enumerate_children_async(FILE_ATTRIBUTE_STANDARD_NAME,
0, Priority.DEFAULT, null); // Se obtienen los ficheros/directorios que contiene el directorio HOME
while (true) {
var files = yield e.next_files_async(10, Priority.DEFAULT, null); // Se van obteniendo hasta que devuelve null y no hay más
if (files == null) {
break;
}
foreach (var info in files) {
print("%s\n", info.get_name()); // Se muestran todos los ficheros obtenidos
}
}
} catch (Error err) {
warning("Error: %s\n", err.message);
}
}
void main() {
list_dir.begin();
new MainLoop().run();
}
El método list_dir() no es bloqueante. Dentro de list_dir(), el método asíncrono enumerate_children_async() y next_files_async() se llaman con la palabra reservada yield. El método list_dir() continuará ejecutándose mientras que se devuelvan los valores de retorno de los método asíncronos y finalicen su ejecución.
Métodos asíncronos personalizados
editarEl ejemplo anterior usaba métodos de la biblioteca GIO para demostrar el uso del método .begin() y de la palabra reservada yield. Pero es posible escribir métodos asíncronos de manera personalizada. A continuación se explicará la manera de hacerlo.
// Ejemplo con métodos asíncronos personalizados:
class Test : Object {
public async string test_string(string s, out string t) {
assert(s == "hello");
Idle.add(test_string.callback);
yield;
t = "world";
return "vala";
}
}
async void run(Test test) {
string t, u;
u = yield test.test_string("hello", out t);
print("%s %s\n", u, t);
main_loop.quit();
}
MainLoop main_loop;
void main() {
var test = new Test();
run.begin(test);
main_loop = new MainLoop();
main_loop.run();
}
La llamada .callback se usa para registrar de forma implícita un método _finish para el método asíncrono. Esto se usa con la palabra reservada yield.
// Se añada el callback al método
Idle.add(async_method.callback);
yield;
// Se devuelve el resultado
return result;
Después de la sentencia yield; el resultado se puede devolver. De manera implícita, se puede realizar con un AsyncResult en el método callback. El método calback se parece mucho al concepto de continuación en ciertos lenguajes de programación (por ejemplo Scheme) salvo que en Vala representa el contexto inmediatamente posterior a la sentencia yield.
El método end() es la sintaxis que se usa para el método _finishi. Toma un AsyncResult y devuelve el resultado real o lanza una excepción (si el método asíncrono lo hace). La llamada se realiza en el callback del método asíncrono de una forma similar a la siguiente:
async_method.end(result)
Referencias débiles
editarLa gestión de memoria en Vala se basa en el conteo automático de referencias. Cada vez que un objeto se asigna a una variable su contador de referencias se incrementa en 1, cada vez que una variable, la cual referencia un objeto, sale del ámbito; su contador interno de referencias se decrementa en 1. Si el contador de referencias alcanza el valor 0 el objeto será liberado (el bloque de memoria que contiene ese objeto será liberado).
Sin embargo, es posible formar un ciclo de referencias con las estructuras de datos que el programador defina. Por ejemplo, con una estructura de árbol de datos dónde un nodo hijo mantiene una referencia a su padre y viceversa, o una lista doblemente enlazada dónde cada elemento mantiene una referencia a su predecesor y el predecesor mantiene una referencia a su sucesor.
En estos casos los objetos podrían mantenerse vivos simplemente referenciándose unos a otros, a pesar de que deberían ser liberados. Para romper este ciclo de referencias se pueden usar el modificador weak para una de las referencias:
class Node : Object {
public Node prev;
public Node next;
public Node (Node? prev = null) {
this.prev = prev; // ref
if (prev != null) {
prev.next = this; // ref
}
}
}
void main () {
var n1 = new Node (); // ref
var n2 = new Node (n1); // ref
// Imprime el conteo de referencias para los dos objetos
stdout.printf ("%u, %u\n", n1.ref_count, n2.ref_count);
} // unref, unref
Los lugares dónde se producen las referencias y los borrados de referencias se han comentado para una mejor comprensión del ejemplo. La siguiente figura muestra la situación después de que los nodos A, B y C hayan sido asignados y enlazados:
Cada flecha representa un enlace a un objeto de la lista doblemente enlazada. Se puede ver como en el ejemplo cada vez que se asigna el objeto a un enlace se aumenta el contador de referencias. Al finalizar todos las asignaciones de los objetos se deben obtener un contador de referencias de dos para cada nodo. Cuando se finalice el uso de los nodos éstos se eliminará la referencia a ellos, por lo que el contador de referencias se valdrá 1 y no 0 por lo que la memoria que ocupan no será liberada. En este caso el programa finalizará y el trabajo que no ha realizado el programa (liberar esos recursos) lo hará el sistema operativo. Sin embargo que pasaría si el programa fuera algo así:
void main () {
while (true) {
var a = new Node ();
var b = new Node (a);
var c = new Node (b);
Thread.usleep (1000);
}
}
Para comprobarlo sólo tienes que abrir el gestor de tareas (por ejemplo gnome-system-monitor) e iniciar el programa. Podrás ver que ese programa está devorando la memoria. Finaliza el proceso antes de que haga que tu sistema no responda adecuadamente (la memoria será liberada inmediatamente).
Un programa equivalente en C# o Java no tendría ningún problema, por que el recolector de basuras puede detectar estos ciclos en las referencias en tiempo de ejecución. Pero Vala esto no lo realiza (por que no hay recolector de basuras) y el programador debe tener en cuenta este tipo de problemas.
La forma de romper el ciclo es mediante la definición de alguna de las referencias como una referencia débil (weak):
public weak Node prev;
public Node next;
Este modificador hace que la asignación de esta variable no haga que su contador de referencias se incremente en 1. De esta forma uno de los nodos tendrá un contador de referencias de 1 en lugar de 2, por lo que cuando finalice el programa se eliminará esa referencias y el contador de referencias alcanzará el valor de 0, por lo que se liberará la memoria que ocupaba ese nodo. Esto al provocar que todas las referencias que contenía el nodo se eliminen, hará que se produzca un efecto cascada liberando de memoria todos los nodos que había.
Propiedad de las referencias
editarReferencias sin propietario
editarNormalmente cuando se crea un objeto en Vala se devuelve una referencia que apunta a dicho objeto. Esto significa que además de haberse pasado un puntero al objeto a memoria, también se ha almacenado en el propio objeto que ese puntero existe. De forma similar, cuando otra referencia al objeto se crea, también es almacenada. De la misma forma que un objeto sabe cuantas referencias tiene, puede ser eliminado de ellas cuando lo necesite. Este es el comportamiento básico de la gestión de memoria.
Las referencias sin propietario no se almacenan en el objeto al que referencian. Esto permite al objeto ser eliminado cuando deba serlo, sin importar el hecho de que aún haya referencias que apunten hacia él. La forma usual de alcanzar esto es con un método definido con un valor de vuelta de una referencia sin propietario, como el siguiente código muestra:
class Test {
private Object o;
public unowned Object get_unowned_ref() {
this.o = new Object();
return this.o;
}
}
Cuando se llame a este método, para recoger una referencia al objeto devuelto, se debe esperar recibir una referencia débil:
unowned Object o = get_unowned_ref();
La razón para este ejemplo tan complicado es debida al concepto de propiedad:
- Si el objeto "o" no se almacena en la clase, entonces cuando el método get_unowned_ref devuelve el valor, "o" sería un objeto sin propietario (por ejemplo no habría referencias hacia él). Si este fuera el caso, el objeto sería borrado y el método jamás devolvería una referencia válida.
- Si el valor de retorno no se define como unowned, la propiedad pasaría al nodo que ha realizado la llamada. El código de llamada está, sin embargo, esperando una referencia sin propietario (unowned), la cual no puede recibir la propiedad.
Si el código de llamada se escribe de la siguiente forma:
Object o = get_unowned_ref();
Vala intentará o bien obtener una referencia del objeto o bien una instancia duplicada a dónde apunta la referencia. En contraste a los métodos normales, las propiedades siempre devuelven valores sin propietario. Esto significa que no se puede devolver un objeto nuevo creado dentro de un método get de una propiedad. También significa que, no se puede usar una referencia con propietario como valor de retorno de una llamada a un método. Este hecho es debido a que el valor de la propiedad está asignado (es propiedad de) al objeto que tiene la propiedad. Debido a esto el siguiente código devolverá un error:
public Object property {
get {
return new Object(); // MAL: La propiedad devuelve una referencia sin dueño,
// el objeto nuevo será borrado cuando
// el ámbito del método 'get' finalice, finalizando el código que
// ha llamada al método 'get' obteniendo una referencia a un
// objeto borrado.
}
}
Tampoco estaría permitido realizar una cosa similar al siguiente código:
public string property {
get {
return getter_method(); // MAL: Por la misma razón que arriba.
}
}
public string getter_method() {
return "some text"; // "some text" se duplica y se devuelve en este punto del código.
}
Por el contrario, el siguiente código es perfectamente legal y compila sin ningún problema:
public string property {
get {
return getter_method(); // BIEN: El método 'getter_method' devuelve una referencia sin propietario
}
}
public unowned string getter_method() {
return "some text";
// No se preocupe por que el texto no sea asignado a alguna variable fuerte.
// las cadenas literales son propiedad del módulo del programa en Vala,
// y existen mientras el programa esté cargado en memoria.
}
El modificador unowned se puede usar para hacer el almacenamiento de las propiedades sin propietario. Es decir:
public unowned Object property { get; private set; }
Es idéntico al código:
private unowned Object _property;
public Object property {
get { return _property; }
}
La palabra reservada owned se puede usar para pedir que una propiedad devuelva específicamente una referencia al valor con propietario, por lo tanto, causando que el valor de la referencia se copie del lado del objeto. Piense dos veces antes de añadir la palabra reservada owned. ¿Es una propiedad o simplemente un método get_xxx? Puede que tenga problemas en el diseño. De todas formas, el código que sigue es correcto y compilará sin problemas:
public owned Object property { owned get { return new Object(); } }
Las referencias sin propietario juegan un papel similar a los punteros que serán descritos más adelante. Sin embargo, es más simple usar punteros, ya que pueden ser convertidos en referencias normales de forma simple. Sin embargo, no está aconsejado su uso en un programa a menos que el programador sepa lo que está haciendo.
Transferencia de la propiedad
editarLa palabra reservada owned se usa para realizar una transferencia de la propiedad de una referencia de las siguientes formas:
- Como un prefijo del tipo del parámetro, lo cual indica que la propiedad del objeto se transfiere dentro del contexto del código.
- Como un operador de conversión, se puede usar para evitar la duplicidad de clases sin conteo de referencia, lo cual es imposible en Vala. Por ejemplo:
Foo foo = (owned) bar;
Este código indica que bar seré inicializada a null y que foo heredará la propiedad del objeto al que bar apunta.
Listas de parámetros de longitud variable
editarExiste la posibilidad de usar las listas de argumentos de tamaño variable para los métodos en Vala, igual que en otros lenguajes de programación, como por ejemplo C. Se declaran mediante puntos suspensivos (...) en los parámetros del método. Un método que tiene una lista de parámetros de longitud variable debe tener al menos un parámetro fijo:
void method_with_varargs(int x, ...) {
var l = va_list();
string s = l.arg();
int i = l.arg();
stdout.printf("%s: %d\n", s, i);
}
En este ejemplo x es un argumento fijo para cumplir con los requisitos. La lista de argumentos se obtiene con el método va_list(). Después se puede ir obteniendo argumento tras argumento mediante el método arg(T) de la lista de argumentos, siendo T el tipo que debería ser el argumento. Si el tipo es evidente (como en el ejemplo anterior) se infiere automáticamente y se puede utilizar la llamada a arg() sin argumentos.
El siguiente ejemplo pasa un número indeterminado de cadenas de caracteres que se convierten en números reales (double):
void method_with_varargs(int fixed, ...) {
var l = va_list();
while (true) {
string? key = l.arg();
if (key == null) {
break; // fin de la lista y se sale del bucle infinito
}
double val = l.arg();
stdout.printf("%s: %g\n", key, val);
}
}
void main() {
method_with_varargs(42, "foo", 0.75, "bar", 0.25, "baz", 0.32);
}
En el ejemplo se comprueba que la cadena sea null para reconocer el final de la lista de parámetros. Vala pasa implícitamente el valor null como el último argumento de la lista que reciben estos métodos.
Este forma de recibir listas de parámetros de tamaño indeterminado tiene un inconveniente que el programador debe tener en cuenta. El compilador no puede decirle al programador cuando se están pasando argumentos del tipo correcto al método y cuando no. Por este motivo el programador debe considerar el uso de estas listas sólo si tiene una buena razón para hacerlo y es imprescindible, por ejemplo: suministrar una función conveniente para los programadores de C que usen una biblioteca desarrollada en Vala. A menudo un argumento del tipo vector es una elección más acertada y segura.
Un patrón común para el uso de las listas de parámetros es esperar parejas de argumentos del tipo propiedad y valor a menudo referidas a gobject. En este caso se puede escribir propiedad : valor, como en el siguiente ejemplo:
actor.animate (AnimationMode.EASE_OUT_BOUNCE, 3000, x: 100.0, y: 200.0, rotation_angle_z: 500.0, opacity: 0);
Este código de arriba sería equivalente al siguiente código fuente:
actor.animate (AnimationMode.EASE_OUT_BOUNCE, 3000, "x", 100.0, "y", 200.0, "rotation-angle-z", 500.0, "opacity", 0);
Punteros
editarLos punteros de Vala son una forma de permitir la gestión manual de la memoria en los programas. Por regla general, cuando el programador crea una instancia de un tipo se devuelve una referencia a ese objeto, y Vala se encarga de destruir la instancia cuando no haya más referencias a ese objeto, y por lo tanto no sea útil. Si se pide un puntero en lugar de una referencia, será el programador el encargado de destruir la instancia cuando ya no se necesite, y por lo tanto obtendrá un mayor control sobre la memoria que está usando el programa en desarrollo.
Esta funcionalidad no se necesita en la mayoría de las ocasiones, puesto que los ordenadores modernos son suficientemente rápidos para manejar las referencias (y el conteo de referencias) y tienen suficiente memoria que las pequeñas problemas de eficiencia que genera la gestión de memoria automática no son importantes. Sin embargo pueden darse algunas situaciones en las cuales será necesario usar la gestión manual de la memoria. Por ejemplo:
- En caso de que el programador quiera optimizar un programa específicamente en el uso de memoria.
- Cuando el programa trabaja con una biblioteca externa que no implementa el conteo de referencias para la gestión de memoria (probablemente por que no está basada en g-object).
Para crear una instancia de un tipo y recibir un puntero a ese nuevo objeto (en lugar de una referencia) se procede de la siguiente forma:
Object* o = new Object();
Para acceder a los métodos y los miembros en general del objeto se usa el operador ->:
o->method_1();
o->data_1;
Para liberar la memoria en la que se almacena el objeto (cuando éste ya no sea útil):
delete o;
Vala tiene soporte para los operadores de punteros que se usan en C, es decir, el operador dirección de (&) y el operador indirección (*). El primer operador obtiene la dirección de memoria de un objeto y el segundo obtiene el propio objeto a partir de un puntero (se usa para acceder al contenido del objeto):
int i = 42; // Un entero con valor 42
int* i_ptr = &i; // Obtenemos un puntero que apunta al entero i mediante el operador dirección de
int j = *i_ptr; // Mediante el operador indirección se obtiene el contenido del entero 'i'.
*i_ptr = 7; // Se modifica el valor del entero también mediante el operador indirección.
Cuando en lugar de valores primitivos (como en el caso anterior en el que se ha usado un entero) se usan referencia a objetos se pueden omitir los operadores, ya que, en este caso se están usando sobre referencias que apuntan a los objetos.
Foo f = new Foo();
Foo* f_ptr = f; // Se obtiene la dirección del objeto que apunta la referencia 'f'
Foo g = f_ptr; // Se obtiene el contenido del objeto y se apunta mediante una nueva referencia llamada 'g'
unowned Foo f_weak = f; // Esto es equivalente a la segunda línea de código
El uso de punteros es equivalente al uso de referencias sin propietario como se puede ver en el ejemplo de arriba en la última línea de código.
Clases que no heredan de GLib.Object
editarLas clases que no hereden en ningún nivel de GLib.Object son tratadas como un caso especial. Estas clases derivan directamente desde el sistema de tipos de GLib y por lo tanto son más ligeras (en uso de recursos).
Un caso obvio de este tipo de clases son algunos de los Binding a la biblioteca GLib. Puesto que la biblioteca GLib hace un trabajo a más bajo nivel que GObject, la mayoría de las clases que se definen en el binding son de este tipo. Como se ha comentado anteriormente, estas clases son más ligeras, lo cual las haces útiles en algunos casos (por ejemplo en el uso del propio compilador de Vala). Sin embargo, el uso de este tipo de clases no es muy habitual por lo que no se tratará en este documento.