Programación en Vala/Conceptos básicos del lenguaje
Conceptos básicos del lenguaje
editarArchivos de código y compilación
editarLos archivos de código fuente de vala tienen, normalmente, la extensión ".vala". El lenguaje de programación Vala no fuerza a que los proyectos tengan una determinada estructura, en cuanto a los paquetes o los nombres de los archivos que contiene una clase, como hacen otros lenguajes como Java. En lugar de eso la estructura se define dentro de los archivos de código mediante texto, definiendo la localización y estructura lógica mediante elementos como los espacios de nombres. Cuando se quiere compilar código Vala, se le pasa al compilador una lista de los archivos de código fuente necesarios, y el compilador determina como ensamblarlos todos juntos.
La ventaja de todo esto es que se pueden definir tantas clases o funciones como se desee dentro de un único archivo de código, incluso combinando distintos espacios de nombres todos dentro del mismo archivo (aunque no se demasiado recomendable por cuestiones de legibilidad y estructura). Vala por lo tanto no exige de manera inherente una determinada estructura para los proyectos que se desarrollan usando esta plataforma. Sin embargo, si existen ciertas convenciones a la hora de estructurar un proyecto desarrollado en Vala. Un buen ejemplo sería como se estructura el propio proyecto del compilador de Vala.
Todos los archivos de código que pertenezcan al mismo proyecto son suministrados al compilador "valac" mediante la línea de comandos, junto con los correspondientes parámetros de compilación. Esto funciona de forma similar a como se haría con el código fuente compilado en Java. Por ejemplo:
$ valac compiler.vala --pkg libvala
Esa línea de código produciría un archivo binario llamado "compiler" que sería enlazado con el paquete "libvala" (cuando nos referimos a paquete lo estamos haciendo a un concepto similar a biblioteca de funciones o la mal traducida librería). De hecho, así es como se genera el compilador de Vala en realidad.
Si se quiere que el archivo ejecutable producido tenga un nombre específico (distinto al que el compilador le da) o si se le pasan varios archivos al compilador, es posible especificar el nombre del fichero ejecutable mediante la opción "-o":
$ valac source1.vala source2.vala -o myprogram $ ./myprogram
Como se ha comentado anteriormente el compilador de vala es capaz de generar código en lenguaje C en lugar de un archivo ejecutable. Así para realizar esto existe la opción "-C" que indicará al compilador que genere todo el código C necesario para crear el ejecutable del proyecto. El compilador creará un archivo con extensión ".c" por cada archivo con extensión ".vala" que le pasemos. Si abrimos el contenido de esos archivos generados se puede ver el código C equivalente al programa que hemos desarrollado en Vala. Se puede observar como se crean las mismas clases que en Vala pero mediante la biblioteca GObject y cómo se registran de forma dinámica en el sistema. Este es un ejemplo del poder que tiene la plataforma de desarrollo de GNOME. Sin embargo, todo esto es a título informativo, ya que, por suerte no es necesario generar todos estos ficheros, ni entender como funcionan internamente para poder programar en Vala. Si en lugar de generar el fichero de código C sólo necesitamos generar la cabecera (por ejemplo si estamos generando una biblioteca de funciones será necesario para poder usarla en C o en otro lenguaje) existe la opción "-H" con la que conseguiremos ese objetivo. A continuación un ejemplo que genera los dos ficheros (un fichero de código C y su correspondiente cabecera ".h"):
$ valac hello.vala -C -H hello.h
Visión general de la sintaxis
editarLa sintaxis de Vala está fuertemente inspirada en la sintaxis del lenguaje de programación C#, por lo que la mayoría de lo explicado aquí será muy familiar para los programadores de C# e incluso para los que sepan algún lenguaje con un sintaxis similar a C. Independientemente de eso se intentará dar explicaciones breves de los conceptos que se estimen oportunos, aunque no en demasiada profundidad por que no es el cometido del presente documento.
Al igual que en otros lenguaje de programación, en Vala existe el concepto de visibilidad o ámbito de las variables. Un objeto o referencia únicamente es válida dentro del ámbito definido mediante corchetes ({}) dónde se definió dicho objeto. Estos delimitadores son los mismos que se usan para delimitar las definiciones de las clases, los métodos, los bloques de códigos, etc; por lo que todos estos conceptos tienen su propio ámbito. Por ejemplo:
int funcion1 (void) {
int a=0;
}
int funcion2 (void) {
int a=1;
stdout.printf("%d\n", a);
}
El código anterior define dos funciones que tiene sendas variables "a" definidas con distintos valores. La función "funcion2" mostrará el valor de "a", el cúal será de 1 ya que la variable que se encuentra definida en el ámbito de la función "funcion2" vale 1.
El lenguaje Vala, a diferencia de otros, no es estricto en la localización de la definición de variables. Así no existe una localización fija para este propósito, si bien es cierto que se aconseja por temas de legibilidad y estructura definirlas al inicio del ámbito en el que van a ser usadas.
Una variable se define mediante un tipo y un nombre después del tipo. Es decir si queremos definir un entero de nombre edad sería algo así:
int edad;
En caso de que el tipo fuera una clase (en lugar de un tipo de datos básico) se crea una instancia de una clase. Si se crea una referencia que no se inicia esta no apunta a ningún objeto. El concepto de referencia aquí es el mismo que en el lenguaje de programación Java, es decir, un puntero que apunta a un objeto. Así para definir un nuevo objeto de una clase se usará el operador new:
// Referencia no inicializada
Clase objeto;
// Referencia inicializada que apunta al nuevo objeto
Clase objeto = new Clase();
Comentarios
editarLos comentarios se definen mediante los mismos símbolos que en C#, es decir, "//" para los comentarios de 1 línea y "/**/" para los comentarios de más líneas. Así tenemos:
// Comentario de una línea
/*
Comentario escrito en
más de una línea
*/
/*
* Comentario especial para sistema de documentación
* automática.
*/
En relación al último tipo de comentarios, el lenguaje de programación Vala dispone de un sistema generador de documentación automática propio cuyo nombre es Valadoc.
Tipos de datos
editarHablando en general podemos separar los tipos de datos en Vala en dos grandes subgrupos, los tipos de referencia y los de valor. Así estos nombres nos indican la forma en la que estos valores son pasados en el sistema. De esta forma un tipo de valor es copiado en todos los lugares en los que es asignado a otro identificador (otra variable), mientras que un valor de referencia se copia la referencia pero apunta al mismo objeto.
Constantes
editarLas constantes no son propiamente un tipo de dato ya que una constante define un valor de cualquier tipo de dato que no se modifica durante la ejecución del programa. Así una constante se define mediante la palabra reservada "const" seguida del tipo de constante que se define. Así una constante de tipo real se definiría mediante:
const float PI = 3.1415926;
Tipos básicos
editarVala dispone de un conjunto amplio de datos básicos entre los que se encuentran los siguientes:
- Byte (char, uchar): Es un valor que ocupa el mínimo posible en memoria (1 byte) y que se utiliza para almacenar 1 carácter o valores enteros de 0 a 255.
- Character (unichar): Es un carácter de tipo unicode por lo que ocupará en memoria 2 bytes.
- Integer (int, uint): Enteros positivos que van desde 0 a 65536 (o desde -32768 a 32767 si es con signo), ocupa 2 bytes en memoria.
- Long Integer (long, ulong): Es un entero largo que ocupa 4 bytes en memoria y que puede representar desde 0 a 4294967296.
- Short Integer (short, ushort): Es un entero que ocupa 2 bytes en memoria y que funciona como un Integer.
- Enteros de tamaño fijo garantizado: Estos tipos garantizan que independientemente de la plataforma de ejecución del programa el entero ocupa el mismo tamaño en memoria y tiene por lo tanto los mismos límites. Estos tipos de datos son: int8, int16, int32 e int64 (para los enteros con signo) y uint8, uint16, uint32 y uint64 (para los enteros sin signo).
- Números de coma flotante: Hay dos tipos de datos de coma flotante, float y double. Los números float ocupan en memoria 4 bytes y los double 8 bytes. La principal diferencia es la precisión del número. Un número double permite representar números con más precisión que un float.
- Boolean (bool): Tipo de dato booleano con dos valores posibles cierto (true) y falso (false).
- Datos compuestos (struct): Permite definir datos compuestos mediante la palabra reservada struct. Ejemplo: struct persona { int edad, String nombre };
- Enumeraciones (enum): Conjunto de valores enteros enumerados, es decir, salvo que no se indique otra cosa se incrementan los valores.
Normalmente los valores que se especifican ocupan lo establecida en la lista, sin embargo sólo se garantiza para los enteros de tamaño fijo garantizado. Para determinar el tamaño en memoria de una variable se usa la palabra reservada "sizeof". Se puede ver el valor máximo y mínimo que pueden tener un tipo de dato se usan los valores MIN y MAX definidos. Por ejemplo int.MIN y int.MAX.
Cadenas
editarLas cadenas de texto en Vala se definen mediante la palabra reservada string. Estas cadenas son de tipo UTF-8, es decir, son cadenas de texto unicode que pueden representar cualquier texto.
string a = "Hola";
Además de este tipo de cadenas existen las cadenas literales, es decir, que no se interpretan los caracteres de escape como "\n". Estas cadenas se definen mediante una triple comillas. Cualquier carácter que se encuentre entre el par de tres comillas se inserta dentro de la cadena literal, por ejemplo un tabulador. Ejemplo:
string literal = """Esto es una cadena "literal" y puede contener cualquier carácter.\n\tTexto tabulado.\n""";
Las cadenas que empiezan mediante una @ son plantillas. Estas plantillas pueden evalúan variables y/o expresiones que están definidas mediante $. Por ejemplo:
string plantilla = @"$a * $b = $(a*b)"; // Devolverá una cadena como "6 * 7 = 42"
Los operadores == y != se utilizan en las cadenas y su comportamiento es distinto a otros lenguajes. Así en Vala los operadores == y != compara el contenido de las cadenas y no la referencias de las mismas. Por ejemplo en Java:
Código Java
String a = "hola";
String b = "hola";
if (a == b) {
} else {
}
Código Vala
string a = "hola";
string b = "hola";
if (a == b) {
} else {
}
El código Java indicará que las variables apuntes a dos objetos distintos (dos referencias distintas) mientras que en Vala si se devolverá el valor de cierto ya que ambas cadenas son iguales, aunque se encuentren almacenadas en dos variables distintas.
Al igual que en otros lenguajes de programación como Python, las cadenas se pueden partir en partes mediante los operadores [inicio:fin]. Así si queremos seleccionar desde el carácter 5 hasta el 9 podemos hacer lo siguiente:
string a = "hola mundo";
string b = a[5:9];
Se puede acceder a un determinado carácter mediante los corchetes indicando el índice del carácter, teniendo en cuenta que los vectores en Vala empiezan en el índice 0.
string a = "hola mundo";
unichar c = a[0];
Sin embargo esta forma de acceso a las cadenas es de sólo lectura, es decir, no se puedes escribir un carácter (o una cadena) indicando la posición en la que debe ser insertado. Por ejemplo el siguiente código es inválido:
Código INVALIDO en Vala
string a = "hola mundo";
a[0] = "a";
Existen, asimismo, diversos métodos para poder realizar conversiones entre cadenas y otros tipos de datos básicos y viceversa. A continuación se presentan algunos ejemplos:
Ejemplos de conversiones entre cadenas y otros datos básicos y viceversa
bool b = "false".to_bool(); // => false
int i = "-52".to_int(); // => -52
double d = "6.67428E-11".to_double(); // => 6.67428E-11
string s1 = true.to_string(); // => "true"
string s2 = 21.to_string(); // => "21"
string s3 = 24.17.to_string(); // => "24.170000000000002"
Vectores
editarLos vectores se declaran en Vala mediante el tipo de vector (el tipo de dato) seguido de "[]" y un nombre al final. Antes de usar el vector además de declararlo lo debemos inicializar indicando el tamaño del mismo o en su caso los valores que lo componen. Por ejemplo si queremos declarar un vector de 100 números enteros llamado "lista_primos" sería algo así:
int[] lista_primos = new int[100];
// Para saber el tamaño de un vector se usa la propiedad "length"
stdout.printf("%d\n", lista_primos.length);
Al igual que con las cadenas, los vectores también pueden ser divididos en varias partes usando los operadores "[]". Así teniendo definido un vector de enteros tal que "{ 2, 4, 6, 8 }" podemos trocearlo de la siguiente forma:
int[] lista = { 2, 4, 6, 8 };
int[] c = lista[1:3]; // => { 4, 6 }
El nuevo vector es un vector completamente independiente del original y los cambios realizados sobre el segundo no afectan al primero.
Además de vectores se pueden definir matrices multidimensionales posicionando una coma (o más dependiendo del número de dimensiones que se quiera) dentro de los corchetes. Así por ejemplo para definir una matriz bidimensional vacía de 3x3 se realizaría de la siguiente forma:
int[,] c = new int[3,3];
// Si se quiere una matriz con los valores sólo tenemos que indicarlos después del =
int[,] d = {{2, 4, 6},
{3, 5, 7},
{1, 3, 5}};
Este tipo de matrices se representa internamente en memoria como un bloque contiguo. Los vectores de vectores "[][]" en los cuales cada fila tiene un tamaño distinto no están soportados aún.
Se pueden añadir elementos al vector mediante el uso del operado "+=". Sin embargo, esto sólo funciona para vectores definidos como locales o privados. El vector será redimensionado en caso de ser necesario. En caso de que esto pase, internamente se incrementa en potencias de 2 por temas de eficiencia y velocidad. Sin embargo la propiedad ".length" indicará el número actual de elementos y no el valor interno. Por ejemplo:
int[] e = {}
e += 12; // Ahora e tiene un tamaño interno de 2 (Aunque la propiedad length vale 1)
e += 5; // Ahora e tiene un tamaño interno de 2 (La propiedad length vale 2)
e += 37; // Se añade otro elemento y se vuelve a redimensionar internamente. Ahora su tamaño interno sera de 4 elementos (La propiedad length valdrá 3)
Si después de los paréntesis después del identificador junto con un tamaño se obtendrá un vector de tamaño fijo. Este tipo de vectores se aloja en memoria en la pila o mediante "in-line allocated" (si se usa como variable de una clase y no puede ser redimensionado después. Ejemplo:
int e[10]; // No necesita new. No se le pueden añadir más elementos.
Vala no realiza comprobaciones de límites en el acceso a los vectores en tiempo de ejecución. Si se necesita más seguridad sobre esta temática se recomienda el uso de estructuras de datos más sofisticadas como por ejemplo los "ArrayList". Más adelante se tratarán más en profundidad esta familia de estructuras de datos (colecciones, lista, etc). Así por ejemplo en el siguiente ejemplo se accede a una posición inexistente del vector; en lugar de mostrarnos un error mostrará el contenido de una posición de memoria externa al vector y de contenido indeterminado (basura):
int[] vector = new int[10];
stdout.printf("%d\n", vector[300]); // Puede mostrar cualquier valor, pero no mostrará errores
Referencias
editarEl tipo de dato conocido como referencia es un tipo de dato que contiene un valor que permite el acceso indirecto al contenido de una variable. El termino referencia es muy similar al termino puntero que se usa en otros lenguajes de programación como C, sin embargo en este caso una referencia normalmente se usará para los objetos creados, mientras que un puntero puede ser de cualquier tipo de variable (entero, coma flotante, cadena, etc). Así cada vez que dentro de un programa escrito en Vala pasemos un objeto a una función o método de otra clase, en realidad estaríamos pasando una referencia. El sistema es capaz de tener un registro de cuantas referencias siguen en uso, con el fin de que pueda realizar la gestión de memoria por nosotros. Entre otras cosas el sistema de gestión de memoria de Vala se encarga de liberar la memoria que ocupa un objeto cuando todas las referencias que apuntan a él hayan dejado de usarse. El valor de una referencia que no apunte a ninguna parte será null. Más información acerca de las referencias en el capítulo de Programación orientada a objetos en Vala.
Conversión estática de tipos
editarCuando nos encontramos desarrollando un programa en cualquier lenguaje de programación fuertemente tipado, normalmente nos encontramos ante el problema de pasar de un tipo de datos a otro con bastante frecuencia. En una conversión de datos estática el compilador sabe cuales son los tipos de datos origen y destino antes de ejecutar el programa compilado. Así en Vala se puede realizar este tipo de conversión de datos estableciendo el tipo de datos entre paréntesis después de una igualdad. En una conversión estática de datos no se establece ningún tipo de comprobación de seguridad. Este tipo de conversiones son válidas para todos los tipos de datos en Vala. Por ejemplo:
int i = 10;
float j = (float) i;
Conversión dinámica de tipos (Inferencia)
editarExiste otro tipo de conversión de tipos de datos conocida como conversión dinámica o inferencia de tipos que se usa sobre todo en lenguajes de programación funcionales. Así dado que Vala permite definir variables sin tipo inicial usando para ello la palabra reservada var, es necesario que se pueda establecer el tipo de una variable con posterioridad a su definición, es decir, inferir ese tipo a partir de una expresión que se encuentra a la derecha de una igualdad. Por ejemplo el siguiente código sería legal en Vala:
var p = new Person(); // es lo mismo que: Person p = new Person();
var s = "hello"; // es lo mismo que: string s = "hello";
var l = new List<int>(); // es lo mismo que: List<int> l = new List<int>();
var i = 10; // es lo mismo que: int i = 10;
Este tipo de variables sin tipo sólo están permitidas como variables locales. Este mecanismo es muy útil para usar en los parámetros de los métodos genéricos.
Operadores
editarComo otros lenguajes de programación, Vala tiene a disposición de los programadores una gran variedad de operadores para usar con distintos propósitos. Podemos separar los operadores de Vala en varios tipos según el tipo de datos sobre el que operan.
Operadores aritméticos
editarEstos operadores se aplican a todos los tipos numéricos definidos anteriormente. Estos operadores son los que aparecen en la siguiente tabla:
Operador | Descripción |
= | El operador de asignación se utiliza para realizar asignaciones entre una variable y el resultado de una expresión o un valor (otra variable). A la izquierda de este operador debe aparecer un identificador (una variable) y a la derecha del mismo puede aparecer un valor, una expresión o otra variable. |
+ | El operador de suma realiza la adición de las expresiones que tiene a su izquierda y a su derecha. Este operador es aplicable a las cadenas de texto ("string"). |
- | El operador de resta realiza la resta de la expresión que tiene a la derecha sobre la expresión que tiene a su izquierda. |
* | El operador de multiplicación realiza el producto entre la expresión que tiene a su izquierda y a su derecha. |
/ | El operador de división realiza esta operación usando como dividendo la expresión que tiene a su izquierda y como divisor la expresión que tiene a su derecha. |
% | El operador de módulo calcula el resto de una división siendo la expresión que tiene a su izquierda el cociente y la expresión que existe a su derecha el divisor. |
Operador | Descripción |
+= | Este operador realiza la adición de la expresión sobre el contenido que tenga el identificador a su izquierda. |
-= | Este operador realiza la resta de la expresión sobre el contenido que tenga el identificador a su izquierda. |
*= | Este operador realiza el producto entre el contenido del identificador a su izquierda y la expresión a su derecha. El resultado se almacena en el identificador. |
/= | Este operador realiza la división tomando como dividendo el contenido del identificador y como divisor el resultado de la expresión de la derecha. El resultado de la división se almacena en el identificador. |
%= | Este operador calcula el módulo (resto) de la división, tomando como dividendo el contenido del identificador y como divisor el resultado de la expresión de la derecha. El resultado de la división se almacena en el identificador. |
Operador | Descripción |
++ | Este operador incrementa en uno el contenido del identificador. |
-- | Este operador decrementa en uno el contenido del identificador. |
Los operadores "++" y "--" se pueden usar tanto en una posición prefija al identificador como postfija al mismo. Sin embargo existe una sutil diferencia entre ambos usos. Supongamos que tenemos este operador en una expresión (variable++ o ++variable). Así, si el operador está delante del identificador la expresión devolverá el nuevo valor (variable + 1) sin embargo, si tenemos el operador detrás del identificador entonces la expresión devolverá el valor antiguo (variable). Esto puede hacer que cometamos errores difíciles de detectar. Por ejemplo:
Código con operador prefijo
int a = 0;
int b = ++a; // Aquí b valdría 1 que sería el valor que devuelve la expresión
Código con operador postfijo
int a = 0;
int b = a++; // Aquí b valdría 0 que sería el valor que devuelve la expresión
Operadores a nivel de bits
editarUn operador a nivel de bits es aquel que realiza una operación sobre los bits que componen los parámetros que recibe. Los operadores booleanos que se definen en Vala son los mismos que se definen en el álgebra de Boole o compuestos por varios básicos. Así tendremos los siguientes operadores:
Operador | Descripción |
| | Este operador realiza la operación booleana OR de la expresión que tiene a su izquierda y la expresión que tiene a su derecha. |
^ | Este operador realiza la operación booleana XOR de la expresión que tiene a su izquierda y la expresión que tiene a su derecha. |
& | Este operador realiza la operación booleana AND de la expresión que tiene a su izquierda y la expresión que tiene a su derecha. |
Operador | Descripción |
~ | Este operador realiza la operación booleana NOT de la expresión que tiene a su derecha. |
Operador | Descripción |
|= | Este operador realiza la operación booleana OR entre el identificador y el resultado de la expresión booleana. El resultado se almacena dentro del identificador. |
^= | Este operador realiza la operación booleana XOR entre el identificador y el resultado de la expresión booleana. El resultado se almacena dentro del identificador. |
&= | Este operador realiza la operación booleana AND entre el identificador y el resultado de la expresión booleana. El resultado se almacena dentro del identificador. |
Operador | Descripción |
>> | Este operador realiza el movimiento de los bits de izquierda a derecha un número de veces igual al valor devuelto por la expresión entera. Se introducirán tantos 0 por la izquierda como indique la expresión entera. |
<< | Este operador realiza el movimiento de los bits de derecha a izquierda un número de veces igual al valor devuelto por la expresión entera. Se introducirán tantos 0 por la derecha como indique la expresión entera. |
Operador | Descripción |
>>= | Este operador realiza el movimiento de los bits de izquierda a derecha un número de veces igual al valor devuelto por la expresión entera. Se introducirán tantos 0 por la izquierda como indique la expresión entera. El resultado se almacena en el identificador. |
<<= | Este operador realiza el movimiento de los bits de derecha a izquierda un número de veces igual al valor devuelto por la expresión entera. Se introducirán tantos 0 por la derecha como indique la expresión entera. El resultado se almacena en el identificador. |
Operadores lógicos
editarLos operadores lógicos son aquellos que toman unos operandos y realizan alguna operación de tipo lógico sobre ellos. Estos operadores se utilizan para comprobar si se satisface una condición todos ellos devuelven un valor booleano (true o false) que determina si esa condición es cierta o no.
Operador | Descripción |
! | El operador lógico NOT hace que la condición tome el valor contrario al que tiene. Por ejemplo si una expresión lógica devuelve true y aplicamos el operador NOT la expresión se convierte en false. |
&& | El operador lógico AND comprueba dos expresiones lógicas sean ciertas. En ese caso el resultado será true, en cualquier otro caso será false. |
|| | El operador lógico OR comprueba al menos una de las expresiones lógicas sea cierta. En ese caso el resultado será true, en caso de ambas expresiones sean falsas el operador devolverá false. |
expresion_evaluacion ? expresion_A : expresion_B | El operador ternario evalúa una expresión de evaluación y comprueba que sea cierta. En ese caso devuelve como resultado el valor de la expresión A, en caso contrario devuelve el contenido de la expresión B. |
expresion_evaluacion ?? expresion | Este operador es equivalente a escribir la expresión "a != null ? a : b. Es decir, si la expresión es distinto de null devuelve el valor de la expresión, en caso contrario devuelve el valor de expresión. Este operador es útil para suministrar un valor por defecto cuando una referencia en null.
Ejemplo: stdout.printf("Hola, %s!\n", nombre ?? "Desconocido");
|
expresion_A in expresion_B | El operador in comprueba si la expresión B se encuentra dentro de la expresión A. Este operador se utiliza sobre conjuntos o listas de elementos, así si un conjunto de elementos se encuentra dentro de otro entonces el operador devolverá true; en caso contrario devuelve false. Este operador es válido para las cadenas, realizando una búsqueda de una cadena B dentro de la cadena A.
Ejemplo: bool a = "Hola" in "Hola mundo"; // La variable a tendrá el valor true
|
Estructuras de control
editarLas estructuras de control son las que nos permiten estructurar el programa y definir el flujo de trabajo necesario para conseguir el resultado esperado. Así todos los lenguajes de programación cuentan con más o menos estructuras de control. El lenguaje Vala por su parte dispone de un amplio número de estructuras de control a disposición de los programadores.
Bucles
editarLos bucles son estructuras de control cuya utilidad es la de repetir un código un número de veces. Dependiendo de si ese número de veces es conocido de antemano o no tenemos dos tipos de bucles.
En el primer grupo de bucles conocemos el número de repeticiones que va a realizar el bucle antes de que se ejecute el programa. Dentro de este grupo Vala dispone de dos estructuras de control. La primera es la que se define mediante la palabra reservada for. Este bucle tiene en su definición tres partes que distribuyen así:
for ( inicialización_contador; condicion_salida; codigo_contador ) {
// Código que se repite dentro del bucle
...
}
La parte de inicializacion_contador se utiliza para definir la variable que tendrá el valor del contador en cada pasada del bucle. Después tendremos la parte de condición_salida que debe contener una expresión booleana que cuando devuelva que es cierta saldrá del bucle. Por último se define la parte del codigo_contador dónde se define el incremento del paso de cada iteración, normalmente será un incremento (de una unidad) sobre el contenido de una variable. Veamos un ejemplo:
for ( int i = 0; i < 10; i++ ) {
// Este bucle se ejecutará 10 veces yendo los valores del contador "i" desde 0 a 9, siendo el paso 1
stdout.printf("Iteración %d\n", i);
}
El otro tipo de bucle es el que se define mediante la palabra reservada foreach. Este bloque se define en dos partes como las que aparecen en el siguiente ejemplo:
foreach ( elemento in lista_elementos ) {
// Se ejecuta tantas veces como elementos haya en la lista.
}
Así en la primera parte se define una variable del mismo tipo que los objetos que forma la lista, mientras que en la segunda parte se especifica la lista de objeto a recorrer. Por ejemplo:
int[] lista_enteros = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
foreach( int entero in lista_enteros ) {
// Mostramos el contenido
stdout.printf("Contenido del elemento: %d\n", entero);
}
Este código recorre un vector de enteros y va almacenando el contenido de cada elemento del vector en la variable entero definida sólo para ese ámbito. Hay que señalar que aunque cambiemos el contenido de la variable entero no se almacenará en el vector lista_enteros el nuevo valor, ya que, se hace una copia de los valores.
El segundo grupo de bucles que se ha definido al principio de esta sección es aquel en el que no conocemos a priori el número de ejecuciones que va a tener. Dentro de este grupo Vala define dos estructuras de control distintas. La primera de ellas es el bucle mientras o while. Este tipo de bucle se ejecutará mientras la expresión booleana que se especifica al inicio del bucle sea cierta. Así el bucle tiene la siguiente estructura:
bool condicion_salida = true;
while ( condicion_salida ) {
// Acciones que hacen que llegado el momento "condicion_salida" tenga un valor false y se salga del bucle
}
Este bucle evalúa la condición de salida antes de ejecutar las acciones que tiene definidas dentro del cuerpo del mismo. Así este bucle puede llegar a no ejecutarse ninguna vez.
El otro bucle definido en Vala es el conocido como bucle "Hacer mientras que" o "Do ... While". Este bucle realiza al menos una ejecución de las sentencias que tiene definidas dentro su cuerpo. Después llega a la parte de evaluación de la condición, ejecutándose este proceso mientras la condición que evalúa sea cierta. Ejemplo de definición:
bool condicion_salida = true;
do {
// Acciones que hacen que la condicion evaluada sea falsa
} while ( condicion );
Todos estos bucle puede ser manipulados haciendo uso de las palabras reservadas break y continue. La palabra reservada break se utiliza para salir incondicionalmente del cuerpo del bucle (independientemente de que la condición sea cierta o no). Por su parte, la palabra reservada continue hace que el flujo del programa vaya al lugar donde se realiza la evaluación de la condición de salida del bucle. Ejemplo:
bool condicion_salida = true;
do {
// Nos salimos del bucle
break;
// Operaciones que no llegaran ejecutarse
} while ( condicion );
// Este bucle se ejecutaría siempre
while( condicion ) {
// No hacemos nada y nos vamos a la evaluación
continue;
// Acciones que cambiarían el valor de la condición a false
}
Estructuras de control condicionales
editarLa estructuras de control condicionales son aquellas que permiten ejecutar un trozo de código dependiendo de si una condición es cierta o no. Dentro de este tipo de estructuras de control Vala dispone de dos tipos que están a disposición del programador para su uso.
La primera de ellas es la estructura que se define con las palabras reservadas if ... else if ... else. Esta estructura de control permite definir una condición que de cumplirse ejecuta el trozo de código que tiene encerrado entre paréntesis. La definición sería algo así:
int a = 1;
if ( a == 1 ) {
// Acciones para cuando la variable a valga 1
} else {
// Acciones para cuando la variable tenga un valor distinto a 1
}
Adicionalmente esta estructura permite definir más condiciones distintas a la primera mediante las palabras reservadas else if. Así si ampliamos el ejemplo anterior tendríamos algo así:
int a = 1;
if ( a == 1 ) {
// Acciones para cuando la variable a valga 1
} else if ( a == 2 ) {
// Acciones para cuando la variable a valga 2
} else if ( a == 3 ) {
// Acciones para cuando la variable a valga 3
} else {
// Acciones para cuando la variable tenga un valor distinto a 1, 2 o 3
}
Además de esta estructura de control condicional, Vala dispone de otra estructura que es muy útil cuando tenemos un gran número de casos a tratar sobre una misma variable. Esta estructura de control es la llamada switch case. Esta estructura de control define una variable sobre la que se van a realizar varios casos mediante la palabra switch. El código a aplicar se encuentra definido mediante la palabra reservada case. Un ejemplo variando el anterior sería:
int a = 1;
switch (a) {
case 1:
// Acciones para cuando la variable a valga 1
break;
case 2:
// Acciones para cuando la variable a valga 2
break;
case 3:
// Acciones para cuando la variable a valga 3
break;
default:
// Acciones para cuando la variable tenga un valor distinto a 1, 2 o 3
break;
}
Esta definición es análoga a la realizada mediante if ... else if ... else teniendo en cuenta que todos los casos no contemplados y definidos mediante la palabra reservada case se definen mediante la palabra reservada default. Es muy importante no olvidar poner la palabra reservada break al final del bloque de código de cada caso, ya que en caso contrario, no se sale de la estructura de control condicional.
Una nota para programadores de C y otros lenguajes similares: las condiciones deben devolver un valor booleano explícitamente, es decir, si queremos evaluar que una variable sea null o tenga un valor de 0 hay que especificarlo explícitamente. Por ejemplo:
if ( objeto == null ) // BIEN
if ( objeto ) // MAL
Elementos del lenguaje
editarMétodos
editarEn Vala llamaremos métodos a todas las funciones indepedendientemente de si son independientes o se encuentran definidas dentro de una clase de objetos, tampoco se tendrá en cuenta si devuelven un valor o no. A partir de ahora a todas ellas nos referiremos con el nombre de métodos. Ejemplo de definición de un método:
int nombre_del_metodo ( int argumento1, Object argumento2 ) {
// Algoritmo a ejecutar
return valor_devuelto;
}
El código de arriba define un método con el nombre nombre_del_metodo que toma dos argumentos y devuelve un entero. Dentro del método se colocarán las acciones que el método debe ejecutar para obtener la funcionalidad deseada.
Como se ha comentado ya en varias ocasiones todo el código Vala se traduce a código C, por lo que todos los métodos Vala se traducen en funciones escritas en C, por lo que pueden recibir un número arbitrario de argumentos y devuelve un valor (o ninguno si se definen como void). Se podrían devolver más de un valor poniendo esos valores extra en un lugar conocido por el código que llama al método. Más información acerca de esta técnica se darán en la sección Funcionalidades avanzadas del lenguaje.
La convención para la nomenclatura de los métodos en Vala es mediante palabras en minúscula y separando las palabras mediante guiones bajos "_". Esto puede resultar algo extraño para los programadores que sepan C# o Java y estén acostumbrados a usar CamelCase o CamelCasemixto. Pero se sigue esta nomenclatura para ser consistente con otras bibliotecas ya desarrolladas en Vala y C/GObject que ya usan dicha nomenclatura.
A diferencia de otros lenguajes de programación, Vala no soporta la sobrecarga de métodos por lo que no es posible definir dos o más funciones con el mismo nombre y distinto número y/o tipos de argumentos. Ejemplo de sobrecarga:
void draw(string text) { }
void draw(Shape shape) { } // Esta definición nos dará un error.
Esto es debido a que las bibliotecas desarrolladas en Vala están diseñadas para que puedan ser usadas por programadores de C también sin ningún cambio (y en C no existe la sobrecarga de funciones). En lenguaje Vala se puede realizar algo parecido a lo siguiente para solucionar este inconveniente:
void draw_text(string text) { }
void draw_shape(Shape shape) { }
Eligiendo nombres ligeramente distintos el desarrollador puede evitar este inconveniente. En lenguajes que si soportan la sobrecarga de métodos se usa esta técnica para suministrar métodos con menos parámetros que un método general.
En caso de querer desarrollar métodos generales se puede usar una característica de Vala que son los argumentos por defecto de los métodos para obtener un comportamiento similar. De esta forma no es necesario pasar todos los parámetros a los métodos que han sido definidos de esta forma:
void metodo(int x, string s = "hola", double z = 0.5) { }
Este método definido podría ser llamado de alguna de las siguientes formas:
metodo(2);
metodo(2, "¿que tal?");
metodo(2, "¿que tal?", 0.75);
Es posible incluso definir métodos con un número de parámetros indefinidos y variable usando varargs como el método stdout.printf. Esta técnica se explicará más adelante.
Vala realiza una comprobación básica para comprobar que los parámetros (y el valor de vuelta) son nulos o no. Los parámetros (o valor de vuelta) que se definen con el símbolo ? postfijo al nombre de la variable se les permite que sean nulos (null). Esta información ayuda al compilador a realizar comprobaciones estáticas y a añadir comprobaciones en tiempo de ejecución en las precondiciones de los métodos, los cuáles pueden ayudar a anular errores relacionados con las referencias nulas.
string? method_name(string? text, Foo? foo, Bar bar) {
// ...
}
En este ejemplo, foo y el valor de vuelta puede ser null, sin embargo, bar no debe ser null.
Métodos delegados
editarLos métodos delegados, permiten pasar trozos de código entre objetos y otros métodos. Así por ejemplo se podría definir un método delegado para pasar a otro método de la siguiente forma:
delegate void DelegateType(int a);
void f1(int a) {
stdout.printf("%d\n", a);
}
void f2(DelegateType d, int a) {
d(a); // LLamada a un método delegado
}
void main() {
f2(f1, 5); // Se pasa un método como un parámetro delegado
}
El ejemplo de arriba define un nuevo tipo llamada DelegateType el cual, representa un método que recibe un entero y no devuelve ningún valor. Cualquier método que tenga este número de parámetros puede ser asignado a una variable de este tipo o pasado como un argumento de este tipo. El código ejecutará el método f2, pasado como una referencia al método f1 y el número 5. El método f2 ejecutará el método f1, pasándole a él el número.
Los método delegados puede ser creados de forma local. Un método miembro puede ser asignado también como método delegado. Por ejemplo:
class Foo {
public void f1(int a) {
stdout.printf("a = %d\n", a);
}
delegate void DelegateType(int a);
public static int main(string[] args) {
Foo foo = new Foo();
DelegateType d1 = foo.f1;
d1(10);
return 0;
}
}
En este ejemplo dentro de la clase Foo se define un método llamado f1 y un tipo delegado. Dentro del método main se define un método delegado y se asigna el método f1 del objeto foo.
Métodos anónimos (Clausura/Closure)
editarUn método anónimo, también conocido como una expresión lambda, función literal o clausura, puede ser definido usando el operador =>. La lista de parámetros se encuentran definidos en la parte de la izquierda del operador, mientras que el cuerpo del método se define a la derecha del operador.
Un método anónimo por si sólo no tiene sentido. Sólo es útil si se asignan directamente a una variable o a un tipo delegado o se pasa como argumento a otro método. Hay que darse cuenta de que ni los parámetros ni los tipos devueltos se dan explícitamente. En lugar de eso los tipos son inferidos de los parámetros que se usan con el tipo delegado. Ejemplo de asignación del método a una variable de tipo delegado:
delegate void PrintIntFunc(int a);
void main() {
PrintIntFunc p1 = (a) => { stdout.printf("%d\n", a); };
p1(10);
// Las llaves {} son opcionales si el cuerpo del método anónimo es de una línea
PrintIntFunc p2 = (a) => stdout.printf("%d\n", a);
p2(20):
}
Ejemplo de como se pasa un método anónimo a otro método
delegate int Comparator(int a, int b);
void my_sorting_algorithm(int[] data, Comparator compare) {
// ... el método 'compare' es llamado en alguna parte de este método ...
}
void main() {
int[] data = { 3, 9, 2, 7, 5 };
// Un método anónimo se pasa como segundo parámetro:
my_sorting_algorithm(data, (a, b) => {
if (a < b) return -1;
if (a > b) return 1;
return 0;
});
}
Los métodos anónimos son auténticas clausuras. Esto significa que pueden acceder a las variables locales del método exterior dentro de la expresión lambda. Ejemplo:
delegate int IntOperation(int i);
IntOperation curried_add(int a) {
return (b) => a + b; // 'a' es una variable externa
}
void main() {
stdout.printf("2 + 4 = %d\n", curried_add(2)(4));
}
En este ejemplo el método curried_add (ver Currificación) devuelve un método nuevo creado que mantiene el valor de a. Este método devuelto se llama con 4 como argumento, resultado de la suma de los dos números.
Espacios de nombres
editarEn programación orientada a objetos un espacio de nombres se utiliza para agrupar un conjunto de funcionalidad (clases, constantes, métodos, etc) que deben tener un nombre único. Así un espacio de nombres nos va a servir para desarrollar bibliotecas o módulos.
Para definir un espacio de nombres se utiliza la palabra reservada namespace seguida del identificador único que dará nombre al espacio de nombres. Así un ejemplo de espacio de nombres sería:
namespace EspacioNombres {
// Las clases, métodos, constantes que pertenecen al espacio de nombres
}
Todo lo que se defina dentro de los corchetes pertenecerá la espacio de nombres. Todo aquello definido fuera del espacio de nombres debe usar nombres cualificados (EspacioNombres.clase para definir un objeto de una clase por ejemplo), o por otra parte si se encuentra dentro de un archivo que contenga la línea con using EspacioNombres; podrá usar todos los identificadores dentro de ese espacio de nombres sin necesidad de anteponer el nombre del espacio de nombres delante de aquello a lo que quiera acceder. Por ejemplo:
Código con using
// Usamos el espacio de nombres GLib
using GLib;
o = Object;
Código sin using
o = GLib.Object;
Hay una ocasión en la cual es inevitable el uso de los nombres cualificados (independientemente de los espacios de nombres que estemos usando), y es cuando se produce una ambigüedad, es decir, existen dos identificados iguales en distintos espacios de nombres que estamos usando. Por ejemplo existen dos clases Object definidas una en el espacio de nombres GLib y otra en Gtk, así si hemos incluido ambos y queremos asegurarnos que usamos una clase y no otra debemos hacerlo mediante los identificadores cualificados; es decir, GLib.Object o bien Gtk.Object dependiendo del identificador al que queramos acceder.
En todos los programas escritos en Vala el espacio de nombres GLib se importa por defecto por lo que tenemos que tener en cuenta los identificadores que existen dentro de este espacio de nombres por si surgen casos de ambigüedad como el anteriormente descrito.
Todo aquello que no se define dentro de algún espacio de nombres concreto, se supone que está definido en un espacio de nombres genérico. Para acceder a este espacio de nombres global se usa el prefijo global::identificador.
Los espacios de nombres pueden estar anidados, por lo que nos podremos encontrar cosas como EspacioNombres1.EspacioNombres1A.identificador.
Por último decir que la nomenclatura de los espacios de nombres sigue el estándar CamelCase y es muy conveniente que los desarrolladores sigan la norma para conseguir uniformidad en el código desarrollado por toda la comunidad.
Estructuras
editarUna estructura de datos se puede definir como un conglomerado de datos básicos del lenguaje de programación, con el objetivo de facilitar la manipulación de la misma. Así en Vala es posible definir una estructura mediante la palabra reservada struct. A continuación un ejemplo de una definición de una estructura:
struct Persona {
public int edad;
public string nombre;
}
Una estructura en Vala puede tener miembros privados para lo cual se definen mediante la palabra reservada private. Los miembros de una estructura se definen como públicos por defecto si no se indica otra cosa. Para inicializar los valores de una estructura se puede obrar de alguna de las siguientes formas:
Persona p1 = Persona(); // Inicializacion sin valores
Persona p2 = Persona(18, "Juan"); // Inicializacion con valores en una única línea
// Inicializacion con valores definidos en varias líneas
Persona p3 = Persona() {
edad = 18,
nombre = "Juan"
};
Las estructuras se almacenan en la pila del programa y cuando se realiza una asignación de una estructura a otra se realiza mediante la copia de los valores, es decir, se realiza copia por valor.
Clases
editarUna clase es un tipo de objetos definido para resolver una parte del problema definido. Se verá una definición más en profundidad de este tipo de programación en la sección Programación Orientada a Objetos en Vala. A continuación se define una clase de objetos simple:
class NombreClase : NombreSuperClase, NombreInterfaz {
// Definición de la clase
}
Mediante este código definiríamos una nueva clase llamada NombreClase, que heredaría de una clase llamada NombreSuperClase y que implementaría la interfaz NombreInterfaz. Todos estos terminos se tratarán en más profundidad en la sección Programación Orientada a Objetos en Vala.
Interfaces
editarLas interfaces en Vala son algo distintas a las definidas por Java o C# y es que pueden ser usadas como mixins que tienen una serie de métodos y propiedades que la clase hija hereda, aunque estos mixins no están pensados para ser autónomos. Se ofrecerán más detalles sobre las interfaces en Vala en la sección Programación orientada a objetos en Vala. A continuación un ejemplo simple de definición de una interfaz en Vala:
interface NombreInterfaz : NombreInterfazPadre {
}
Como se ha visto en el ejemplo de arriba una interfaz puede heredar también de otra interfaz padre.
Atributos del código
editarLos atributos del código indican al compilador de Vala como se supone que debe funcionar el código en la plataforma destino. La sintaxis que usan estos atributos es [Atributo] o [Atributo(parametro1 = valor1, parametro2 = valor2, ... )].
En la mayoría de ocasiones se usan para definir los archivos vapi que usa el compilador de Vala para poder usar las bibliotecas escritas en C desde el lenguaje de programación Vala.