This site has been retired. For up to date information, see handbook.gnome.org or gitlab.gnome.org.


[Home] [TitleIndex] [WordIndex

Traducción al castellano del tutorial de Vala

Esta página se está traduciendo actualmente, puede consultar el manual completo en inglés en la siguiente dirección: http://live.gnome.org/Vala/Tutorial

Introducción

Aviso: Vala es un proyecto en curso, y sus características podrían sufrir cambios. Se intentará mantener el tutorial lo más actualizado posible. No se puede prometer que las técnicas sugeridas sean necesariamente las mejores prácticas, pero de nuevo se intentará estar al día con este tipo de cosas.

¿Qué es Vala?

Vala es un nuevo lenguaje de programación que permite utilizar modernas técnicas de programación para escribir aplicaciones que funcionan con las bibliotecas de tiempo de ejecución de GNOME, particularmente GLib y GObject. Esta plataforma ha proporcionado durante mucho tiempo un entorno de programación muy completo, con características como un sistema de tipado dinámico y gestión asistida de memoria. Antes de crear Vala, la única manera de programar para la plataforma era con la API nativa de C, que expone muchos detalles no deseados, con un lenguaje de alto nivel que tiene una máquina virtual auxiliar, como Python o el lenguaje C# de Mono o, alternativamente, con C++ a través de una biblioteca contenedora (wrapper)

Vala es diferente a todas estas otras técnicas, ya que genera código C que se puede compilar para ejecutarse sin necesidad de bibliotecas externas aparte de la plataforma GNOME. Esto tiene bastantes consecuencias, pero las más importantes son:

Por tanto, aunque Vala es un lenguaje moderno con muchas de las características que esperaba, obtiene su potencia de una plataforma existente, y debe de alguna forma cumplir con las reglas establecidas por la misma.

¿A quién va dirigido el tutorial?

Este tutorial no entrará en profundidad en las prácticas de programación básicas. Solo se explicarán brevemente los principios de la programación orientada a objetos, enfocada en como Vala aplica los conceptos. Por tanto, será de utilidad si ya tiene experiencia con varios lenguajes de programación, aunque no se requiere el conocimiento de ninguno en particular.

Vala comparte bastante sintaxis con C#, pero se intentará evitar describir las características en función a su parecido o diferencia con C# o Java, con el objetivo de hacer un tutorial más accesible.

Será útil tener una comprensión razonable de C. Si bien esto no es necesario para entender Vala como tal, es importante darse cuenta de que los programas en Vala se ejecutan como C, y probablemente interactuarán con bibliotecas en C. El conocimiento de C realmente hará más fácil una comprensión mas profunda de Vala.

Convenciones

El código se presentará en texto monoespaciado, los comandos se precederán por el símbolo $. Aparte de esto, todo debería ser obvio. Los fragmentos de código están escritos de forma muy explícita, es posible que algunos de los comentarios den información redundante. Se intentará explicar dónde se pueden omitir algunas cosas, pero eso no significa que se recomiende hacerlo.

En algún momento habrá referencias a la documentación de Vala, pero eso aún no es posible, realmente.

Un primer programa

Fácil de adivinar, pero aún así:

class Demo.HelloWorld : GLib.Object {

    public static int main(string[] args) {

        stdout.printf("Hello, World\n");

        return 0;
    }
}

Por supuesto, un Hola Mundo. Algunas partes deberían resultarle familiares al lector pero, no obstante, a continuación se hará un recorrido paso a paso por el código.

class Demo.HelloWorld : GLib.Object {

Esta línea identifica el principio de la definición de una clase. Las clases en Vala son conceptualmente similares a las de otros lenguajes. Una clase es, básicamente, un tipo de objeto, del cual se pueden crear instancias, que tendrán las mismas propiedades. La implementación de los tipos de clase es manejada por la librería gobject, pero los detalles de esto no son importantes para su uso general.

Lo que es importante tener en cuenta es que esta clase está definida específicamente como una subclase de GLib.Object. Esto se debe a que Vala permite otros tipos de clases, pero en la mayoría de casos, estas son las que se usarán. De hecho, algunas de las características del lenguaje Vala solo están permitidas si la clase es descendiente del Object de GLib.

Otras partes de esta línea muestran el uso de espacios de nombres y de nombres completos, no obstante esto se explicará después.

public static int main(string[] args) {

Esto es el principio de la definición de un método. Un método es una función relacionada con un tipo de objeto que se puede ejecutar con un objeto de ese tipo. Que sea estático significa que se puede llamar al método sin poseer una instancia particular de ese tipo. El hecho de que este método se llame main y tenga la cabecera que tiene, significa que Vala lo reconocerá como el punto de entrada para el programa.

El método main no tiene que estar definido dentro de una clase. Sin embargo, si se define dentro de la clase, deberá ser static. No importa si es public o private. El tipo devuelto puede ser int o void. Si es void el programa terminará implícitamente con código de finalización 0. El parámetro matriz de string, que contiene los argumentos de línea de comandos, es opcional.

stdout.printf("Hello, World\n");

stdout es un objeto del espacio de nombres («namespace») GLib al que Vala asegura el acceso siempre que se requiera. Esta línea ordena a Vala ejecutar el método llamado printf del objeto stdout, con la cadena Hello, World\n como argumento. En Vala, esta es la sintaxis usada siempre que se llama a un método que se encuentre en un objeto, o para acceder a los datos del objeto. \n es la secuencia para una nueva línea.

return 0;

return sirve para devolver un valor y terminar la ejecución del método main que también termina la ejecución del programa. El valor devuelto por el método main se toma como código de finalización del programa.

Las últimas líneas simplemente cierran las definiciones del método y la clase.

Compilar y ejecutar

Asumiendo que Vala está instalado, todo lo necesario para compilar y ejecutar este programa es:

$ valac hello.vala
$ ./hello

valac es el compilador de Vala, que compilará el código Vala en un archivo binario. El binario resultante tendrá el mismo nombre que el archivo fuente y se podrá ejecutar. Probablemente pueda deducirse el resultado.

Conceptos básicos

Archivos fuente y compilación

El código Vala se escribe en archivos con extensión .vala. Vala no impone tanta estructura a nivel de lenguaje como Java; no hay conceptos como paquetes o archivos clase en el mismo sentido. En lugar de eso, la estructura se define con el texto contenido en cada archivo, describiendo la ubicación lógica del código con construcciones como por ejemplo los espacios de nombres. Cuando se quiere compilar código Vala, se pasa al compilador la lista de archivos requeridos y Vala se encarga de averiguar cómo se combinan entre sí.

Debido a esto, se pueden poner tantas clases o funciones como quiera en el mismo archivo, incluso combinando partes de distintos espacios de nombres juntas. No obstante, no se recomienda hacerlo. Hay ciertos convenios que sí se recomienda seguir. Un ejemplo de cómo estructurar el código Vala es el propio proyecto Vala.

Todos los archivos de fuentes de un mismo paquete se proporcionan como parámetros en línea de comandos al compilador de Vala, valac, junto con las opciones («flags») del compilador. Esto es similar a como se compila el código fuente Java. Por ejemplo:

$ valac compiler.vala --pkg libvala

producirá un archivo binario con el nombre compiler enlazado al paquete libvala. De hecho, así es como se produjo el compilador valac.

Si se desea que el binario tenga un nombre distinto o si se han pasado varios archivos al compilador, se puede especificar el nombre del binario explícitamente con la opción -o:

$ valac source1.vala source2.vala -o myprogram
$ ./myprogram

Llamar a valac con la opción -C, no compilará el programa en un binario. En lugar de esto, proporcionará el código C intermedio para cada uno de los archivos fuente Vala dentro de el correspondiente archivo fuente C, en el caso que se acaba de ver source1.c y source2.c. Si se observa el contenido de estos archivos, se puede ver que una clase Vala es equivalente a la misma funcionalidad en C, pero mucho más breve. También puede observarse que dicha clase se registra dinámicamente en el sistema en ejecución. Es un buen ejemplo de la potencia de la plataforma GNOME, pero como se ha dicho antes, no es necesario conocer estos detalles para hacer un uso correcto de Vala.

En el caso de que se quiera obtener un archivo de cabecera C se deberá emplear la opción -H de valac:

$ valac hello.vala -C -H hello.h

Visión general de la sintaxis

La sintaxis de Vala es una mezcla, basada principalmente en la de C#. Como resultado, la mayor parte de esta debería ser familiar para los programadores que conozcan cualquier lenguaje similar a C. Teniendo esto en cuenta, se hará una descripción breve de la misma.

El alcance de declaración se define mediante corchetes. Un objeto o referencia sólo es válido entre { y }. Estos delimitadores se usan también para definir clases, métodos, bloques de código, etc, de manera que tengan automáticamente su propio alcance. Vala no es estricto acerca de dónde se declaran las variables.

Un identificador se define por su tipo y nombre, por ejemplo, int c describe un entero llamado c. En el caso de los tipos por valor, esto también crea un objeto del tipo dado. Para tipos referenciados esto simplemente define una nueva referencia que no apunta a nada inicialmente.

Para los nombres de identificadores, se aplican las mismas reglas que para los identificadores de C: el primer carácter debe estar entre [a-z], [A-Z] o subrayados ( _ ), los siguientes pueden estar entre estos y [0-9]. No se permiten otros caracteres Unicode. Es posible usar palabras reservadas como identificadores, añadiéndoles el carácter @ como prefijo. Este carácter no forma parte del nombre. Por ejemplo, puede llamar a un método foreach escribiendo @foreach, siendo esta una palabra reservada de Vala.

Los tipos por referencia se instancian empleando el operador new y el nombre de un método de construcción, que generalmente es el propio nombre del tipo, por ejemplo, Object o = new Object() crea un Object nuevo y convierte a o en una referencia a este.

Comentarios

Vala permite varias maneras de escribir comentarios en el código:

// Comment continues until end of line

/* Comment lasts between delimiters */

/**
 * Documentation comment
 */

Estos comentarios se manejan de la misma manera que en la mayoría de lenguajes, y requieren por ello poca explicación. Los comentarios para documentación no son específicos de Vala, pero una herramienta de generación de documentación como Valadoc los reconoce.

Tipos de datos

En general, hay dos tipos de dato en Vala: tipos referenciados y tipos por valor. Estos nombres describen cómo se usan en el sistema las instancias de los tipos; un tipo por valor se copia cada vez que se asigna a un identificador nuevo, un tipo referenciado no se copia, en lugar de esto el nuevo identificador es una nueva referencia al mismo objeto.

Una constante se define poniendo const delante del tipo. El convenio para constantes es TODO_EN_MAYUSCULAS.

Tipos por valor

Vala soporta un conjunto de tipos simples, como la mayoría de lenguajes.

A continuación se muestran algunos ejemplos.

/* atomic types */
unichar c = 'u';
float percentile = 0.75f;
const double MU_BOHR = 927.400915E-26;
bool the_box_has_crashed = false;

/* defining a struct */
struct Vector {
    public double x;
    public double y;
    public double z;
}

/* defining an enum */
enum WindowType {
    TOPLEVEL,
    POPUP
}

La mayoría de estos tipos pueden tener diferentes tamaños en distintas plataformas, exceptuando los tipos de tamaño garantizado. El operador sizeof devuelve el tamaño que ocupa, en bytes, una variable de un tipo dado:

ulong nbytes = sizeof(int32);    // nbytes will be 4 (= 32 bits)

Se pueden determinar los valores máximo y mínimo de un tipo numérico con .MIN y .MAX, por ejemplo, int.MIN e int.MAX.

Cadenas de caracteres

El tipo de dato para cadenas de caracteres es string. Las cadenas de Vala están codificadas en UTF-8 y son inmutables.

string text = "A string literal";

Vala ofrece una característica llamada cadenas literalesverbatim strings»). Son cadenas de caracteres en las cuales las secuencias de escape (como puede ser \n) no se interpretan, los saltos de línea se conservan y los signos de puntuación no tienen que enmascararse. Estas cadenas se delimitan con tres comillas dobles. Las posibles sangrías tras un salto de línea también forman parte de la cadena.

string verbatim = """This is a so-called "verbatim string".
Verbatim strings don't process escape sequences, such as \n, \t, \\, etc.
They may contain quotes and may span multiple lines.""";

Las cadenas precedidas por @ son consideradas patrones. En ellas se evalúan las variables y expresiones precedidas por $:

int a = 6, b = 7;
string s = @"$a * $b = $(a * b)";  // => "6 * 7 = 42"

Los operadores de igualdad == y != comparan el contenido de dos cadenas de caracteres, a diferencia del comportamiento de Java, donde se compara la igualdad entre referencias.

Es posible dividir una cadena con [inicio:fin]. Los valores negativos representan posiciones relativas al final de la cadena:

string greeting = "hello, world";
string s1 = greeting[7:12];        // => "world"
string s2 = greeting[-4:-2];       // => "or"

Obsérvese que los índices en Vala empiezan en 0 como en la mayoría de lenguajes de programación. Desde Vala 0.11 se puede acceder a un solo byte mediante [índice]:

uint8 b = greeting[7];             // => 0x77

Sin embargo, no se puede asignar un byte nuevo a esta posición ya que, como se ha comentado anteriormente, las cadenas de caracteres en Vala son inmutables.

Muchos de los tipos básicos tienen métodos intuitivos para tomar el valor de una cadena, así como para convertir su valor a una cadena, por ejemplo:

bool b = bool.parse("false");           // => false
int i = int.parse("-52");               // => -52
double d = double.parse("6.67428E-11"); // => 6.67428E-11
string s1 = true.to_string();           // => "true"
string s2 = 21.to_string();             // => "21"

Dos métodos útiles para escribir y leer cadenas en y desde la línea de comandos (y para las primeras pruebas con Vala) son, respectivamente, stdout.printf() y stdin.read_line():

stdout.printf("Hello, world\n");
stdout.printf("%d %g %s\n", 42, 3.1415, "Vala");
string input = stdin.read_line();
int number = int.parse(stdin.read_line());

Ya se ha presentado stdout.printf() en el ejemplo Hello World. Este método acepta un número arbitrario de argumentos de distintos tipos, mientras que el primer argumento es una cadena de formato, que sigue las mismas reglas que las cadenas de formato de C. Si se requiere mostrar un mensaje de error, se puede emplear stderr.printf() en lugar de stdout.printf().

Adicionalmente, la operación in puede utilizarse para determinar si una cadena contiene a otra, por ejemplo:

if ("ere" in "Able was I ere I saw Elba.") ...

Para mayor información, consulte la documentación completa de la clase ''string''.

También está disponible un programa de ejemplo que muestra el uso de las cadenas de caracteres.

Matrices

Las matrices se declaran dando un nombre de tipo seguido de [] y se crean usando el operador new, por ejemplo, int[]a = new int[10] para crear una matriz de enteros. La longitud de estas matrices se puede obtener accediendo al miembro variable length, por ejemplo, int count = a.length. Se debe tener en cuenta que si se escribe Object[] a = new Object[10] no se creará ningún objeto, sólo la matriz para almacenarlos.

int[] a = new int[10];
int[] b = { 2, 4, 6, 8 };

Una matriz se puede dividir con [inicio:fin]:

int[] c = b[1:3];     // => { 4, 6 }

El resultado de dividir una matriz es una referencia a los datos requeridos, no una copia de estos. Sin embargo, si se asigna este resultado a una variable owned (como se ha hecho antes) sí que se copian los datos. Si se quiere evitar la copia, se puede optar por asignar el resultado a una matriz unowned o se puede pasar directamente a un argumento (los argumentos son, de manera predeterminada, unowned).

unowned int[] c = b[1:3];     // => { 4, 6 }

Las matrices multidimensionales se definen con [,] o [,,], etc.

int[,] c = new int[3,4];
int[,] d = {{2, 4, 6, 8},
            {3, 5, 7, 9},
            {1, 3, 5, 7}};
d[2,3] = 42;

Este tipo de matriz se representa como un bloque de memoria contigua. Las matrices multidimensionales compuestas ([][], también conocidas como "matrices apiladas" o "matrices de matrices"), en las que cada fila puede tener diferente longitud, no están soportadas aún.

Para obtener la longitud de cada dimensión de una matriz multidimensional, el miembro length pasa a ser una matriz que almacena la longitud o cada dimensión respectivamente.

int[,] arr = new int[4,5];
int r = arr.length[0];
int c = arr.length[1];

Tenga en cuenta que se no se pueden obtener una matriz de una dimensión a partir de una matriz multidimensional, ni siquiera dividir una matriz multidimensional:

int[,] arr = {{1,2},
                {3,4}};
int[] b = arr[0];  // won't work
int[] c = arr[0,];  // won't work
int[] d = arr[:,0];  // won't work
int[] e = arr[0:1,0];  // won't work
int[,] f = arr[0:1,0:1];  // won't work

Se pueden añadir elementos a una matriz dinámicamente haciendo uso del operador +=. Sin embargo, sólo funciona para las matrices definidas localmente o privadas. La matriz se reubica automáticamente si es necesario. Internamente esta reubicación se realiza con tamaños crecientes en potencias de 2 por razones de eficiencia en tiempo de ejecución. No obstante, .length contiene el número actual de elementos, no el tamaño interno.

int[] e = {};
e += 12;
e += 5;
e += 37;

El tamaño de una matriz se puede variar llamando a resize() sobre esta. Esta operación mantiene el contenido original (tanto de éste como quepa en el nuevo tamaño).

int[] a = new int[5];
a.resize(12);

Si se ponen corchetes después del identificador junto a una indicación de tamaño, se creará una matriz de tamaño fijo. Las matrices de tamaño fijo se almacenan en la pila (si se usan como variables locales) o se almacenan in-line (si se usan como campos) y no se pueden reubicar más tarde.

int f[10];     // no 'new ...'

Vala no comprueba los límites en tiempo de ejecución en el acceso a matrices. Si se requiere mayor seguridad, se debería usar una estructura de datos más sofisticada, como el ArrayList. Se abordará este tema más adelante en la sección sobre colecciones.

Tipos referenciados

Los tipos referenciados son todos los tipos declarados como una clase, independientemente de si descienden o no del Object de GLib. Vala se asegurará de que cuando se pasa un objeto por referencia el sistema mantenga la cuenta del número de referencias realizadas actualmente con el propósito de gestionar la memoria de forma automática. El valor de una referencia que no apunta a nada es null. Se tratarán las clases y sus características en la sección sobre programación orientada a objetos.

/* defining a class */
class Track : GLib.Object {             /* subclassing 'GLib.Object' */
    public double mass;                 /* a public field */
    public double name { get; set; }    /* a public property */
    private bool terminated = false;    /* a private field */
    public void terminate() {           /* a public method */
        terminated = true;
    }
}

Promoción de tipos estáticos

En Vala es posible promocionar una variable de un tipo a otro. Para promocionar una variable de un tipo estático se escribe el tipo deseado entre paréntesis. La promoción estática no impone ningún tipo de comprobación de seguridad de los tipos en tiempo de ejecución. Funciona para todos los tipos disponibles en Vala. Por ejemplo:

int i = 10;
float j = (float) i;

Vala soporta otro mecanismo de promoción llamado promoción dinámica que realiza comprobación de tipos en tiempo de ejecución y se describe en la sección sobre programación orientada a objetos.

Inferencia de tipos

Vala cuenta con un mecanismo llamado inferencia de tipos, con la que una variable local se puede definir usando var en vez de indicar un tipo, mientras que no haya ambigüedad en el tipo que se ha querido indicar. El tipo se infiere de la asignación del lado derecho del igual. Esto ayuda a reducir redundancia innecesaria en el código sin sacrificar el tipado estático:

var p = new Person();     // same as: Person p = new Person();
var s = "hello";          // same as: string s = "hello";
var l = new List<int>();  // same as: List<int> l = new List<int>();
var i = 10;               // same as: int i = 10;

Este mecanismo sólo funciona para variables locales. La inferencia de tipos es especialmente útil para tipos con argumentos genéricos (esto se tratará más adelante). Compare:

MyFoo<string, MyBar<string, int>> foo = new MyFoo<string, MyBar<string, int>>();

, frente a:

var foo = new MyFoo<string, MyBar<string, int>>();

Definir un tipo nuevo a partir de otro

Definir un tipo nuevo significa derivarlo del que se necesite. Por ejemplo:

/* Define a new type from a container like GLib.List with elements type GLib.Value */
public class ValueList : GLib.List<GLib.Value> {
        [CCode (has_construct_function = false)]
        protected ValueList ();
        public static GLib.Type get_type ();
}

Operadores

=

Asignación. El operando de la izquierda debe ser un identificador, y el de la derecha un valor o referencia, según el caso.

+, -, /, *, %

Aritmética básica, aplicada a los operandos izquierdo y derecho. El operador + también puede concatenar cadenas de caracteres.

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

Operaciones aritmeticas entre los operandos izquierdo y derecho, donde el operando de la izquierda debe ser un identificador al que se asigna el resultado.

++, --

Operaciones de incremento y decremento con asignación implícita. Estos operandos sólo requieren un argumento, que debe ser un identificador de un tipo de dato simple. El valor se cambia y se asigna de nuevo al identificador. Estos operadores deben situarse como prefijo o como sufijo; en el primer caso el valor que se evalúe será el recién calculado, mientras que en el segundo caso se evaluará el valor original.

|, ^, &, ~, |=, &=, ^=

Operaciones a nivel de bit: disyunción (o ó "or"), disyunción exclusiva ("xor"), conjunción (y o "and") y negación. Los operadores del segundo grupo incluyen asignación y son análogos a las versiones aritméticas. Todos estos operadores se pueden aplicar a cualquier tipo de dato simple. El hecho de que no exista un operador de asignación asociado a ~ se debe a que este es un operador unario. La operación equivalente es simplemente a = ~a.

<<, >>

Operaciones de desplazamiento de bits. Desplazan el operando de la izquierda un número de bits de acuerdo al operando de la derecha.

<<=, >>=

Operaciones de desplazamiento de bits. Desplazan el operando de la izquierda un número de bits de acuerdo al operando de la derecha. El operando de la izquierda debe ser un identificador, al que se asigna el resultado.

==

Comprobación de igualdad. Evalúa a un valor bool dependiendo de si los operandos izquierdo y derecho son iguales. En caso de los tipos por valor esto significa que los valores son iguales, en el caso de los tipos por referencia implica que los objetos sean la misma instancia. Una excepción a esta regla es el tipo string, que se evalúa según la igualdad del valor.

<, >, >=, <=, !=

Comprobaciones de desigualdad. Se evalua a un valor bool dependiendo de si los operandos izquierdo y derecho son distintos de la manera descrita en cada caso. Son validos para tipos por valor, y para el tipo string. En este último caso estos operadores compara orden lexicográfico.

!, &&, ||

Operaciones lógicas: negación (no), conjunción (y) y disyunción (o). Estos operadores se pueden aplicar a valores booleanos, requiriendo el primero un solo valor y los otros, dos valores.

? :

Operador condicional ternario. Evalúa una condición y devuelve el valor de la sub-expresión a la izquierda o a la derecha basándose en la veracidad de la condición: condición ? valor si es cierta : valor si es falsa.

??

Operador de comprobación de nulidad y asignación: a ?? b es equivalente a a != null ? a : b. Este operador es útil, por ejemplo, para asignar un valor predeterminado en caso de que una referencia sea nula:

stdout.printf("Hello, %s!\n", name ?? "unknown person");

in

Comprueba si el operando de la derecha contiene al operando de la izquierda. este operador funciona en matrices, cadenas de caracteres, colecciones de datos o cualquier otro tipo que tenga un método contains() apropiado. Para cadenas puede realizar búsqueda de subcadenas.

Los operadores no se pueden sobrecargar en Vala. Hay operadores adicionales, válidos en el contexto de las declaraciones lambda y otras tareas específicas; estos se explicaran en el contexto en el que sean aplicables.

Estructuras de control

A continuación se muestran varios ejemplos de código de diferentes estructuras de control y el efecto de dicho código:

while (a > b) { a--; }

Decrementa a repetidamente, comprobando antes de cada iteración que a sea mayor que b.

do { a--; } while (a > b);

Decrementa a repetidamente, comprobando después de cada iteración que a sea mayor que b.

for (int a = 0; a < 10; a++) { stdout.printf("%d\n", a); }

Inicializa a a 0, después se imprime a repetidamente hasta que su valor deje de ser mayor de 10, el valor de a se incrementa tras cada iteración.

foreach (int a in int_array) { stdout.printf("%d\n", a); }

Imprime en la salida cada entero de una matriz, o cualquier otra colección iterable. El significado de «iterable» se describirá más adelante.

Los cuatro tipos anteriores de bucle se pueden controlar con las palabras reservadas break y continue. Una instrucción break provocará que el bucle termine inmediatamente, mientras que continue saltará a la parte de la comprobación de la iteración (terminando así la iteración actual, pero sin salir del bucle).

if (a > 0) { stdout.printf("a is greater than 0\n"); }
else if (a < 0) { stdout.printf("a is less than 0\n"); }
else { stdout.printf("a is equal to 0\n"); }

Ejecuta partes de código concretas dependiendo de un conjunto de condiciones. La primera condición que cumplir decide qué código se ejecuta, si a es mayor que 0 no se realizará la comprobación de si es menor que 0. Se permite cualquier cantidad de bloques else if, y uno o ningún bloque else.

switch (a) {
case 1:
    stdout.printf("one\n");
    break;
case 2:
case 3:
    stdout.printf("two or three\n");
    break;
default:
    stdout.printf("unknown\n");
    break;
}

Una sentencia switch ejecuta exactamente una o ninguna sección de código basándose en el valor que se le pasa. En Vala no hay paso a través de los distintos casos (bloques etiquetados con la palabra reservada case), excepto en los casos vacíos. Para asegurar esto, cada case no vacío debe terminar con una de las palabras reservadas break, return o throw. Se pueden utilizar las sentencias switch con cadenas de caracteres.

Como nota para los programadores de C: las condiciones deben evaluarse siempre un valor booleano. Es decir, que si uno quiere comprobar si una variable es nula o 0, debe hacerlo explícitamente: if (object != null) { } or if (number != 0) { }.

Elementos del lenguaje

Métodos

En Vala, las funciones se llaman métodos, independientemente de si están definidas dentro de una clase o no. Desde este momento se empleará el término método.

int method_name(int arg1, Object arg2) {
    return 1;
}

El anterior fragmento de código define un método con el nombre method_name, que toma dos argumentos, un entero y un Object (el primero pasado por valor y el segundo por referencia, como se ha descrito anteriormente). El método devuelve un entero, que en este caso es 1.

Todos los métodos Vala son funciones C, y por ello toman un numero arbitrario de argumentos y devuelven un valor (o ninguno si el método se declara como void). Se puede aproximar un mecanismo para devolver más valores, poniendo datos en lugares conocidos por el código que llama al método. Los detalles de este mecanismo se explicarán en la sección "Dirección de los parámetros", en la parte avanzada de este tutorial.

El convenio para los nombres de métodos en Vala es todo_en_minúscula con subrayados como separadores entre palabras. Este proceder puede parecer poco familiar a programadores de Java o C#, acostumbrados a los nombres de métodos en CamelCase o mixedCamelCase. Pero con este estilo se consigue consistencia con otras bibliotecas de Vala y C/GObject.

No es posible tener varios métodos con el mismo nombre pero distinto prototipo dentro del mismo ámbito de definición (mecanismo conocido como "sobrecarga de métodos"):

void draw(string text) { }
void draw(Shape shape) { }  // not possible

Esto se debe al hecho de que se pretende que las librerías producidas con Vala puedan ser utilizadas también por programadores de C. En su lugar, en Vala se haría lo siguiente:

void draw_text(string text) { }
void draw_shape(Shape shape) { }

Eligiendo nombres ligeramente distintos puede evitarse la colisión entre los nombres. En lenguajes que soportan sobrecarga de métodos, esta suele emplearse para ofrecer métodos de conveniencia con menos parámetros que enlazan con un método más general:

void f(int x, string s, double z) { }
void f(int x, string s) { f(x, s, 0.5); }  // not possible
void f(int x) { f(x, "hello"); }           // not possible

En este caso puede usarse la característica predeterminada de los argumentos de Vala con el fin de conseguir un comportamiento parecido con un solo método. Pueden definirse valores por defecto para los últimos parámetros de un método, para evitar pasarlos explícitamente al llamar al método:

void f(int x, string s = "hello", double z = 0.5) { }

Algunas llamadas posibles a este método podrían ser:

f(2);
f(2, "hi");
f(2, "hi", 0.75);

Incluso se puede definir métodos con una lista de argumentos de longitud variable (varargs) como stdout.printf, aunque no es del todo recomendable. Más adelante se hablará de esto.

Vala realiza una comprobación de nulidad básica en los parámetros y los valores devueltos de los métodos. Si se permite que un parámetro o un valor de retorno sea null, el símbolo del tipo debe ir seguido con un modificador ?. Esta información adicional ayuda al compilador de Vala a realizar comprobaciones estáticas y añadir verificaciones en tiempo de ejecución en las precondiciones de los métodos, lo que puede ayudar a evitar errores relacionados como eliminar una referencia a un puntero a null.

string? method_name(string? text, Foo? foo, Bar bar) {
    // ...
}

En este ejemplo text, foo y el valor de retorno pueden ser null, no obstante bar no debe ser null.

Delegados

delegate void DelegateType(int a);

Los delegados («delegate») representan métodos, permitiendo que se puedan pasar fragmentos de código entre objetos. En el ejemplo de arriba se define un tipo llamado DelegateType que representa métodos que toman un int y no devuelven nada. Cualquier método que concuerde con este prototipo puede asignarse a una variable de este tipo o pasarse como un argumento de este tipo a un método.

delegate void DelegateType(int a);

void f1(int a) {
    stdout.printf("%d\n", a);
}

void f2(DelegateType d, int a) {
    d(a);       // Calling a delegate
}

void main() {
    f2(f1, 5);  // Passing a method as delegate argument to another method
}

En este código se ejecuta el método f2, pasándole una referencia al método f1 y el número 5. f2 ejecutará entonces el método f1, pasándole a este el número.

Los delegados también pueden crearse localmente. Un método miembro de una clase puede asignarse a un 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;
    }
}

