Rust

Rust

Tutorial Rust: Variables y tipos básicos

Aprende a declarar variables, tipos primitivos, inferencia y casting en Rust para escribir código seguro y eficiente paso a paso.

Aprende Rust y certifícate

let, mut y const

En Rust, la forma en que declaramos y utilizamos variables es fundamental para entender el lenguaje. A diferencia de otros lenguajes de programación, Rust adopta un enfoque único hacia las variables que prioriza la seguridad y la prevención de errores.

Declaración de variables con let

Para declarar una variable en Rust, utilizamos la palabra clave let. Esta es la forma básica de introducir un nuevo valor en nuestro programa:

let x = 5;

En este ejemplo, hemos creado una variable llamada x con el valor 5. Algo importante que debes saber es que, por defecto, todas las variables en Rust son inmutables. Esto significa que una vez que asignamos un valor a una variable, no podemos cambiarlo.

let x = 5;
x = 10; // ¡Error! No podemos cambiar el valor de x

Si intentamos ejecutar este código, el compilador nos mostrará un error similar a:

error[E0384]: cannot assign twice to immutable variable `x`

Esta inmutabilidad por defecto es una característica distintiva de Rust y forma parte de su filosofía de seguridad. Al hacer que las variables sean inmutables por defecto, Rust nos ayuda a evitar cambios accidentales que podrían introducir errores en nuestro código.

Variables mutables con mut

Aunque la inmutabilidad es el comportamiento predeterminado, a veces necesitamos variables que puedan cambiar su valor. Para esto, Rust nos proporciona la palabra clave mut:

let mut y = 5;
y = 10; // Ahora sí funciona
println!("El valor de y es: {}", y); // Imprime: El valor de y es: 10

Al añadir mut antes del nombre de la variable, le estamos indicando al compilador que esta variable puede cambiar su valor a lo largo del programa. Es importante usar mut solo cuando sea necesario, ya que las variables inmutables nos ayudan a escribir código más seguro y fácil de razonar.

Constantes con const

Además de las variables (mutables e inmutables), Rust también tiene constantes. Las constantes son valores que nunca cambian durante toda la ejecución del programa y se declaran con la palabra clave const:

const MAX_POINTS: u32 = 100_000;

Hay varias diferencias importantes entre constantes y variables inmutables:

  • Las constantes deben tener su tipo explícitamente declarado (en el ejemplo, u32).
  • Las constantes se declaran con const en lugar de let.
  • El valor de una constante debe ser una expresión constante que pueda determinarse en tiempo de compilación, no puede ser el resultado de una función o cualquier otro valor que solo se conozca en tiempo de ejecución.
  • Las constantes pueden declararse en cualquier ámbito, incluyendo el ámbito global.
  • Las constantes son válidas durante todo el tiempo que se ejecuta el programa, dentro del ámbito en que fueron declaradas.
// Ejemplo de constantes
const HORAS_DIA: u8 = 24;
const DIAS_SEMANA: u8 = 7;
const SEGUNDOS_SEMANA: u32 = 60 * 60 * HORAS_DIA * DIAS_SEMANA;

En este ejemplo, SEGUNDOS_SEMANA se calcula a partir de otras constantes, lo cual es válido porque todas son conocidas en tiempo de compilación.

Sombreado (Shadowing)

Rust permite una técnica llamada sombreado (shadowing), que consiste en declarar una nueva variable con el mismo nombre que una variable existente:

let z = 5;
let z = z + 1; // Sombreado: nueva variable z con valor 6
let z = z * 2; // Sombreado: nueva variable z con valor 12

El sombreado es diferente de marcar una variable como mut. Con el sombreado, estamos creando una nueva variable cada vez, lo que nos permite cambiar el tipo de la variable manteniendo el mismo nombre:

let espacios = "   "; // espacios es un &str (string)
let espacios = espacios.len(); // espacios ahora es un número (usize)

Esto no sería posible con una variable mutable, ya que Rust no permite cambiar el tipo de una variable existente.

Cuándo usar cada opción

  • Usa let (inmutable) como opción predeterminada para la mayoría de las variables.
  • Usa mut cuando necesites cambiar el valor de una variable.
  • Usa const para valores que nunca cambiarán y que son conocidos en tiempo de compilación.
  • Usa el sombreado cuando quieras transformar una variable y posiblemente cambiar su tipo.

