Tipos de datos

Antes de entrar en los distintos tipos de datos propios de las variables, hay que señalar que también podemos definir constantes.

Realmente las constantes no son propiamente un tipo de dato ya que una constante define un valor de cualquier tipo de dato que se especifica como fijo para que no se modifique durante la ejecución del programa.

Definimos una constante anteponiendo la palabra reservada "const" seguida del tipo de constante que se define. El convenio para constantes es TODO_EN_MAYUSCULAS. Por ejemplo:

const NUMPI:double = 3.1415926
 
init
	stdout.printf ("%.7f\n", NUMPI)

En cuanto a las variables, existen distintos tipos de variables y cada uno de ellos admite un distinto tipo de datos (texto, números, booleanos…).

Habitualmente el tipo de datos se define con el nombre de la variable seguido de dos puntos y el tipo concreto. Una variable es un identificador que identifica una instancia de un tipo de datos. Se declara una variable con su etiqueta, dos puntos y su tipo de datos (por ejemplo, a:string, o b:int).

var especifica que lo que sigue es la declaración de una variable. Genie tiene un mecanismo llamado 'Inferencia de Tipo', por el que una variable local puede ser definida usando var en lugar de especificar un tipo, siempre y cuando su tipo sea inequívoco. Esto es, cuando se sobreentiende el tipo de datos que maneja una variable podemos declararla directamente sin especificar el tipo de datos que contiene.

Podemos inicializar la variable al declararla aunque en general se considera que es una buena práctica inicializar la variable asignándole un valor al mismo tiempo (a:string = "texto" o b:int = 0). Esto evita el error si se intenta leer una variable no inicializada (como veremos en Alcance de las variables).

Vemos todas las posibilidades comentadas en este ejemplo:

init
	n:int  // la variable n es un entero sin valor definido
	n = 3  // ahora vale 3 (pero luego puede valer 5, que para eso es una variable)
	print "%d",n
 
	numero:int = 5  // la variable numero es un entero con valor 5
	print "%d",numero
 
	var coche = "Skoda"
	// está claro que es una cadena o string (var coche:string = "Skoda")
	var velocidad = 100
	// está claro que es un número entero (var velocidad:int = 100)
	print "Mi %s corre a más de %d km/h.",coche, velocidad
	// vemos que no imprime bien los acentos; lo arreglamos:
	stdout.printf( "Mi %s corre a más de %d km/h.",coche, velocidad )
 
	var 
		nombre = "Pepe"      // string
		apellido= "García"   // string
		edad = 44            // int
	stdout.printf( "\nMe llamo %s %s y tengo %d años.",nombre, apellido, edad )
	/* con \n empezamos en una línea nueva puesto que
	con stdout.printf() seguimos en la misma linea */

Para conocer el estilo de formato de cada tipo de dato, puedes visitar printf o printf format string.

Texto

El texto se almacena como una cadena de caracteres o un string y se delimita entre comillas dobles.

El texto que ocupa varias líneas puede delimitarse entre tres comillas dobles. Y un único carácter entre comillas simples.

nombre:string = "Manolo"
 
menu:string = """
Primer plato
Segundo plato
Postre"""
 
caracter:char = 't'

Genie proporciona varias maneras de devolver el valor de una variable de texto, cadena o string.

Se pueden concatenar cadenas con el operador +

init
	lenguaje:string = "Genie"
	print( "Aprendo a programar con " + lenguaje )

Se puede utilizar la plantilla %s

init
	lenguaje:string = "Genie"
	print( "Programo con %s", lenguaje )
	print( "Python y %s me gustan mucho, pero prefiero %s.", lenguaje, lenguaje )

Se puede utilizar el prefijo @ antes de la cadena y el prefijo $ antes de la variable:

init
	nombre:string = "Rita"
	print( @"$nombre, $nombre, lo que se da no se quita." )

Para referirnos a un solo carácter usamos comillas simples, y puede ser:

  • :char, :uchar, un solo carácter
  • :unichar, carácter Unicode

Números

Podemos especificar el tipo de número que utilizamos:

  • :int para números enteros
  • :float para números decimales
  • :double para decimales grandes
init
	a:int = 1  // número entero
	b:float = 2 // número decimal
	c:float = 3 // número decimal que sacamos con 2 decimales
	d:double = 4 // números grandes, exponenciales
	print "%i, %f, %.2f, %e", a, b, c, d

Este código nos devuelve en consola:

1, 2.000000, 3.00, 4.000000e+00

También podemos especificar los bits del entero (y sus versiones sin signo con uint):