Métodos anónimos o clausuras

(a) => { stdout.printf("%d\n", a); }

Un método anónimo, también conocido como expresión lambda, función literal o clausura, puede definirse en Vala con el operador =>. La lista de parámetros se sitúa al lado izquierdo del operador, mientras que el cuerpo del método va en el lado derecho.

Un método anónimo por sí mismo, como el del ejemplo de arriba, no tiene mucho sentido. Estos métodos sólo son útiles si se asignan directamente a una variable de un tipo delegado o si se pasan como argumento a otro método.

Tenga en cuenta que ni los tipos de los parámetros ni los del valor de retorno se indican explícitamente. En lugar de esto, los tipos se infieren del prototipo del delegado con el que se utilizan.

A continuación se muestra un ejemplo de asignación de un método anónimo a una variable delegada:

delegate void PrintIntFunc(int a);

void main() {
    PrintIntFunc p1 = (a) => { stdout.printf("%d\n", a); };
    p1(10);

    // Curly braces are optional if the body contains only one statement:
    PrintIntFunc p2 = (a) => stdout.printf("%d\n", a);
    p2(20):
}

Y seguidamente el paso de un método anónimo a otro método:

delegate int Comparator(int a, int b);

void my_sorting_algorithm(int[] data, Comparator compare) {
    // ... 'compare' is called somewhere in here ...
}