La elección entre estas opciones afecta directamente a la seguridad y claridad de tu código. La preferencia por la inmutabilidad en Rust no es arbitraria: ayuda a prevenir una clase entera de errores relacionados con el estado compartido y modificaciones inesperadas.

// Ejemplo completo que muestra los diferentes tipos de variables
fn main() {
    // Variable inmutable
    let edad = 30;
    println!("Mi edad es: {}", edad);
    
    // Variable mutable
    let mut contador = 0;
    contador = contador + 1;
    println!("Contador: {}", contador);
    
    // Constante
    const PI: f64 = 3.14159;
    println!("El valor de PI es: {}", PI);
    
    // Sombreado
    let mensaje = "Hola";
    let mensaje = mensaje.len();
    println!("Longitud del mensaje: {}", mensaje);
}

Este ejemplo muestra las diferentes formas de declarar variables en Rust, cada una con su propósito específico. Entender estas diferencias es fundamental para escribir código Rust efectivo y seguro.

Tipos primitivos

Rust proporciona un conjunto de tipos de datos primitivos que sirven como bloques fundamentales para construir programas. Estos tipos están integrados directamente en el lenguaje y representan los valores más básicos con los que trabajarás.

Tipos enteros

Los tipos enteros en Rust representan números sin parte decimal. Se dividen en dos categorías principales: con signo (pueden ser positivos o negativos) y sin signo (solo positivos).

Cada tipo entero tiene un tamaño específico que determina el rango de valores que puede almacenar:

Longitud Con signo Sin signo
8 bits i8 u8
16 bits i16 u16
32 bits i32 u32
64 bits i64 u64
128 bits i128 u128
arch isize usize

El tipo i32 es el predeterminado para enteros en Rust, ya que ofrece un buen equilibrio entre rango y rendimiento:

let entero_con_signo: i32 = -42;
let entero_sin_signo: u32 = 42;

Los tipos isize y usize dependen de la arquitectura del sistema en el que se ejecuta el programa (32 bits o 64 bits). Son especialmente útiles cuando necesitas indexar colecciones:

let tamaño: usize = 8; // Útil para índices y tamaños de colecciones

Rust permite escribir literales numéricos de varias formas para mejorar la legibilidad:

let decimal = 98_222;      // Separadores para legibilidad: 98,222
let hexadecimal = 0xff;    // Hexadecimal (base 16)
let octal = 0o77;          // Octal (base 8)
let binario = 0b1111_0000; // Binario (base 2)
let byte = b'A';           // Byte (solo u8): valor ASCII de 'A'

Tipos de punto flotante

Para representar números con parte decimal, Rust ofrece dos tipos de punto flotante que siguen el estándar IEEE-754:

let x: f64 = 2.0;      // f64 (doble precisión, 64 bits) - predeterminado
let y: f32 = 3.0;      // f32 (precisión simple, 32 bits)

El tipo f64 es el predeterminado porque en los procesadores modernos tiene prácticamente la misma velocidad que f32 pero con mayor precisión.

let pi: f64 = 3.14159265358979323846;
let avogadro: f64 = 6.022_140_76e23; // Notación científica con separador

Tipo booleano

El tipo bool representa valores de verdad y puede tener solo dos valores posibles:

let verdadero: bool = true;
let falso: bool = false;

Los booleanos ocupan un byte en memoria y se utilizan frecuentemente en condiciones y operaciones lógicas.

Tipo carácter

El tipo char en Rust representa un valor Unicode escalar, lo que significa que puede almacenar mucho más que solo caracteres ASCII:

let c: char = 'z';
let emoji: char = '😻';
let kanji: char = '国';