init
	a:int = 1
	b:int8 = 2
	c:int16 = 3
	d:int32 = 4
	e:int64 = 5
	print "%i, %i, %i, %i, %lli", a, b, c, d, e
 
	f:uint = 6
	g:uint8 = 7
	h:uint16 = 8
	i:uint32 = 9
	j:uint64 = 10
	print "%u, %u, %u, %u, %llu", f, g, h, i, j

En cuanto a los números con decimales, float y double, mientras que el primero ocupa en memoria 4 bytes el segundo ocupa 8 bytes. La principal diferencia entre ellos es la precisión del número. Un número double permite representar números con más precisión que un float.

Boleanos

Un booleano almacena true o false.

a:bool = true

Enumeraciones

Con enum creamos un tipo de enumeración. Se trata de una secuencia ordinal (enumerable) de valores en la que cada entrada equivale a un valor numérico (0, 1, 2, etc.). En principio empiezan desde cero, aunque también es posible asignar un valor entero específico a un indicador enum.

Para rescatar un valor enum utilizamos su nombre seguido de un punto y su identificador por lo que en el ejemplo de código MiEnum.Jueves da como resultado 3.

enum MiEnum
	DIA_LUNES
	DIA_MARTES
	DIA_MIERCOLES
	DIA_JUEVES
	DIA_VIERNES
init
	print("%d",MiEnum.Jueves)

null

Por defecto, Genie se asegura de que todos los puntos de referencia (tanto variables como funciones) apunten a objetos reales. Esto significa que no se puede asignar arbitrariamente null a una variable.

Para permitir que una referencia sea null se debe seguir el tipo de dato con un interrogante, por ejemplo: string?, con lo que estamos afirmando que esa variable puede ser nula y así evitamos errores de código.

Podemos utilizar este modificador tanto en tipos de parámetros como en tipos de retorno de funciones, y aunque puede resultar útil en fase de depuración para la comprobación del código, se aconseja su deshabilitación posterior.

def permite_nulls (param : string?) : string?
	return param

Estructrura (struct)

Las estructuras son un caso especial compuesto de valores de distinto tipo.

struct PlatoMenu
	orden : int
	tipo : string

Se puede inicializar de cualquiera de las siguientes maneras:

var menu = PlatoMenu()
 
var menu = PlatoMenu() {1, "sopa"}
 
var menu = PlatoMenu() {
	orden = 1,
	tipo = "sopa"    
}
 
menu: PlatoMenu = {1, "sopa"}

Podemos recuperar los datos así:

print("%d %s", menu.orden, menu.tipo)

Un ejemplo (no te preocupes si no entiendes parte del código, más adelante se verán las funciones):

// compila con valac nombre_archivo.gs
 
struct PlatoMenu
	orden : int
	tipo : string
 
def nuevo(menu_new: PlatoMenu) : int
	orden_nuevo:int = menu_new.orden + 1
	return orden_nuevo
 
init
	stdout.printf("Menú:\n")
 
	// asignamos valores a la estructura
	menu: PlatoMenu = {1, "Sopa"}
	stdout.printf("%d %s\n", menu.orden, menu.tipo)
 
	// cambiamos los valores utilizando una función
	orden_new:int = nuevo(menu)
	menu.tipo = "Salmón"
	stdout.printf("%d %s\n", orden_new, menu.tipo)
	menu_new :PlatoMenu = {orden_new, menu.tipo}
 
	// cambiamos otra vez los valores
	orden_new = nuevo(menu_new)
	menu.tipo = "Chocolate"
	stdout.printf("%d %s\n", orden_new, menu.tipo)

Conversión de variables

Tenemos disponibles distintos métodos para transformar el tipo de dato que contiene una variable.

Por ejemplo, podemos convertir un número a string añadiendo al final de la variable el método .to_string()

Al revés, de un string a un entero, con int.parse().

init
	num:int = 5
	print num.to_string()
 
	var i = 2 + 2
	print i.to_string()
 
	piso:string
	piso = "12"
	print "%i", int.parse(piso)

Y podemos convertir un double en entero así:

init
	num_dec: double = 12.345
	num_ent: int = (int)num_dec
	print "%d", num_ent  // imprime 12

Alcance de las variables

Como hemos dicho, Genie utiliza espacios en blanco para organizar el código en bloques. Esto hace que cada bloque sea fácil de identificar. Cuando declaramos una variable solo podemos acceder a ella en el bloque de código donde ha sido declarada.

Si intentamos acceder a esa variable fuera de ese bloque, el compilador nos mostrará un error.

Por eso dijimos antes que era una buena práctica inicializar la variable asignándole un valor al mismo tiempo; así evitamos un posible error si se intenta leer una variable no declarada en ese bloque.