void main() {
    int[] data = { 3, 9, 2, 7, 5 };
    // An anonymous method is passed as the second argument:
    my_sorting_algorithm(data, (a, b) => {
        if (a < b) return -1;
        if (a > b) return 1;
        return 0;
    });
}

Los métodos anónimos son verdaderas clausuras. Esto significa que se puede acceder a las variables locales del método externo desde el interior de la expresión lambda:

delegate int IntOperation(int i);

IntOperation curried_add(int a) {
    return (b) => a + b;  // 'a' is an outer variable
}

void main() {
    stdout.printf("2 + 4 = %d\n", curried_add(2)(4));
}

En este ejemplo, curried_add (consulte Currificación) devuelve un método recién creado que conserva el valor de a. El método devuelto se llama directamente más adelante con 4 como argumento, dando como resultado la suma de los dos números.

Espacios de nombres

namespace NameSpaceName {
    // ...
}

Todo lo que se encuentra entre las llaves en el código anterior está dentro del espacio de nombres NameSpaceName y se debe hacer referencia a ello como tal. Cualquier fragmento de código fuera de este espacio de nombres, deberá usar nombres completos para acceder a todo lo que se encuentra dentro, o bien estar situado en un archivo con la declaración using adecuada para importar este espacio de nombres:

using NameSpaceName;

