Fundamentos de los datos básicos en Rust
En el contexto de un libro sobre programación en Rust, este capítulo examina los tipos de datos primitivos que forman la base de cualquier programa, junto con las estructuras para manejar texto. Estos elementos son esenciales para entender cómo Rust gestiona la memoria y la seguridad de tipos de manera estática, permitiendo construcciones eficientes y seguras desde el principio. Dominar estos conceptos facilita la transición a temas más avanzados como las estructuras de datos compuestas.
Tipos Numéricos
Rust proporciona un conjunto de tipos numéricos integrados que cubren tanto enteros como números de coma flotante, diseñados para ofrecer precisión y control sobre el tamaño y el signo. Los tipos enteros incluyen variantes con signo (i8, i16, i32, i64, i128) y sin signo (u8, u16, u32, u64, u128), donde el número indica los bits de almacenamiento. Por ejemplo, un i32 ocupa 32 bits y puede representar valores desde -2^31 hasta 2^31 – 1. De manera similar, los tipos de coma flotante son f32 y f64, siguiendo los estándares IEEE 754 para precisión simple y doble, respectivamente.
La elección del tipo numérico afecta directamente la eficiencia y la prevención de desbordamientos. Rust verifica los desbordamientos en tiempo de compilación en modo de depuración, pero en modo de liberación, estos se envuelven (wrap around) por defecto, aunque se pueden usar métodos como checked_add para operaciones seguras. Un caso borde notable surge con los literales: Rust infiere el tipo basado en el contexto, pero se puede especificar explícitamente con sufijos como 42i32 o 3.14f64.
let x: i32 = 42;
let y: f64 = 3.14159;
let suma = x as f64 + y; // Conversión explícita para compatibilidad
En comparación con lenguajes como C++, donde los tipos enteros pueden variar por plataforma (por ejemplo, int podría ser 32 o 64 bits), Rust garantiza portabilidad mediante tamaños fijos. Los desbordamientos no controlados pueden llevar a comportamiento indefinido en otros lenguajes, pero Rust mitiga esto con chequeos en tiempo de ejecución opcionales. Otro detalle sutil es el tipo isize y usize, dependientes de la arquitectura (por ejemplo, 64 bits en sistemas x86-64), útiles para índices y tamaños de memoria.
Tipo Booleano
El tipo booleano en Rust, denominado bool, representa valores lógicos verdaderos (true) o falsos (false) y ocupa un byte de memoria. Este tipo es fundamental para expresiones condicionales y operaciones lógicas, integrándose de manera natural con estructuras de control como if y while. A diferencia de algunos lenguajes como C, donde los booleanos son meras convenciones sobre enteros (0 para false, no cero para true), Rust impone una separación estricta, evitando conversiones implícitas que podrían llevar a errores.
Las operaciones lógicas incluyen && (y), || (o) y ! (no), con evaluación de cortocircuito para eficiencia: en una expresión como a && b, b solo se evalúa si a es true. Un caso borde se presenta en contextos donde se espera un bool pero se proporciona un valor no booleano, lo que resulta en un error de compilación.
let es_verdadero: bool = true;
let resultado = !es_verdadero && (5 > 3); // false && true = false
En lenguajes como Python, los booleanos heredan de enteros, permitiendo usos como sumas (True + True = 2), pero Rust prohíbe tales conversiones para mantener la integridad de tipos. Es crucial destacar que las comparaciones siempre producen bool, como en 10 == 10, y no se permiten en contextos no booleanos sin conversión explícita.
Tipo Char
El tipo char en Rust representa un carácter Unicode escalar, ocupando cuatro bytes para abarcar el rango completo de Unicode (U+0000 a U+10FFFF). A diferencia de lenguajes como Java, donde char es de 16 bits y limitado a BMP (Basic Multilingual Plane), Rust maneja puntos de código completos, permitiendo caracteres como emojis o scripts complejos directamente.
Los literales se denotan con comillas simples, como ‘a’ o ‘😊’. Operaciones comunes incluyen comparaciones y conversiones a enteros (por ejemplo, ‘a’ as u32 = 97). Un detalle sutil es que char no representa bytes individuales en cadenas UTF-8, lo que evita confusiones comunes en codificaciones variables.
let letra: char = 'ñ';
let codigo = letra as u32; // 241 en hexadecimal En comparación con C, donde char es un byte y puede ser signed o unsigned dependiendo del compilador, Rust asegura consistencia y seguridad Unicode. Casos borde incluyen caracteres no válidos en literales, que causan errores de compilación, y la distinción entre char y &str para secuencias de caracteres.
Cadenas: &str y String
Rust distingue entre dos tipos principales para manejar texto: &str, una referencia a una porción de cadena inmutable codificada en UTF-8, y String, una estructura dinámica que posee y gestiona su propio buffer de bytes UTF-8. &str es eficiente para texto estático o prestado, ya que no implica asignación, mientras que String permite mutabilidad y crecimiento dinámico mediante métodos como push y push_str.
String implementa Deref a &str, permitiendo su uso en contextos que esperan una referencia. La codificación UTF-8 asegura validez, y métodos como len devuelven el tamaño en bytes, no en caracteres, destacando un caso borde: una cadena con caracteres multibyte como “café” tiene len() = 5, no 4.
let estatica: &str = "Hola, mundo";
let mut dinamica: String = String::from("Hola");
dinamica.push_str(", mundo");En lenguajes como Go, las cadenas son inmutables y similares a &str, pero Rust ofrece String para escenarios mutables con ownership. Un detalle sutil es la invalidación de referencias al mutar String, enforces por el borrow checker para prevenir errores de memoria.
Conversiones Básicas
Las conversiones entre tipos primitivos y cadenas en Rust se manejan mediante métodos como to_string para convertir a String y parse para el inverso, a menudo envueltos en Result para manejar errores. Por ejemplo, un i32 se convierte a String con to_string(), mientras que parse::() intenta extraer un entero de una &str.
Estas operaciones son explícitas, evitando conversiones implícitas que podrían ocultar errores. Un caso borde ocurre con parse en entradas inválidas, retornando Err.
let numero: i32 = 42;
let cadena: String = numero.to_string();
let parseado: Result<i32, _> = "42".parse();En comparación con JavaScript, donde las conversiones son coercitivas (e.g., “42” + 1 = “421”), Rust exige manejo explícito. Es importante notar que to_string está disponible para tipos que implementan Display, y parse para FromStr.
Proyecto Completo: Analizador de Texto Simple
Para ilustrar la aplicación de tipos primitivos y strings, se presenta un analizador de texto simple que cuenta líneas, palabras y caracteres a partir de entrada estándar. El proyecto utiliza un enfoque modular, leyendo la entrada como String y procesando con tipos numéricos para contadores.
Estructura de carpetas:
analizador_texto/
├── Cargo.toml
└── src/
└── main.rsContenido de Cargo.toml:
[package]
name = "analizador_texto"
version = "0.1.0"
edition = "2021"
[dependencies]Contenido de src/main.rs:
use std::io::{self, BufRead};
fn main() {
let stdin = io::stdin();
let mut lineas: usize = 0;
let mut palabras: usize = 0;
let mut caracteres: usize = 0;
for linea in stdin.lines() {
match linea {
Ok(contenido) => {
lineas += 1;
caracteres += contenido.len();
let palabras_en_linea: usize = contenido.split_whitespace().count();
palabras += palabras_en_linea;
}
Err(_) => break,
}
}
println!("Líneas: {}", lineas);
println!("Palabras: {}", palabras);
println!("Caracteres: {}", caracteres);
}Este programa lee líneas de stdin, incrementa contadores de tipos usize para líneas, palabras (usando split_whitespace en &str) y caracteres (via len en String), y muestra los resultados. Compila con cargo build y ejecuta con cargo run < archivo.txt para redirigir entrada.
Con estos fundamentos en tipos primitivos y strings establecidos, el siguiente capítulo explorará las estructuras compuestas y el manejo de colecciones para construir programas más complejos.