Una ventaja de tener una variable sólo accesible en el bloque que se declara es que puede ser reutilizada en otro bloque sin un conflicto de nombres. Por ejemplo:

init
	if true   // bloque if 1
		a:string = "blanco"
		print "la variable a en el bloque 1 = " + a
	if true  // bloque if 2
		a:string = "negro"
		print "la variable a en el bloque 2 = " + a

Al ejecutar este código compilado, la consola devuelve:

la variable a en el bloque 1 = blanco
la variable a en el bloque 2 = negro

Si en el código anterior marcamos como comentario la linea de asignación de la variable 'a' en el segundo bloque, o incluso si pasa a ser solo una asignación (sin estar inicializada), entonces el compilador avisa del error (error: The name 'a' does not exist in the context of 'main'). Esto se debe a que la variable sólo existe en el bloque condicional 1, pero no es accesible en el bloque raíz (o principal) ni tampoco en el bloque condicional 2.

Códigos con error causado porque la variable no es accesible:

init
	if true   // bloque if 1
		a:string = "blanco"
		print "la variable a en el bloque 1 = " + a
	if true  // bloque if 2
		//a:string = "negro"
		print "la variable a en el bloque 2 = " + a
		// ERROR: la variable 'a' no es accesible en este bloque
init
	if true   // bloque if 1
		a:string = "blanco"
		print "la variable a en el bloque 1 = " + a
	if true  // bloque if 2
		a = "negro"
		// ERROR: la variable 'a' no está inicializada en este bloque
		print "la variable a en el bloque 2 = " + a

Para hacer que la variable esté disponible en el bloque 2 la podemos inicializar en el bloque principal:

init
	a:string = "" // variable inicializada en bloque principal
	if true   // bloque if 1
		a = "blanco"
		// variable inicializada y accesible, le podemos asignar un valor
		print "la variable a en el bloque 1 = " + a
	if true  // bloque if 2
		a = "negro"    // y aquí también le asignamos otro valor
		print "la variable a en el bloque 2 = " + a

En este código la variable 'a' se declara en el bloque principal y es accesible en cualquier sub-bloque.

Espero haberme explicado bien, porque el alcance de las variables a nivel de bloque es un concepto importante.

La ventaja de no tener nombres conflictivos es más clara cuando se aplica a las funciones:

init
	funcion_uno()
	funcion_dos()
 
def funcion_uno()
	valor_uno:int = 1
	valor_dos:int = 2
	resultado:int = valor_uno + valor_dos
	print "%i + %i = %i", valor_uno, valor_dos, resultado
 
def funcion_dos()
	valor_uno:int = 100
	valor_dos:int = 200
	resultado:int = valor_uno * valor_dos
	print "%i * %i = %i", valor_uno, valor_dos, resultado
1 + 2 = 3
100 * 200 = 20000

Aquí las variables de trabajo, como 'resultado', no chocan. Esto hace que sea más fácil escribir un bloque de código porque no es necesario realizar ninguna comprobación de que el identificador ya haya sido utilizado.

Generalmente los identificadores que están disponibles en todos los bloques del programa, de alcance o ámbito global, deben evitarse porque dificultan la comprobación del código. Los identificadores globales pueden introducir incertidumbre en el programa porque no está claro dónde se alteran sus valores.

Tipos de referencia

Básicamente, en Genie hay dos tipos de datos: tipos de valor (los vistos hasta ahora) y tipos de referencia, que se distinguen básicamente en la forma en que son procesados por el sistema.

Cuando trabajamos con un tipo de valor estamos creando un objeto del tipo especificado a través de un identificador. Un identificador se define por su nombre y su tipo, por ejemplo num: int significa un entero llamado num.

Mientras que un tipo de valor es copiado cuando se asigna a otro identificador (otra variable), en cambio en un tipo de referencia el nuevo identificador es simplemente una referencia o dirección que apunta al objeto, permitiendo el acceso indirecto a su contenido.

Los tipos de referencia son declarados como una clase, y como tal, normalmente se instancian utilizando el operador new seguido del tipo de objeto al que hace referencia, por ejemplo: var mi_objeto = new Object() crea un nuevo Objeto y mi_objeto hace referencia a él.

El sistema mantiene un registro del número de las referencias que siguen en uso con el fin de realizar una administración o gestión eficiente de la memoria.