// ...

Por ejemplo si el espacio de nombres Gtk se importa con using Gtk; se puede escribir simplemente Window en lugar de Gtk.Window. Un nombre completo sólo será necesario en caso de ambigüedad, por ejemplo entre GLib.Object y Gtk.Object.

El espacio de nombres GLib se importa de manera predeterminada. Imagine una línea using GLib; al principio de cada archivo Vala.

Todo lo que no se encuentre dentro de un espacio de nombres separado, quedará en el espacio de nombres anónimo global. Si se debe hacer referencia al espacio global explícitamente debido a una ambigüedad, puede hacerse usando el prefijo global::.

Los espacios de nombres puede estar anidados, bien anidando sus declaraciones o bien dando un nombre del tipo NameSpace1.NameSpace2.

Muchas otras definiciones pueden declararse dentro de un espacio de nombres siguiendo el mismo convenio, por ejemplo class NameSpace1.Test { ... }`. Tenga en cuenta que cuando se hace esto el nombre final de la definición será aquel en el cual esté anidada la declaración más el espacio de nombre declarado en la definición.

Estructuras `struct`

struct StructName {
    public int a;
}

El código presentado arriba define un tipo de struct (o estructura), es decir, un tipo de valor compuesto. Una estructura Vala puede contener métodos con ciertas limitaciones así como miembros privados, dado lo cual se requiere el modificador de acceso public.

struct Color {
    public double red;
    public double green;
    public double blue;
}

De la siguiente manera se puede inicializar una estructura:

// without type inference
Color c1 = Color();
Color c2 = { 0.5, 0.5, 1.0 };
Color c3 = Color() {
    red = 0.5,
    green = 0.5,
    blue = 1.0
};

// with type inference
var c4 = Color();
var c5 = Color() {
    red = 0.5,
    green = 0.5,
    blue = 1.0
};

Las estructuras se almacenan en la pila («stack») y se copian al asignarlas.

Para definir una matriz de estructuras, por favor vea las FAQ.

Clases

class ClassName : SuperClassName, InterfaceName {
}

Este ejemplo define una clase, es decir un tipo por referencia. En contraposición a las estructuras, las instancias de las clases se almacenan en el montón («heap»). Hay mucha más sintaxis relacionada con las clases, que se presentará en profundidad en la sección sobre programación orientada a objetos.

Interfaces

interface InterfaceName : SuperInterfaceName {
}

El ejemplo de arriba define una interfaz, esto es, un tipo no instanciable. Para crear una instancia de una interfaz, primero deben implementarse sus métodos abstractos en una clase no abstracta. Las interfaces Vala son mucho más potentes que las interfaces de Java o C#. De hecho se pueden usar como «mixins». Los detalles de las interfaces se describen en la sección sobre programación orientada a objetos.

Atributos del código

Los atributos del código indican al compilador de Vala detalles sobre cómo se supone que debe funcionar el código en la plataforma objetivo. Su sintaxis es [AttributeName] o [AttributeName(param1 = value1, param2 = value2, ...)].

Se utilizan principalmente para enlaces entre lenguajes («bindings») en los achivos vapi, siendo [CCode(...)] el más empleado en estos casos. Otro ejemplo es el atributo [DBus(...)], usado para exportar interfaces remotas vía D-Bus (enlace en inglés).

Programación orientada a objetos

A pesar de que Vala no fuerza al desarrollador a trabajar con objetos, algunas de sus características no están disponibles de otra forma. De esta manera, es deseable programar con un estilo orientado a objetos la mayor parte del tiempo. Como con la mayoría de lenguajes actuales, para definir tipos de objeto propios debe definirse una clase.

La definición de una clase indica qué datos contiene un objeto de ese tipo, a qué otros tipos de objeto puede hacer referencia, y qué métodos pueden ejecutarse sobre este. La definición puede incluir el nombre de otra clase de la cual la presente será una subclase. Una instancia de una clase también puede ser una instancia de cualquiera de sus superclases, ya que hereda de esta todos sus métodos y datos, aunque puede no tener acceso a toda esta información. Una clase también puede implementar un número ilimitado de interfaces, que son conjuntos de definiciones de métodos que deben implementarse por la clase (una instancia de una clase es también una instancia de cada una de las interfaces implementadas por su clase o sus superclases).

Las clases en Vala también tienen miembros «static» (estáticos). Este modificador permite, tanto a los datos como a los métodos, definirse como pertenecientes a la clase como un todo, en lugar de a las instancias específicas de esta. Se puede acceder a este tipo de miembros incluso sin poseer una instancia de la clase.

Conceptos básicos

Una clase simple podría definirse como sigue:

public class TestClass : GLib.Object {

    /* Fields */
    public int first_data = 0;
    private int second_data;

    /* Constructor */
    public TestClass() {
        this.second_data = 5;
    }

    /* Method */
    public int method_1() {
        stdout.printf("private data: %d", this.second_data);
        return this.second_data;
    }
}

El código anterior define un nuevo tipo (registrado automáticamente por el sistema de tipos de la biblioteca gobject) que contiene tres miembros. Dos de los miembros son datos, los enteros definidos en la parte superior, y un método llamado method_1, que devuelve un entero. La declaración de la clase indica que esta clase es una subclase de GLib.Object, y por tanto sus instancias son también Objects, y también contienen los miembros de ese tipo. El hecho de que esta clase descienda de Object también significa que se puede emplear algunas de las propiedades especiales de Vala para acceder a algunas de las características del tipo Object.

Esta clase se ha descrito como public (de manera predeterminada, las clases son internal). Lo que esto implica es que se puede hacer referencia a ésta directamente mediante código que se encuentre fuera de este archivo (si usted es un programador C que ha trabajado con Glib/Gobject, reconocerá esto como un equivalente a definir las interfaces de una clase en un archivo de cabecera separado que el código externo pueda incluir).

Los miembros también se describen como public o private. El miembro first_data es public, por tanto es visible directamente para cualquier usuario de la clase, y puede modificarse sin que la instancia que lo contiene tenga constancia de ello. El segundo dato es private, así que sólo se puede hacer referencia a él desde el código perteneciente a esta clase. Vala soporta cuatro tipos distintos de modificadores de acceso:

public

Sin restricciones de acceso

private

El acceso se limita al interior de la definición de la clase o estructura. Es el predeterminado si no se ha especificado ningún modificador de acceso

protected

El acceso está limitado a la definición de la clase y a cualquier clase que herede de la misma

internal

El acceso se limita a clases definidas dentro del mismo package

El constructor incializa las nuevas instancias de una clase. Tiene el mismo nombre de la clase, puede o no requerir argumentos y se define sin tipo de retorno.

La última parte de esta clase es la definición de un método. Este método se llama method_1, y devuelve un entero. Dado que este método no es estático, se puede ejecutar únicamente sobre una instancia de esta clase, y por tanto puede acceder a miembros de dicha instancia. Puede hacer esto último por medio de la referencia this, que siempre apunta a la instancia sobre la que el método se llama. Mientras no haya ambigüedad el identificador this puede omitirse si así se desea.

Esta nueva clase se puede utilizar de la siguiente manera:

TestClass t = new TestClass();
t.first_data = 5;
t.method_1();

Construcción

Vala soporta dos métodos de construcción ligeramente distintos: el estilo Java/C# al que se atenderá de momento, y el estilo GObject que se describirá en la última sección de este capítulo.

Vala no soporta la sobrecarga de constuctores por los mismos motivos que no se permite la sobrecarga de métodos; esto significa que una clase no puede tener varios constructores con el mismo nombre. Sin embargo, no es un problema ya que Vala soporta constructores con nombre. Si se quieren ofrecer varios constructores se les debe añadir distintas extensiones:

public class Button : Object {

    public Button() {
    }

    public Button.with_label(string label) {
    }

    public Button.from_stock(string stock_id) {
    }
}

La instanciación es análoga:

new Button();
new Button.with_label("Click me");
new Button.from_stock(Gtk.STOCK_OK);

Se pueden enlazar constructores por medio de this() o this.extension_del_nombre():

public class Point : Object {
    public double x;
    public double y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public Point.rectangular(double x, double y) {
        this(x, y);
    }

    public Point.polar(double radius, double angle) {
        this.rectangular(radius * Math.cos(angle), radius * Math.sin(angle));
    }
}

void main() {
    var p1 = new Point.rectangular(5.7, 1.2);
    var p2 = new Point.polar(5.7, 1.2);
}

Destrucción

A pesar de que Vala gestiona la memoria automáticamente, puede ser que se necesite añadir destructores propios si se elige gestionar manualmente la memoria con punteros (en esto se profundizará más adelante) o si deben liberarse otros recursos. La sintaxis es la misma que en C# y C++:

class Demo : Object {
    ~Demo() {
        stdout.printf("in destructor");
    }
}

Dado que la gestión de memoria de Vala se basa en conteo de referencias en lugar de recolección de basura los destructores son deterministas y pueden emplearse para implementar el patrón RAII para la gestión de recursos (cerrar flujos, conexiones a bases de datos, etc.).

Señales

Las señales son un sistema proporcionado por la clase Object de GLib, y accesible fácilmente gracias a Vala desde todos los descendientes de Object. Una señal se puede ver como un evento para los programadores de C# o, para los programadores de Java, como una implementación alternativa de los «event listeners». En resumen, una señal es simplemente una forma de ejecutar un número arbitrario de métodos idénticos desde el exterior (es decir, con el mismo prototipo) aproximadamente en el mismo instante. Los mecanismos reales de ejecución son internos a GObject, y no son importantes para los programas Vala.

Una señal se define como un miembro de una clase, y se parece a un método sin cuerpo. Los manejadores de señales pueden añadirse a la señal usando el método connect(). Con el fin de profundizar en el asunto, el siguiente ejemplo también presenta las expresiones lambda, una forma muy práctica de escribir manejadores de señales en Vala:

public class Test : GLib.Object {

    public signal void sig_1(int a);

    public static int main(string[] args) {
        Test t1 = new Test();

        t1.sig_1.connect((t, a) => {
            stdout.printf("%d\n", a);
        });

        t1.sig_1(5);

        return 0;
    }
}

En el código anterior se crea una nueva clase llamada "Test", usando la sintaxis ya familiar. El primer miembro de la clase es una señal llamada "sig_1" que, según su definición, pasa un entero. En el método main de este programa, primero se crea una instancia de Test (un requisito, dado que las señales siempre pertenecen a instancias de las clases). A continuación, se conecta a la señal "sig_1" de la instancia creada un manejador, definido en línea como una expresión lambda. La definición indica que el método recibirá dos argumentos, llamados "t" y "a", pero no da sus tipos. Se puede hacer esto por que Vala ya conoce la definición de la señal, y por tanto sabe qué tipos se requieren.

El motivo por el que se pasan dos parámetros al manejador es por que cuando una señal se emite, el objeto sobre el cual se emite se pasa como primer argumento al manejador. El segundo argumento es el que pasa la señal.

Finalmente se emite la señal. Esto se hace llamándola como si se tratase de un método de la clase, y GObject se encarga de enviar el mensaje a todos los métodos manejadores conectados a la señal. Entender los mecanismos subyacentes mediante los que se hace esto no es un necesario para usar las señales desde Vala.

NB: actualmente el modificador de acceso public es la única opción posible (todas las señales se pueden conectar y emitir desde cualquier código).

Nota: desde abril de 2010 las señales se pueden anotar con una combinación de «flags»:

    [Signal (action=true, detailed=true, run=true, no_recurse=true, no_hooks=true)]
    public signal void sig_1 ();

Propiedades

Una buena práctica en la programación orientada a objetos es ocultar los detalles de la implementación de las clases creadas (principio de ocultación de información (enlace en inglés)), esto da la posibilidad de modificar su interior sin romper la API publica. Una forma de llevarlo a la práctica es hacer los campos privados y proporcionar métodos de acceso para obtener y modificar sus valores («getters» y «setters»).

Según la programación en Java esto se podría hacer así:

class Person : Object {
    private int age = 32;

    public int get_age() {
        return this.age;
    }

    public void set_age(int age) {
        this.age = age;
    }
}

Esto funcionaría, pero Vala puede hacerlo mejor. El problema es que trabajar con estos métodos puede resultar incómodo. Por ejemplo si se quisiera incrementar la edad de una persona un año:

var alice = new Person();
alice.set_age(alice.get_age() + 1);

Aquí es donde las propiedades entran en juego:

class Person : Object {
    private int _age = 32;  // underscore prefix to avoid name clash with property

    /* Property */
    public int age {
        get { return _age; }
        set { _age = value; }
    }
}

Esta sintaxis debería ser familiar para los programadores de C#. Una propiedad tiene bloques get y set para obtener y modificar su valor. value es una palabra reservada que representa el nuevo valor que se asignará a la propiedad.

Así se puede acceder a la propiedad como si se tratase de un campo público. Pero por detrás se estará ejecutando el código de los bloques get y set.

var alice = new Person();
alice.age = alice.age + 1;  // or even shorter:
alice.age++;

Si sólo se realiza la implementación estándar, cómo se ha visto antes, se puede escribir la propiedad incluso más brevemente:

class Person : Object {
    /* Property with standard getter and setter and default value */
    public int age { get; set; default = 32; }
}

Con las propiedades se puede modificar el trabajo interno de las clases sin modificar la API pública. Por ejemplo:

static int current_year = 2525;

class Person : Object {
    private int year_of_birth = 2493;

    public int age {
        get { return current_year - year_of_birth; }
        set { year_of_birth = current_year - value; }
    }
}

Esta vez la edad se calcula al vuelo a partir del año de nacimiento (year_of_birth). Como se puede observar, se puede realizar algo más que un simple acceso a una variable o una asignación dentro de los bloques get y set. Se podrían hacer incluso accesos a bases de datos, registros de actividad, actualizaciones de caché, etc.

Si se quiere hacer que una propiedad sea de sólo lectura para los usuarios de una clase, se debe declarar el setter como privado:

    public int age { get; private set; default = 32; }

O, alternativamente, se puede quitar el bloque set:

class Person : Object {
    private int _age = 32;

    public int age {
        get { return _age; }
    }
}

Las propiedades pueden tener, además de su nombre, una descripción breve (llamada nick) y una descripción (llamada blurb). Estas pueden anotarse con un atributo especial:

    [Description(nick = "age in years", blurb = "This is the person's age in years")]
    public int age { get; set; default = 32; }

Las propiedades y sus descripciones adicionales pueden consultarse en tiempo de ejecución. Algunos programas, como Glade, el diseñador de interfaces gráficas de usuario, pueden hacer uso de esta información. En este sentido, Glade puede presentar descripciones legibles para los widgets GTK+.

Cada instancia de una clase derivada de GLib.Object tiene una señal llamada notify. Esta señal se emite cada vez que una propiedad de dicho objeto cambia. Se puede, por tanto, conectar esta señal si se necesita obtener notificaciones de cambios en general:

obj.notify.connect((s, p) => {
    stdout.printf("Property '%s' has changed!\n", p.name);
});

s es la instancia a la que pertenece la señal (obj en este ejemplo), p es la información de la propiedad, del tipo ParamSpec, para la propiedad modificada. Si sólo interesa obtener notificaciones de las modificaciones de una propiedad, se puede emplear esta sintaxis:

alice.notify["age"].connect((s, p) => {
    stdout.printf("age has changed\n");
});

Se observa que en este caso se debe usar la representación como cadena de caracteres del nombre propiedad, donde los subrayados se sustituyen por guiones: nombre_de_propiedad pasará a ser nombre-de-propiedad en dicha representación, que sigue el convenio de nombres de GObject.

Las notificaciones de modificación se pueden desactivar con una etiqueta del atributo CCode situada inmediatamente antes de la declaración de la propiedad:

public class MyObject : Object {
    [CCode(notify = false)]
    // notify signal is NOT emitted upon changes in the property
    public int without_notification { get; set; }
    // notify signal is emitted upon changes in the property
    public int with_notification { get; set; }
}

Existe otro tipo de propiedades llamadas propiedades de construcción que se describen más adelante, en la sección sobre la construcción al estilo GObject.

Nota: en caso de que la propiedad sea del tipo struct, para que el valor de la propiedad pueda cogerse con Object.get(), se debe declarar la variable como en el siguiente ejemplo:

struct Color
{
    public uint32 argb;

    public Color() { argb = 0x12345678; }
}

class Shape: GLib.Object
{
    public Color c { get; set; default = Color(); }
}

int main()
{
    Color? c = null;
    Shape s = new Shape();
    s.get("c", out c);
}

De esta manera, c es una referencia en lugar de una instancia de Color en la pila. Lo que se pasa en s.get() es Color ** en lugar de Color *.

Herencia

En Vala, una clase puede derivarse de una o de ninguna clase. En la práctica suele ser una pero, a pesar de ello, no hay herencia implícita como la hay en otros lenguajes, como Java.

Cuando se define una clase que hereda de otra, se crea una relación entre las clases donde las instancias de la subclase son también instancias de la superclase. Esto significa que las operaciones sobre instancias de la superclase son también aplicables a las instancias de la subclase. Así, en cualquier lugar en que se requiera una instancia de la superclase, se puede proporcionar una instancia de la subclase.

Al escribir la definición de una clase, es posible tener control preciso sobre quién puede acceder a los métodos y los datos del objeto. El siguiente ejemplo muestra algunas de estas opciones:

class SuperClass : GLib.Object {

    private int data;

    public SuperClass(int data) {
        this.data = data;
    }

    protected void protected_method() {
    }

    public static void public_static_method() {
    }
}

class SubClass : SuperClass {

    public SubClass() {
        base(10);
    }
}

data es miembro de las instancias de SuperClass. Habrá un miembro de este tipo en cada instancia de SuperClass y, al estar declarado como privado, sólo es accesible desde el código que forma parte de SuperClass.

protected_method es una método de las instancias de SuperClass. Este método sólo se puede ejecutar sobre las instancias de SuperClass o alguna de sus subclases, y únicamente desde el código que pertenece a SuperClass o alguna de sus subclases (esta última regla es resultado del modificador protected).

public_static_method tiene dos modificadores. El modificador static significa que este método puede llamarse sin poseer una instancia de SuperClase o de alguna de sus subclases. Como resultado, este método no tiene acceso a la referencia this cuando se ejecuta. El modificador public indica que el método se puede llamar desde cualquier fragmento de código, independientemente de su relación con SuperClass o sus subclases.

Dadas estas definiciones, una instancia de SubClass contendrá los tres miembros de SuperClass, pero sólo podrá acceder a los miembros no privados. El código externo sólo podrá acceder al método público.

Con la referencia base, el constructor de una subclase puede enlazar con el constructor de su clase base.

Clases abstractas

Existe otro modificador para los métodos, llamado abstract. Este modificador permite describir un método que no se implementa realmente en la clase. En lugar de ello, el método debe implementarse en las subclases antes de poderse llamar. Esto permite que se definan operaciones a las que se pueda acceder desde todas las instancias de un tipo, asegurando a la vez que cada tipo específico proporcione su propia versión de la funcionalidad.

Una clase que contenga un método abstracto debe declararse a su vez como abstract. El resultado de esto es prevenir cualquier instanciación de este tipo.

public abstract class Animal : Object {

    public void eat() {
        stdout.printf("*chomp chomp*\n");
    }

    public abstract void say_hello();
}

public class Tiger : Animal {

    public override void say_hello() {
        stdout.printf("*roar*\n");
    }
}

public class Duck : Animal {

    public override void say_hello() {
        stdout.printf("*quack*\n");
    }
}

La implementación de un método abstracto debe marcarse con override. Las propiedades también pueden ser abstractas.

Interfaces / Mixins

Una clase en Vala puede implementar un número arbitrario de interfaces. Cada interfaz es un tipo, como una clase, pero no se puede instanciar. Implementando una o más interfaces, una clase puede declarar que sus instancias son también instancias de la interfaz, y por tanto podrán usarse en cualquier situación en la que se espere una instancia de dicha interfaz.

El proceso para implementar una interfaz es el mismo que para heredar de clases abstractas con métodos, para que la clase se pueda usar, debe proporcionar implementaciones para todos los métodos descritos pero no implementados.

A continuación se presenta una definición simple de una interfaz:

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

Este código describe una interfaz "ITest" que requiere GLib.Object como padre y contiene dos miembros. "data_1" es una propiedad, como las descritas anteriormente, excepto que aquí se ha declarado como abstract. Vala no implementará esta propiedad, sino que requerirá que las clases que implementen la interfaz tengan una propiedad llamada "data_1" que tenga los métodos de acceso get y set (es obligatorio que se defina como abstracta, ya que las interfaces no pueden tener datos como miembros propios). El segundo miembro "method_1" es un método. Aquí se declara que este método debe ser implementado por las clases que implementen "ITest".

La implementación completa más sencilla de esta interfaz es:

public class Test1 : GLib.Object, ITest {
    public int data_1 { get; set; }
    public void method_1() {
    }
}

Y se puede usar de la siguiente forma:

var t = new Test1();
t.method_1();

ITest i = t;
i.method_1();

Las interfaces en Vala no pueden heredar de otras interfaces, pero pueden declararlas como prerrequisitos, esta técnica es más o menos equivalente. Por ejemplo, se podría querer que cada clase que implemente una interfaz "List" debe implementar también una interfaz "Collection". La sintaxis para hacerlo es exactamente la misma que para describir la implementación de una interfaz en las clases:

public interface List : Collection {
}

Esta definición de "List" no se puede implementar en una clase sin que se implemente también "Collection", y por tanto Vala fuerza un estilo de declaración para las clases que quieran implementar "List", en el que todas las interfaces deben describirse:

public class ListClass : GLib.Object, Collection, List {
}

Las interfaces Vala también pueden tener una clase como prerrequisito. Si se da el nombre de una clase en la lista de prerrequisitos, la interfaz sólo se podrá implementar en clases que deriven de la clase listada. Esto se suele usar para asegurar que una instancia de una interfaz es también una subclase de GLib.Object, y por tanto que la interfaz se pueda usar, por ejemplo, como el tipo de una propiedad.

El hecho de que las interfaces no puedan heredar de otras interfaces es sobretodo una distinción técnica (en la práctica, Vala funciona como otros lenguajes en este aspecto, pero con la característica adicional de las clases prerrequisito).

Existe otra diferencia importante entre las interfaces Vala y las de Java/C#: las de Vala pueden tener métodos no abstractos. Realmente, Vala permite implementaciones de métodos en las interfaces, de aquí que se requiera que los métodos abstractos sean declarados como abstract. Debido a este hecho, las interfaces de Vala pueden funcionar como «mixins». Es una forma restringida de herencia múltiple.

Polimorfismo

El término polimorfismo describe la manera en la que un mismo objeto puede usarse como si fuera de más de un tipo distinto. Varias de las técnicas que ya se han descrito aquí sugieren cómo es posible hacer esto en Vala: una instancia de una clase puede usarse como una instancia de una superclase, o de cualquier interfaz que implemente, sin necesidad de conocer cual es realmente su tipo.

Una extension lógica de esta capacidad es permitir a un subtipo comportarse de forma distinta a su tipo padre cuando se referencia de la misma manera. Dado que este concepto es difícil de explicar, se presenta un ejemplo de lo que pasaría si no se pretende emplear esta propiedad directamente:

class SuperClass : GLib.Object {
    public void method_1() {
        stdout.printf("SuperClass.method_1()\n");
    }
}

class SubClass : SuperClass {
    public void method_1() {
        stdout.printf("SubClass.method_1()\n");
    }
}

Estas dos clases implementan un método llamado "method_1", y por tanto "SubClass" tiene dos métodos llamados "method_1", ya que hereda uno de "SuperClass". Cada uno de estos métodos puede llamarse de la siguiente manera:

SubClass o1 = new SubClass();
o1.method_1();
SuperClass o2 = o1;
o2.method_1();

En el código anterior se llama realmente a dos métodos distintos. En la segunda línea se toma a "o1" por un objeto del tipo "SubClass" y se llama su versión del método. En la cuarta línea se interpreta que "o2" es un objeto del tipo "SuperClass" y se llama a la versión del método correspondiente a dicha clase.

El problema que expone este ejemplo, es que cualquier código que referencia a "SuperClass" llamará a métodos descritos en esta clase, incluso si el tipo real del objeto es "SubClass". La técnica empleada para cambiar este comportamiento es el uso de métodos virtuales. Usándola, el ejemplo anterior se reescribiría como:

class SuperClass : GLib.Object {
    public virtual void method_1() {
        stdout.printf("SuperClass.method_1()\n");
    }
}

class SubClass : SuperClass {
    public override void method_1() {
        stdout.printf("SubClass.method_1()\n");
    }
}

Cuando este código se usa de la misma manera que antes, el "method_1" de "SubClass" se llamará dos veces. Esto es así porque se le dice al sistema que "method_1" es un método virtual, es decir que si se sobreescribe en una subclase, la nueva versión será la que se ejecute sobre las instancias de esa subclase, sin importar lo que se sepa en el momento de la llamada.

Probablemente, esta distinción sea familiar a los programadores de algunos lenguajes, tales como C++, pero es el comportamiento contrario al de lenguajes del estilo de Java, en los que se deben seguir ciertos pasos para evitar que un método sea virtual.

Puede que en este punto el lector haya reconocido que cuando un método se declara como abstract también debe ser virtual. De no ser así no sería posible ejecutar este método dada una instancia que aparentemente es del tipo en el que se ha declarado. Cuando se implementa un método abstracto en una subclase, se debe elegir si se declara la implementación como override (sobreescritura), aclarando así la naturaleza virtual del método y permitiendo que los subtipos hagan mismo.

También es posible implementar los métodos de una interfaz de manera que las subclases puedan cambiar su implementación. El proceso en este caso consiste en declarar la implementación incial del método como virtual, y entonces las subclases pueden sobreescribirlo.

Cuando se escribe una clase, es habitual querer usar funcionalidad heredada de una clase padre. Esto se complica cuando el nombre del método se usa más de una vez en el árbol de herencias de la clase que se está escribiendo. Para estos casos Vala proporciona la palabra reservada base. El caso más común de su uso se da cuando se ha sobreescrito un método para obtener alguna funcionalidad adicional, pero se sigue necesitando llamar al método de la clase padre. El siguiente ejemplo muestra este caso:

public override void method_name() {
    base.method_name();
    extra_task();
}

Finalmente, Vala también permite que las propiedades sean virtuales:

class SuperClass : GLib.Object {
    public virtual string prop_1 {
        get {
            return "SuperClass.prop_1";
        }
    }
}

class SubClass : SuperClass {
    public override string prop_1 {
        get {
            return "SubClass.prop_1";
        }
    }

Ocultación de métodos

Usando el modificador new se puede ocultar un método heredado con un nuevo método con el mismo nombre. El nuevo método puede tener un prototipo distinto. La ocultación de métodos no debe confundirse con la sobreescritura, ya que la ocultación de métodos no tiene un comportamiento polimórfico.

class Foo : Object {
    public void my_method() { }
}

class Bar : Foo {
    public new void my_method() { }
}

Se puede seguir llamando al método original mediante la promoción a la clase base o interfaz de la que se ha heredado.

void main() {
    var bar = new Bar();
    bar.my_method();
    (bar as Foo).my_method();
}

Información de tipos en tiempo de ejecución

Dado que las clases Vala se registran en tiempo de ejecución y cada instancia lleva consigo la información de su tipo, se puede comprobar el tipo de un objeto empleando el operador is:

bool b = object is SomeTypeName;

También se puede obtener la información del tipo de instancias de Object con el método get_type():

Type type = object.get_type();
stdout.printf("%s\n", type.name());

Con el operador typeof() se puede obtener información de un tipo directamente. A partir de esta información de tipo, se pueden crear nuevas instancias escribiendo Object.new():

Type type = typeof(Foo);
Foo foo = (Foo) Object.new(type);

El constructor al que se llamará con esta acción es el bloque construct {}, que se describirá en la sección sobre construcción con el estilo de GObject.

Promoción de tipos dinámicos

Para la promoción dinámica de una variable a TipoObjetivo, se hace uso de la expresión as TipoObjetivo tras el nombre de la variable que se quiere promocionar. Vala realizará una comprobación para asegurar que la promoción es razonable (si la promoción no es posible, se devolverá null). Sin embargo, esto requiere que tanto el tipo fuente como el tipo objetivo sean tipos referenciados.

Por ejemplo,

Button b = widget as Button;

Si por algún motivo la clase de la instancia widget no es Button o una de sus subclases o no implementa la interfaz Button, b será null. Esta forma de promoción es equivalente a:

Button b = (widget is Button) ? (Button) widget : null;

Genericidad

Vala incluye un sistema de genericidad en tiempo de ejecución por medio del cual una instancia concreta de una clase se puede restringir a un tipo concreto o un conjunto de tipos, escogidos en el momento de su construcción. Esta restricción se usa generalmente para requerir que los datos almacenados en el objeto deban ser de un tipo concreto, por ejemplo, para implementar una lista de objetos de cierto tipo. En este ejemplo, Vala se asegurará de que solo objetos del tipo requerido se puedan añadir a la lista, y que al recuperarlos todos los objetos se promocionen a dicho tipo.

En Vala, la genericidad se maneja durante la ejecución. Cuando se define una clase que se puede restringir a un tipo, sigue existiendo sólo una clase, con cada instancia personalizada individualmente. En constraste con C++, donde se crea una nueva clase por cada restricción de tipo existente (el sistema de Vala, es similar al empleado por Java). Esto tiene varias consecuencias, la más importante es que los miembros estáticos se comparten por el tipo como un todo, sean cuales sean las restricciones para cada instancia. Otra consecuencia es que dada una clase y una subclase, un tipo genérico refinado por la subclase se puede usar como un tipo genérico refinado por la clase.

El siguiente fragmento de código muestra como se usa el sistema de genericidad para definir una clase envoltorio mínima:

public class Wrapper<G> : GLib.Object {
    private G data;

    public void set_data(G data) {
        this.data = data;
    }

    public G get_data() {
        return this.data;
    }
}

Esta clase Wrapper debe restringirse con un tipo para que se pueda instanciar (en este caso el tipo se identifica como G, y por tanto las instancias de esta clase contendrán un objeto del tipo G, y tendrán métodos para modificar ese objeto). (La razón de este ejemplo en concreto es proporcionar una explicación razonada de que actualmente una clase generica no puede hacer uso de las propiedades del tipo al que está restringida y por ello, en lugar de acceder directamente, esta clase tiene métodos set y get simples.)

Para instanciar esta clase, se debe escoger un tipo, por ejemplo el tipo string proporcionado por el lenguaje (en Vala no hay restricción sobre los tipos usados para la genericidad). A continuación se muestra un breve ejemplo del uso de la clase anteriormente definida:

var wrapper = new Wrapper<string>();
wrapper.set_data("test");
var data = wrapper.get_data();

Como se puede observar, cuando los datos se recuperan del envoltorio, se asignan a un identificador sin tipo explícito. Esto es posible porque Vala sabe que tipo de objetos se almacenan en cada instancia de Wrapper.

El hecho de que Vala no cree varias clases a partir de la definición generica, significa que se puede escribir lo siguiente:

class TestClass : GLib.Object {
}

void accept_object_wrapper(Wrapper<Glib.Object> w) {
}

...
var test_wrapper = new Wrapper<TestClass>();
accept_object_wrapper(test_wrapper);
...

Dado que todas las instancias de "TestClass" son también Object, el método accept_object_wrapper aceptará sin problemas el objeto que se le pasa, y lo tratará como el objeto al que envuelve como si fuera una instancia de GLib.Object.

Construcción al estilo de GObject

Como se ha dicho en el punto anterior, Vala soporta un esquema de construcción alternativo que es ligeramente diferente al descrito anteriormente, pero parecido a la manera en que funciona la construcción en GObject. El método preferible depende de si el usuario viene del lado de GObject o del lado de Java o C#. El esquema de construcción estilo GObject introduce algunos elementos sintácticos nuevos: propiedades establecidas en la construcción (construct properties), la llamada especial Object(...) y un bloque construct. Echemos un vistazo a cómo funciona esto:

public class Person : Object {

    /* Construction properties */
    public string name { get; construct; }
    public int age { get; construct set; }

    public Person(string name) {
        Object(name: name);
    }

    public Person.with_age(string name, int years) {
        Object(name: name, age: years);
    }

    construct {
        // do anything else
        stdout.printf("Welcome %s\n", this.name);
    }
}

Con el esquema de construcción estilo GObject, cada método de construcción solo contiene una llamada Object(...) para asignar valores a las propiedades establecidas en la construcción. La llamada Object(...) toma un numero variable de argumentos de la forma propiedad: valor. Estas propiedades se deben declarar como propiedades construct. Se ajustarán a los valores indicados y luego se llamará a todos los bloques construct {} en la jerarquía de objetos, desde GLib.Object hasta el de la clase creada.

Se garantiza que el bloque construct se llama cuando se crea una instancia de esta clase, incluso si se crea como un subtipo. Sin tener ningún parámetro, ni un valor de retorno. Dentro de este bloque se puede llamar a otros métodos y establecer las variables miembro cuando sea necesario.

Las propiedades de construcción se definen como get y set, y por lo tanto se puede ejecutar código arbitrario en su asignación. Si se necesita que la inicialización esté basada en una única propiedad de construcción, es posible escribir un bloque construct personalizado para esta propiedad, que se ejecutará inmediatamente en su asignación, y antes de cualquier código de construcción.

Si una propiedad de construcción se declara sin set se denomina propiedad construct only, lo que significa que sólo se puede asignar en la construcción, pero no después. En el ejemplo anterior name es una propiedad de este tipo.

A continuación se muestra un resumen de los diferentes tipos de propiedades junto a la nomenclatura que se suele encontrar en la documentación de librerías basadas en GObject:

    public int a { get; private set; }    // Read
    public int b { private get; set; }    // Write
    public int c { get; set; }            // Read / Write
    public int d { get; set construct; }  // Read / Write / Construct
    public int e { get; construct; }      // Read / Write-Construct-Only

En algunos casos es posible que también se quiera realizar algún tipo de acción, no cuando las instancias de una clase se crean, sino cuando el sistema GObject crea la propia en si. En la terminología de GObject estamos hablando de un fragmento de código que se ejecuta dentro de la función class_init para la clase en cuestión. En Java esto se conoce como bloques inicializadores estáticos. En Vala es así:

    /* This snippet of code is run when the class
     * is registered with the type system */
    static construct {
      ...
    }

2024-10-23 11:37