Tipos de datos
Tabla de Contenidos
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).