Tipos de Datos en Rust
Rust es un lenguaje de tipado estático, lo que significa que debe conocer los tipos de todas las variables en tiempo de compilación. En este capítulo exploraremos los tipos de datos disponibles en Rust.
Tipos Escalares
Los tipos escalares representan un solo valor. Rust tiene cuatro tipos escalares primarios.
Enteros
Los enteros son números sin parte fraccionaria. Rust proporciona varios tamaños:
| Longitud | Con signo | Sin signo |
|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| arch | isize | usize |
fn main() {
// Enteros con signo (pueden ser negativos)
let edad: u8 = 30; // 0 a 255
let temperatura: i32 = -10; // -2^31 a 2^31-1
let poblacion: u64 = 8_000_000; // Usa guiones bajos para legibilidad
// El tipo por defecto es i32
let numero = 42; // i32
// Enteros dependientes de la arquitectura
let indice: usize = 0; // Usado para indexación, tamaño del puntero
let offset: isize = -5; // Con signo, tamaño del puntero
// Diferentes bases numéricas
let decimal = 98_222; // Base 10
let hexadecimal = 0xff; // Base 16
let octal = 0o77; // Base 8
let binario = 0b1111_0000; // Base 2
let byte = b'A'; // Solo u8
println!("Decimal: {decimal}");
println!("Hexadecimal: {hexadecimal}");
println!("Octal: {octal}");
println!("Binario: {binario}");
println!("Byte: {byte}");
}
Overflow de Enteros
fn main() {
// En modo debug, esto causaría panic
// En modo release, haría "wrapping"
let mut x: u8 = 255;
// x = x + 1; // Overflow!
// Maneras seguras de manejar overflow:
// 1. Wrapping: da la vuelta
x = x.wrapping_add(1);
println!("Wrapping: {x}"); // 0
// 2. Checked: retorna Option
let y: u8 = 255;
match y.checked_add(1) {
Some(valor) => println!("Resultado: {valor}"),
None => println!("¡Overflow detectado!"),
}
// 3. Overflowing: retorna tupla (resultado, overflow_ocurrió)
let (resultado, overflow) = y.overflowing_add(1);
println!("Resultado: {resultado}, Overflow: {overflow}");
// 4. Saturating: se queda en el límite
let z = y.saturating_add(1);
println!("Saturating: {z}"); // 255
}
Números de Punto Flotante
Rust tiene dos tipos de punto flotante: f32 y f64. El tipo por defecto es f64.
fn main() {
let x = 2.0; // f64 por defecto
let y: f32 = 3.0; // f32 explícito
// Operaciones con punto flotante
let pi: f64 = 3.14159265359;
let e: f32 = 2.718281828;
println!("Pi: {pi}");
println!("E: {e}");
// Valores especiales
let infinito = f64::INFINITY;
let neg_infinito = f64::NEG_INFINITY;
let nan = f64::NAN;
println!("Infinito: {infinito}");
println!("Infinito negativo: {neg_infinito}");
println!("NaN: {nan}");
// Verificar valores especiales
println!("¿Es infinito? {}", infinito.is_infinite());
println!("¿Es NaN? {}", nan.is_nan());
println!("¿Es finito? {}", pi.is_finite());
}
Operaciones Aritméticas
fn main() {
// Operaciones básicas
let suma = 5 + 10;
let diferencia = 95.5 - 4.3;
let producto = 4 * 30;
let cociente = 56.7 / 32.2;
let residuo = 43 % 5;
println!("Suma: {suma}");
println!("Diferencia: {diferencia}");
println!("Producto: {producto}");
println!("Cociente: {cociente}");
println!("Residuo: {residuo}");
// Operaciones con asignación
let mut x = 5;
x += 3; // x = x + 3
x -= 1; // x = x - 1
x *= 2; // x = x * 2
x /= 4; // x = x / 4
x %= 3; // x = x % 3
println!("Resultado final: {x}");
}
Tipo Boolean
El tipo bool puede ser true o false:
fn main() {
let t = true;
let f: bool = false; // Con anotación de tipo explícita
// Operaciones lógicas
let and = t && f; // false
let or = t || f; // true
let not = !t; // false
println!("AND: {and}");
println!("OR: {or}");
println!("NOT: {not}");
// Los booleanos ocupan 1 byte
println!("Tamaño de bool: {} bytes", std::mem::size_of::<bool>());
}
Tipo Character
El tipo char representa un valor escalar Unicode:
fn main() {
let c = 'z';
let z: char = 'ℤ';
let corazon = '♥';
let emoji = '😻';
println!("Caracteres: {c}, {z}, {corazon}, {emoji}");
// Los chars ocupan 4 bytes (UTF-32)
println!("Tamaño de char: {} bytes", std::mem::size_of::<char>());
// Convertir entre char y u32
let numero: u32 = 'A' as u32;
let caracter: char = 65 as u8 as char;
println!("'A' como número: {numero}");
println!("65 como carácter: {caracter}");
}
Tipos Compuestos
Los tipos compuestos agrupan múltiples valores en un solo tipo.
Tuplas
Las tuplas agrupan valores de diferentes tipos:
fn main() {
// Crear una tupla
let tupla: (i32, f64, char) = (500, 6.4, 'R');
// Destructuring (desestructuración)
let (x, y, z) = tupla;
println!("x: {x}, y: {y}, z: {z}");
// Acceso por índice
let quinientos = tupla.0;
let seis_coma_cuatro = tupla.1;
let r = tupla.2;
println!("Primer elemento: {quinientos}");
println!("Segundo elemento: {seis_coma_cuatro}");
println!("Tercer elemento: {r}");
// Tupla vacía (unit type)
let unit = ();
println!("Unit type: {:?}", unit);
}
Funciones que Retornan Tuplas
fn main() {
let (suma, producto) = calcular(4, 5);
println!("4 + 5 = {suma}");
println!("4 * 5 = {producto}");
// Ignorar valores con _
let (_, area) = dimensiones_rectangulo(3.0, 4.0);
println!("Área: {area}");
}
fn calcular(a: i32, b: i32) -> (i32, i32) {
(a + b, a * b)
}
fn dimensiones_rectangulo(ancho: f64, alto: f64) -> (f64, f64) {
(ancho * 2.0 + alto * 2.0, ancho * alto)
}
Arrays
Los arrays tienen un tamaño fijo y todos los elementos deben ser del mismo tipo:
fn main() {
// Declaración de array
let numeros: [i32; 5] = [1, 2, 3, 4, 5];
// Shortcut para valores repetidos
let cincos = [5; 3]; // [5, 5, 5]
// Acceso a elementos
let primero = numeros[0];
let segundo = numeros[1];
println!("Primero: {primero}");
println!("Segundo: {segundo}");
// Longitud del array
println!("Longitud: {}", numeros.len());
// Iterando sobre un array
for elemento in numeros {
println!("Elemento: {elemento}");
}
// Con índices
for (i, elemento) in numeros.iter().enumerate() {
println!("Índice {i}: {elemento}");
}
}
Acceso Seguro a Arrays
fn main() {
let array = [1, 2, 3, 4, 5];
// Acceso directo (puede causar panic)
// let elemento = array[10]; // ¡Panic en runtime!
// Acceso seguro con get()
match array.get(10) {
Some(valor) => println!("Elemento en índice 10: {valor}"),
None => println!("Índice fuera de rango"),
}
// Usando if let
if let Some(valor) = array.get(2) {
println!("Elemento en índice 2: {valor}");
}
}
Slices
Los slices son referencias a una porción de un array:
fn main() {
let array = [1, 2, 3, 4, 5, 6];
// Slice completo
let slice_completo = &array;
// Slice parcial
let slice = &array[1..4]; // [2, 3, 4]
let desde_inicio = &array[..3]; // [1, 2, 3]
let hasta_final = &array[2..]; // [3, 4, 5, 6]
println!("Slice completo: {:?}", slice_completo);
println!("Slice parcial: {:?}", slice);
println!("Desde inicio: {:?}", desde_inicio);
println!("Hasta final: {:?}", hasta_final);
// Longitud de slices
println!("Longitud del slice: {}", slice.len());
}
Strings
En Rust hay dos tipos principales de strings: &str y String.
String Slices (&str)
fn main() {
// String literal (tipo &str)
let saludo = "¡Hola, mundo!";
// Slice de String
let s = String::from("¡Hola, mundo!");
let slice = &s[0..5]; // "¡Hola"
println!("String literal: {saludo}");
println!("String slice: {slice}");
// Los string literals son inmutables
// saludo.push_str("!!!"); // ERROR: no se puede modificar
}
String (Heap-allocated)
fn main() {
// Crear un String mutable
let mut s = String::new();
s.push_str("¡Hola");
s.push(' ');
s.push_str("mundo!");
println!("String construido: {s}");
// Desde string literal
let s2 = String::from("¡Hola, mundo!");
let s3 = "¡Hola, mundo!".to_string();
println!("s2: {s2}");
println!("s3: {s3}");
// Operaciones con String
let mut texto = String::from("Rust");
texto.push_str(" es");
texto.push(' ');
texto.push_str("genial");
println!("Texto final: {texto}");
println!("Longitud: {}", texto.len());
println!("¿Está vacío? {}", texto.is_empty());
}
Conversiones Entre Tipos
Casting Explícito
fn main() {
// Casting entre enteros
let a: u8 = 10;
let b: u16 = a as u16;
let c: i32 = b as i32;
println!("a: {a}, b: {b}, c: {c}");
// Casting puede truncar valores
let grande: u32 = 300;
let pequeño: u8 = grande as u8; // 300 % 256 = 44
println!("Grande: {grande}, Pequeño: {pequeño}");
// Punto flotante a entero (trunca decimales)
let f = 3.7;
let entero = f as i32; // 3
println!("Float: {f}, Entero: {entero}");
}
Parsing desde Strings
fn main() {
// Parse exitoso
let numero_str = "42";
let numero: i32 = numero_str.parse().unwrap();
println!("Número parseado: {numero}");
// Parse con manejo de errores
let texto = "123.45";
match texto.parse::<f64>() {
Ok(num) => println!("Número: {num}"),
Err(e) => println!("Error parseando: {e}"),
}
// Parse que falla
let malo = "no_es_numero";
match malo.parse::<i32>() {
Ok(num) => println!("Número: {num}"),
Err(_) => println!("No se pudo parsear '{malo}'"),
}
}
Ejercicios Prácticos
Ejercicio 1: Calculadora de IMC
fn main() {
let peso: f64 = 70.5; // kg
let altura: f64 = 1.75; // metros
let imc = calcular_imc(peso, altura);
let categoria = clasificar_imc(imc);
println!("Peso: {peso} kg");
println!("Altura: {altura} m");
println!("IMC: {:.2}", imc);
println!("Clasificación: {categoria}");
}
fn calcular_imc(peso: f64, altura: f64) -> f64 {
peso / (altura * altura)
}
fn clasificar_imc(imc: f64) -> &'static str {
if imc < 18.5 {
"Bajo peso"
} else if imc < 25.0 {
"Peso normal"
} else if imc < 30.0 {
"Sobrepeso"
} else {
"Obesidad"
}
}
Ejercicio 2: Análisis de Array
fn main() {
let numeros = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];
let (suma, promedio, minimo, maximo) = analizar_array(&numeros);
println!("Array: {:?}", numeros);
println!("Suma: {suma}");
println!("Promedio: {:.2}", promedio);
println!("Mínimo: {minimo}");
println!("Máximo: {maximo}");
}
fn analizar_array(arr: &[i32]) -> (i32, f64, i32, i32) {
let suma: i32 = arr.iter().sum();
let promedio = suma as f64 / arr.len() as f64;
let minimo = *arr.iter().min().unwrap();
let maximo = *arr.iter().max().unwrap();
(suma, promedio, minimo, maximo)
}
Puntos Clave para Recordar
- Rust es de tipado estático - todos los tipos deben conocerse en tiempo de compilación
- i32 es el tipo entero por defecto, f64 para punto flotante
- Los arrays tienen tamaño fijo, los slices son referencias flexibles
- char en Rust es Unicode y ocupa 4 bytes
- Las tuplas pueden contener tipos diferentes
- &str es inmutable, String es mutable y está en el heap
- Usa
as para casting explícito entre tipos primitivos
parse() convierte strings a otros tipos
Próximo Paso
En el siguiente capítulo aprenderemos sobre las estructuras de control en Rust: condicionales, bucles y pattern matching, que nos permitirán tomar decisiones y repetir código de manera eficiente.