Cada valor char ocupa 4 bytes (32 bits) en memoria, ya que necesita poder representar cualquier punto de código Unicode. Los caracteres se especifican con comillas simples ('), a diferencia de las cadenas que usan comillas dobles.

Tipo unit

Rust tiene un tipo especial llamado unit, representado por (), que no contiene ningún valor. Es similar al void en otros lenguajes, pero se considera un tipo con un único valor posible:

let nada: () = ();

Este tipo se utiliza principalmente como valor de retorno para funciones que no devuelven nada útil.

Literales y sufijos de tipo

Puedes especificar el tipo de un literal numérico añadiendo un sufijo:

let entero_i8: i8 = 127_i8;
let entero_u32 = 42u32;
let flotante_f32 = 3.14f32;

Esto es útil cuando necesitas un tipo específico sin una anotación de tipo completa.

Límites de los tipos numéricos

Cada tipo numérico tiene un rango específico de valores que puede representar:

// Límites de tipos enteros
let min_i8: i8 = -128;         // -2^7
let max_i8: i8 = 127;          // 2^7 - 1
let max_u8: u8 = 255;          // 2^8 - 1

// Para tipos más grandes
let max_u32: u32 = 4_294_967_295;  // 2^32 - 1

Rust proporciona constantes en la biblioteca estándar para acceder a estos límites:

let min_i32 = i32::MIN;  // Valor mínimo para i32
let max_u64 = u64::MAX;  // Valor máximo para u64

Tamaño de los tipos en memoria

El tamaño en memoria de cada tipo primitivo es fijo y está determinado por su nombre (excepto para isize y usize):

// Tamaños en bytes
let tamaño_i8 = std::mem::size_of::<i8>();    // 1 byte
let tamaño_i32 = std::mem::size_of::<i32>();  // 4 bytes
let tamaño_f64 = std::mem::size_of::<f64>();  // 8 bytes
let tamaño_bool = std::mem::size_of::<bool>(); // 1 byte
let tamaño_char = std::mem::size_of::<char>(); // 4 bytes

Conocer el tamaño de los tipos es importante para optimizar el uso de memoria, especialmente en sistemas embebidos o cuando se trabaja con grandes cantidades de datos.

Ejemplos prácticos

Veamos algunos ejemplos que ilustran el uso de estos tipos primitivos:

fn main() {
    // Enteros
    let edad: u8 = 35;
    let población_mundial: u64 = 7_900_000_000;
    
    // Punto flotante
    let pi: f64 = 3.14159;
    let temperatura: f32 = -2.5;
    
    // Booleanos
    let es_mayor_de_edad: bool = edad >= 18;
    
    // Caracteres
    let inicial: char = 'R';
    let corazón: char = '❤';
    
    // Imprimiendo los valores
    println!("Edad: {}", edad);
    println!("Población mundial: {}", población_mundial);
    println!("Valor de pi: {}", pi);
    println!("Temperatura: {}°C", temperatura);
    println!("¿Es mayor de edad? {}", es_mayor_de_edad);
    println!("Inicial: {}", inicial);
    println!("Símbolo: {}", corazón);
}

Los tipos primitivos en Rust proporcionan una base sólida para construir programas seguros y eficientes. Su diseño cuidadoso ayuda a prevenir errores comunes como desbordamientos y problemas de precisión numérica, al tiempo que ofrece un rendimiento óptimo.

Inferencia y casting

Rust combina un sistema de tipos estático con la capacidad de inferir tipos automáticamente, lo que nos permite escribir código conciso sin sacrificar la seguridad. Entender cómo funciona la inferencia de tipos y cómo convertir entre diferentes tipos es fundamental para programar eficientemente en Rust.

Inferencia de tipos

La inferencia de tipos permite al compilador de Rust determinar automáticamente el tipo de una variable basándose en el valor asignado y el contexto en el que se usa. Esto nos ahorra tener que especificar explícitamente el tipo en cada declaración:

let número = 42; // Rust infiere que es i32
let decimal = 3.14; // Rust infiere que es f64
let activo = true; // Rust infiere que es bool

El compilador analiza cómo usamos estas variables para determinar su tipo más apropiado. Por ejemplo, si asignamos un número entero sin especificar el tipo, Rust asumirá por defecto que es un i32.

Cuándo es necesaria la anotación de tipos

Aunque la inferencia de tipos es potente, hay situaciones donde debemos especificar el tipo explícitamente:

  1. Cuando declaramos una variable sin inicializarla:
let edad: u8; // Debemos especificar el tipo si no hay valor inicial
edad = 25; // Asignación posterior
  1. Cuando la inferencia no tiene suficiente contexto para determinar un tipo específico:
let entero_grande: u64 = 9_000_000_000; // Excede el rango de i32, necesitamos especificar u64
let pequeño_flotante: f32 = 3.14; // Por defecto sería f64, pero queremos f32
  1. Cuando queremos un tipo diferente al que Rust inferiría por defecto:
let byte: u8 = 65; // Forzamos a que sea u8 en lugar de i32

Sintaxis de anotación de tipos

La anotación de tipos en Rust sigue una sintaxis clara donde el tipo se especifica después de dos puntos:

let variable: tipo = valor;

Algunos ejemplos:

let contador: u32 = 0;
let pi: f64 = 3.14159;
let letra: char = 'A';
let es_válido: bool = false;

Conversión entre tipos (casting)

Rust es muy estricto con los tipos y no realiza conversiones implícitas entre tipos numéricos, incluso cuando no habría pérdida de información. Esto previene errores sutiles y comportamientos inesperados.

Para convertir entre tipos numéricos, usamos el operador as:

let entero = 65;
let caracter = entero as u8 as char; // Convertimos i32 -> u8 -> char
println!("Entero {} como carácter: {}", entero, caracter); // Imprime: "Entero 65 como carácter: A"

let x = 3.14;
let y = x as i32; // Trunca el valor a 3
println!("Flotante {} como entero: {}", x, y); // Imprime: "Flotante 3.14 como entero: 3"

Es importante entender que el casting con as puede provocar truncamiento o saturación dependiendo de los tipos involucrados:

let grande = 1000;
let pequeño: u8 = grande as u8; // 1000 no cabe en u8, se produce saturación
println!("Valor después de casting: {}", pequeño); // Imprime: "Valor después de casting: 255"

Conversiones seguras con métodos

Para conversiones más controladas y seguras, Rust proporciona métodos específicos:

// Conversión con verificación de rango
let grande = 500;
let pequeño = u8::try_from(grande);

match pequeño {
    Ok(valor) => println!("Conversión exitosa: {}", valor),
    Err(_) => println!("Error: el valor {} no cabe en u8", grande)
}

Para convertir entre tipos numéricos y strings, podemos usar métodos como parse():

let texto = "42";
let número: i32 = texto.parse().expect("No es un número válido");
println!("Texto convertido a número: {}", número);

El método parse() devuelve un Result que debemos manejar, ya que la conversión podría fallar:

let texto = "3.14";
let número: f64 = match texto.parse() {
    Ok(num) => num,
    Err(_) => {
        println!("Error al convertir '{}' a número", texto);
        0.0 // Valor por defecto en caso de error
    }
};

Inferencia en expresiones complejas

La inferencia de tipos en Rust también funciona en expresiones más complejas:

let a = 5;
let b = 10;
let suma = a + b; // Rust infiere que suma es del mismo tipo que a y b (i32)

let c = 15.0;
let producto = b as f64 * c; // Rust infiere que producto es f64

El compilador analiza el flujo de datos para determinar los tipos más apropiados en cada contexto.

Ejemplos prácticos

Veamos algunos ejemplos que ilustran la inferencia y conversión de tipos en situaciones cotidianas:

fn main() {
    // Inferencia básica
    let edad = 30; // i32
    let altura = 1.75; // f64
    
    // Conversión explícita para cálculos
    let peso = 70.5; // f64
    let peso_redondeado = peso as i32; // Conversión a entero (trunca a 70)
    
    // Conversión entre tipos numéricos
    let pequeño = 100;
    let grande = pequeño as u64; // Conversión segura (ampliación)
    
    // Conversión de caracteres
    let inicial = 'R';
    let código_ascii = inicial as u32;
    println!("El código ASCII de '{}' es {}", inicial, código_ascii);
    
    // Conversión de string a número
    let año_texto = "2023";
    let año_número: i32 = año_texto.parse().unwrap();
    println!("Año: {}", año_número);
    
    // Inferencia en expresiones
    let x = 5;
    let y = 10.5;
    let resultado = x as f64 + y; // Rust infiere que resultado es f64
    println!("Resultado: {}", resultado);
}

La combinación de un sistema de tipos estricto con una inferencia potente hace que Rust sea tanto seguro como conveniente de usar. La inferencia reduce la verbosidad del código mientras que las conversiones explícitas nos aseguran que entendemos y controlamos exactamente cómo se manipulan nuestros datos.

Aprende Rust online

Otras lecciones de Rust

Accede a todas las lecciones de Rust y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Rust y certifícate

Ejercicios de programación de Rust

Evalúa tus conocimientos de esta lección Variables y tipos básicos con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender la diferencia entre variables inmutables, mutables y constantes en Rust.
  • Conocer los tipos de datos primitivos básicos y sus características en Rust.
  • Aprender a utilizar la inferencia de tipos y cuándo es necesaria la anotación explícita.
  • Entender cómo realizar conversiones (casting) entre tipos de datos de forma segura.
  • Aplicar técnicas como el sombreado para modificar variables manteniendo la seguridad del código.