Sin entrar mucho en el tema (para más detalles visita Vala's Memory Management Explained), esa administración de memoria en Genie se basa en el recuento automático de referencias, incrementándose en 1 por cada nueva referencia y disminuyendo en 1 cada vez que una variable de referencia sale de su alcance. Finalmente, si el recuento de referencias llega a 0, la memoria se libera del objeto.

Sin embargo, a veces involuntariamente podemos formar un ciclo de referencias cruzadas o doblemente vinculadas que mantienen “vivos” a los objetos aunque deberían quedar liberados (fuera de la memoria). Para romper ese bucle de referencia se puede utilizar el modificador weak (débil) para una de las referencias.

Otro modificador es unowned. Normalmente cuando creamos un objeto pasamos a la memoria una referencia al objeto, pero además también se registra en el propio objeto que esa referencia existe. Y si se crea otra referencia a ese mismo objeto, también queda registrada en el objeto. Como un objeto sabe cuántas referencias apuntan a él, puede ser eliminado cuando sea necesario.

Sin embargo, las referencias con el modificador unowned (sin propietario o sin dueño) no se registran en el objeto al que hacen referencia, con lo que el objeto puede ser eliminado a pesar de tener aún referencias a él. Y al contrario, la palabra clave owned puede usarse para recuperar una referencia que vuelve a estar registrada en el objeto.

Además, owned también se puede utilizar para transferir el propietario de un objeto, por ejemplo:

init
	var x = "Pepe"
 
	y : string = (owned) x  // transferencia de propietario de x a y
 
	print x // es null porque no tiene propietario
	print y // hereda el propietario de y (y su referencia al valor Pepe)

Además de este sistema automático de gestión de la memoria, que libera memoria cuando no hay referencias activas a una instancia, Genie admite la administración manual de la memoria creando punteros. Cuando se crea un puntero a una instancia se asume la responsabilidad (si no se hace, mejor dejar la gestión automática) de destruirla cuando ya no se necesita, con lo que se obtiene un mayor control sobre la memoria que se utiliza.

Sin embargo, esta funcionalidad no suele ser necesaria en los equipos informáticos actuales, suficientemente rápidos y con la suficiente memoria para manejar el recuento de referencias a pesar de pequeñas ineficiencias en el código. Quizá puede ser relevante recurrir a la administración de memoria manual en casos especiales en los que se desea optimizar una parte específica de un programa o cuando se utiliza una librería externa que no implementa el recuento de referencias para la administración de memoria (una librería no basada en GObject).

Para crear una instancia y recibir un puntero, se agrega el sufijo * a la declaración, por ejemplo mi_objeto : object* = new Object (y se libera la memoria con delete mi_objeto;).

Tipos genéricos

Por otra parte, y aunque puede resultar muy avanzado si estás empezando (te lo puedes saltar y volver más adelante), simplemente quédate con la idea de que Genie incluye un sistema (parámetros de tipo o definición de tipo genérico) mediante el cual una instancia particular de una clase puede ser restringida a un tipo particular en el momento de su construcción. Esta restricción se utiliza para exigir que los datos almacenados en el objeto deben ser de un tipo particular (y no de otros tipos), por ejemplo para implementar una lista de objetos de cierto tipo. De esta manera nos aseguramos que solo los objetos del tipo solicitado se podrán agregar a esa lista.

Para ello se utiliza la clase Wrapper con un tipo identificado como “G” (de Genérico) y las instancias de esta clase almacenarán objetos tipo “G” que como son genéricos luego pueden convertirse en cualquier tipo de dato (metafóricamente, son como las células madre de los organismos pluricelulares que tienen la capacidad de diferenciarse en diversos tipos de células especializadas).

En el momento de instanciar esta clase, debe elegirse un tipo concreto de dato. El siguiente ejemplo muestra una instancia de tipo string y la otra de tipo int sobre la misma clase Wrapper.

init
 
	var solo_string = new Wrapper of string // instancia a la clase Wrapper con tipo string
	solo_string.set_data ("Hola Mundo") // método set: aquí solo se admiten string
	var cadena = solo_string.get_data () // método get: no hace falta que le diga que es un string 
	print cadena
 
	var solo_int = new Wrapper of int // instancia a la clase Wrapper con tipo int
	solo_int.set_data (6) // método set: aquí solo se admiten int
	var numero = solo_int.get_data () // método get: ya sabe que esto es un int
	print "El entero es %d", numero
 
 
class Wrapper of G : Object // clase del contenedor de tipo genérico
	_data : G
 
	def set_data (data : G)
		_data = data
 
	def get_data () : G
		return _data

Fíjate que cuando los datos se recuperan de Wrapper, se asignan a variables sin tipo explícito (cadena y numero); no necesitamos escribir 'cadena:string' ni 'numero:int' porque Genie ya sabe qué tipo de datos son (en caso que fueran otro tipo de datos, el compilador valac avisaría del error).

🔝