5 Tipos básicos, variables y constantes
En este capítulo aprenderemos a declarar variables y constantes, a distinguir entre datos mutables e inmutables y a utilizar los tipos de datos básicos de Rust. A diferencia de muchos otros lenguajes, Rust considera la inmutabilidad como la opción por defecto, una característica que suele resultar sorprendente a los principiantes. También veremos los operadores más habituales.
5.1 Salida por pantalla
Antes de comenzar a estudiar las variables y los tipos básicos, mostraremos cómo escribir en la salida estándar (pantalla), ya que nos será útil en prácticamente todos los ejemplos de este y de los siguientes capítulos.
Las construcciones print! y println! son macros de Rust que permiten escribir texto y valores en la salida estándar. La diferencia entre ambas es que println! añade un salto de línea al final, mientras que print! no lo hace.
Una macro es un mecanismo que permite al compilador generar código a partir de una plantilla, proporcionando una sintaxis más flexible que la de una función convencional. En este curso no estudiaremos cómo definir nuestras propias macros, pero utilizaremos con frecuencia algunas de las que proporciona la biblioteca estándar.
Por convenio, los nombres de las macros terminan con un signo de exclamación (!), lo que permite distinguirlas fácilmente de las funciones.
fn main() {
print!("Hola ");
println!("mundo.");
println!("Adiós mundo.");
}
5.1.1 Formato de salida
Las macros print! y println! pueden recibir uno o varios argumentos. El primero es siempre una cadena de formato, que indica cómo debe mostrarse el texto. Los argumentos restantes son los valores que se insertarán en esa cadena.
Dentro de la cadena de formato, cada marcador {} se sustituye por el valor del argumento correspondiente. El primer marcador {} se reemplaza por el segundo argumento de la macro, el segundo marcador (si existe) por el tercer argumento, y así sucesivamente.
fn main() {
let x = 5.23;
println!("x: {}", x);
let a = 2;
let b = 3;
println!("a:{} b:{} a+b:{}", a, b, a + b);
}En la primera llamada a
println!, el único marcador{}se sustituye por el valor de la variablex.En la segunda llamada aparecen tres marcadores. El primero se sustituye por el valor de
a, el segundo por el deby el tercero por el resultado de la expresióna + b.
Esta es la sintaxis general. Rust permite una sintaxis abreviada, que consiste en escribir una variable directamente dentro de las llaves,
println!("{x}"); // Sintaxis abreviada.Aunque podremos encontrar documentación y programas que usan la sintaxis abreviada, en estos apuntes utilizaremos siempre la forma general
println!("{}", x); // Sintaxis generalLa preferimos porque funciona en todos los casos y resulta más uniforme. Por el contrario, la forma abreviada solo puede utilizarse cuando dentro de las llaves solo aparece el nombre de una variable (o una constante). En cuanto queremos imprimir una expresión, debemos utilizar la forma general.
Por ejemplo:
println!("{}", a + b);
println!("{}", v[0]);
println!("{}", persona.nombre);
println!("{}", texto.len());
println!("{}", *r);En todos estos casos la forma abreviada no sería válida.
fn main() {
let s = "hola-hola";
let v = ["hola", "mundo"];
println!("{}", s); // Forma general. ok.
println!("{s}"); // Forma abreviada. Aquí, ok.
//println!("{v[0]}"); // ¡Error! La forma abreviada no es válida
println!("{}", v[0]); // Forma general. ok.
}5.2 Tipos de datos básicos
Las variables se declaran mediante la palabra reservada let. Lo más idiomático es omitir el tipo, ya que Rust puede deducirlo a partir del valor inicial.
En programación se dice que un fragmento de código es idiomático cuando sigue los usos y convenciones habituales del lenguaje.
let nombre = "URJC";
let creacion = 1996;
let publica = true;También es correcto declarar los tipos de manera explícita, pero se considera redundante. La Tabla 5.1 resume los tipos básicos de Rust.
let nombre: &str = "URJC";
let creacion: i32 = 1996;
let publica: bool = true;Habrá ocasiones donde sí es necesario indicar el tipo. Por ejemplo, para los literales de coma flotante, Rust infiere por defecto un número real de doble precisión (f64). Si deseamos un real de simple precisión (f32), deberemos indicarlo de forma explícita.
let radio: f32 = 3.5;También puede ser conveniente hacerlo para mejorar la legibilidad del código o para documentar una decisión de diseño:
let puerto: u16 = 8080;
let temperatura: f32 = 21.5;Más adelante veremos otras situaciones en las que el compilador no dispone de suficiente información para deducir el tipo y es obligatorio especificarlo explícitamente.
Los tipos enteros y de coma flotante también admiten anotaciones explícitas en los literales:
let edad = 54u8;
let poblacion = 8_000_000u64;
let radio = 3.5f32;5.2.1 El tipo &str
En casi cualquier lenguaje existe un único tipo de datos para representar cadenas de caracteres. En Rust, sin embargo, existen varios. Podemos considerar &str como un tipo básico, en el sentido de que es muy habitual. Es una referencia a una cadena ya existente y lo usaremos para:
Cadenas literales. Esto es, cadenas escritas directamente en el código fuente. Por ejemplo
"Hola".Parámetros que solo necesiten leer una cadena.
También existe str, que representa una cadena de caracteres, pero no puede utilizarse como valor independiente y es raro usarlo de forma directa.
En la Capítulo 15 veremos el tipo String (con mayúscula), que es diferente del tipo str (con minúscula). Un String es un objeto que contiene y gestiona una cadena de caracteres.
En resumen, usaremos
&strpara acceder a una cadena que ya existe.Stringpara crear, almacenar y modificar una cadena.
5.2.2 El tipo unitario ()
Rust dispone de un tipo especial denominado tipo unitario (unit type), cuyo único valor posible es el valor unitario. Ambos se representan de la misma forma, con la tupla vacía: ().
let x = ();Aunque pueda parecer un concepto peculiar, no es muy distinto de otros tipos básicos. Por ejemplo, un valor de tipo i32 puede tomar cualquiera de los 2³² valores enteros representables mediante 32 bits. Un valor de tipo bool puede tomar únicamente dos valores posibles: true y false. Por su parte, el tipo unitario () solo admite un único valor: ().
El tipo () se utiliza para representar la ausencia de información útil. Como trataremos posteriormente, una función que no devuelve ningún resultado devuelve implícitamente un valor de tipo ():
fn saludar() {
println!("Hola");
}equivale a:
fn saludar() -> () {
println!("Hola");
}Además, cuando una expresión termina en punto y coma, su valor se descarta y la expresión pasa a producir el valor ().
5.2.3 Resumen de tipos básicos
| Tipo | Descripción | Ejemplo |
|---|---|---|
bool |
Valor lógico | true, false |
char |
Carácter Unicode | 'A', 'ñ', '€' |
&str |
Cadena de texto inmutable | "Hola" |
i8, i16, i32, i64, i128 |
Enteros con signo | -42 |
u8, u16, u32, u64, u128 |
Enteros sin signo | 42 |
isize |
Entero con signo del tamaño de un puntero | -42 |
usize |
Entero sin signo del tamaño de un puntero | 42 |
f32 |
Real de simple precisión | 3.14 |
f64 |
Real de doble precisión | 3.14 |
() |
Tipo unitario | () |
5.3 Mutables e inmutables
Comenzaremos por repasar dos ideas previas:
Tiempo de compilación (compile time): fase en la que el compilador analiza y traduce el código fuente a código máquina o ejecutable.
Tiempo de ejecución (run time): fase en la que el programa ya compilado se está ejecutando.
La mayoría de lenguajes de programación incluyen constantes y variables. Mientras que Rust distingue tres categorías: constantes, variables inmutables y variables mutables.
Por defecto, las variables son inmutables. Esto es, una vez que reciben un valor, este no puede cambiar durante la ejecución del programa. En esto coinciden con las constantes. La diferencia entre una variable inmutable y una constante es:
La constante tiene que inicializarse con una expresión cuyo valor puede determinarse durante la compilación.
La variable inmutable puede recibir su valor en tiempo de ejecución. Cada vez que se ejecute el programa puede tener un valor inicial distinto: aleatorio, leído de un fichero, consultado al usuario, etc. Aunque una vez asignado, no puede cambiar.
El siguiente ejemplo intenta cambiar una variable inmutable, y por tanto genera un error de compilación.
fn main() {
let x = 5;
println!("x:{}",x);
x = 6; // ¡¡Error!!
println!("x:{}",x);
}Para que una variable sea mutable, como la de prácticamente cualquier otro lenguaje de programación, debe declararse con mut como en el siguiente ejemplo:
fn main() {
let mut x = 5;
println!("x:{}",x);
x = 6;
println!("x:{}",x);
}5.4 Constantes
Las constantes se definen con la palabra reservada const.
Su valor debe conocerse en tiempo de compilación.
Por convención, se escriben en MAYÚSCULAS.
Son inmutables. Su valor no puede cambiar.
Deben indicar explícitamente el tipo.
Puede tratarse de un valor literal
const MAX_CLIENTES: u32 = 100;o de una expresión cuyo valor pueda calcularse durante la compilación.
const TAM: usize = 1024 * 1024;Ejemplo:
const PUERTO_HTTP: u16 = 80;
const TIMEOUT_SEGUNDOS: u64 = 30;
fn main() {
println!("Puerto: {}", PUERTO_HTTP);
println!("Timeout: {}", TIMEOUT_SEGUNDOS);
}Más adelante estudiaremos otros conceptos relacionados como el shadowing, las variables estáticas (static) y las funciones que pueden evaluarse en tiempo de compilación (const fn).
5.5 Operadores
Tal y como muestra la Tabla 5.2, los operadores básicos de Rust coinciden con los del lenguaje C y sus herederos, como C++, C#, Java, JavaScript, Go, Kotlin, etc.
| Operación | Operador |
|---|---|
| Suma | + |
| Resta | - |
| Multiplicación | * |
| División | / |
| Resto | % |
| Igualdad | == |
| Desigualdad | != |
| Menor que | < |
| Menor o igual que | <= |
| Mayor que | > |
| Mayor o igual que | >= |
| AND lógico | && |
| OR lógico | || |
| NOT lógico | ! |
Los operadores de asignación compuesta (+=, -=, *=, etc.) también están disponibles.
5.6 Resumen
En este capítulo hemos presentado los elementos básicos con los que se construyen los programas en Rust.
Las ideas principales son las siguientes:
- Las variables se declaran con
lety, por defecto, son inmutables. - Una variable puede hacerse mutable mediante la palabra clave
mut. - Rust dispone de un sistema de inferencia de tipos, aunque también es posible indicar el tipo explícitamente cuando sea conveniente.
- Los tipos básicos incluyen enteros, números en coma flotante, valores booleanos y caracteres.
- Las constantes se declaran con
const, deben tener un tipo explícito y su valor debe conocerse en tiempo de compilación. - Rust proporciona los operadores aritméticos, relacionales y lógicos habituales en la mayoría de los lenguajes de programación.
Estos conceptos constituyen la base sobre la que se apoyarán los capítulos siguientes, donde comenzaremos a trabajar con tipos más complejos y con las estructuras fundamentales del lenguaje.