🦀 De regreso a los archivos .DAT 😄
A finales del siglo XX, aprendimos Turbo Pascal, Un lenguaje de programación de alto nivel desarrollado por el profesor Niklaus Wirth, que tiempos aquellos...
Estás diciendo que eres viejo, pero sin decir, que eres viejo... :)
-- un visitante del blog
cof, hmm, cof, cof
Bueno, en aquella época se utilizaba este lenguaje, para aprender algoritmos, estructuras de datos, archivos (el tema que nos compete en esta entrada) y más.
Para el tema de manejo de archivos, la información se organizaba en registros, y se almacenaba en archivos de tipo binario usualmente con la extensión .DAT
Un ejemplo práctico en Pascal de un tipo de dato Registro:
type persona = record
nombre: string[50];
edad: integer;
end;
Volviendo al presente, en esta entrada usaremos Structs, Un struct, ó estructura, es un tipo de datos personalizado que permite empaquetar y nombrar múltiples valores relacionados, que conforman un grupo significativo.
Si estás familiarizado con un lenguaje de programación Orientado a Objetos, una estructura es como los atributos de datos de un objeto.
Texto tomado de The Rust Programming Language
En el caso de Rust, definimos un Struct como sigue:
struct Person {
nombre: String,
edad: u8,
}
Iniciando
Damos por entendido que temas relacionados como:
ya son de tu conocimiento, ó al menos, sabes de que tratan. En lo posible explicaremos brevemente para el ejemplo en curso, pero el uso de a profundidad de estos temas, dependerá de tí investigar.
Para este ejemplo, haremos lo siguiente:
- Crearemos un programa en Rust, que utilice Structs, para almacenar la información básica de una persona, en un archivo binario(un archivo con extensión .DAT 😈 🔥).
- Mejoraremos la aplicación para guardar varios
registrosstructs 😄 en el mismo archivo.
En nuestro archivo Cargo.toml, agregamos las siguientes dependencias:
# Cargo.toml
...
[package]
name = "binary-files"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bincode = "1.3.3"
serde = { version = "1.0", features = ["derive"] }
bincode Es una librería que nos permite serializar/deserializar structs en bytes y viceversa, El tamaño del objeto codificado será igual o menor, que el tamaño que el objeto ocuparía en memoria en una aplicación Rust en ejecución, además expone una API de Lectura/Escritura que hace que funcione perfectamente con otras APIs basadas en flujos como archivos de Rust, flujos de red, etc... perfecto para nuestras necesidades en esta entrada.
En cristiano significa que...
-- un visitante del blog
... Que nos permite generar archivos binarios a partir de un Struct dado (ó arreglo de ellos), y después, al recuperar la información almacenada en el archivo, convertirla nuevamente a un struct.
Solicitando información al usuario
Definimos nuestro struct, con información básica(muy muy básica) de la persona. Usamos serde, para manejar nuestro struct de manera más eficiente.
use serde::{Deserialize, Serialize};
...
#[derive(Debug, Serialize, Deserialize)]
struct Person {
name: String,
age: u8,
}
En la función main, pedimos la información al usuario:
fn main() {
println!("nuevo usuario");
println!();
let name: String = get_input_default("nombre: ");
let age: u8 = get_input_default("edad: ");
println!();
let people = Person { name, age };
println!("nuestro usuario es: {:?}", people)
}
Nada nuevo bajo el sol, creamos una instancia de nuestro struct y la imprimimos por la consola.
La salida luce algo como:
❯ cargo run
nuevo usuario
nombre: diniremix
edad: 250
nuestro usuario es: Person { name: "diniremix", age: 250 }
y la función "get_input_default" ?
-- un visitante del blog
En esta entrada, se explica con más detalle, una función bastante útil y hemos querido reutilizarla para agilizar el proceso de pedir datos por la consola en nuestra aplicación.
Guardando el registro struct en un archivo binario
Crearemos una función llamada guardar_archivo, y hace lo siguiente:
- recibe como parámetro, un struct(de tipo Person).
- crea un archivo llamado usuario.dat.
- utiliza el método serialize de bincode, para convertir a bytes nuestra instancia de Person.
- guarda los bytes generados en el archivo usuario.dat, utilizando una referencia de File
- revisa que todo este bien, y termina.
fn guardar_archivo(people: &Person) {
let mut f = File::create("usuario.dat").unwrap();
let bytes = bincode::serialize(&people).unwrap();
let result = f.write(bytes.as_slice()).expect("Ocurrio un error mientras se escribían los datos en el archivo");
if f.sync_all().is_ok() {
println!("archivo guardado");
} else {
println!("ocurrio un error al guardar el archivo");
}
}
Modificamos la función main para invocar a la función guardar_archivo y enviarle la instancia de nuestro struct:
fn main() {
println!("nuevo usuario");
println!();
let name: String = get_input_default("nombre: ");
let age: u8 = get_input_default("edad: ");
println!();
let people = Person { name, age };
//println!("nuestro usuario es: {:?}", people)
guardar_archivo(&people);
}
La salida luce algo como:
❯ cargo run
nuevo usuario
nombre: diniremix
edad: 250
archivo guardado
y nuestro archivo usuario.dat, utilizando un editor hexadecimal para ver su contenido, usando la consola, luce así:
❯ xxd usuario.dat
00000000: 0900 0000 0000 0000 6469 6e69 7265 6d69 ........diniremi
00000010: 78fa x.
ohh!, y como recupero mi información?
-- un visitante del blog
Calma, veamos...
Recuperando datos del archivo binario
Crearemos una función llamada leer_archivo, y hace lo siguiente:
- lee el archivo llamado usuario.dat, en caso de que no exista o no se pueda acceder, generará un error.
- crea un búfer(BufReader) en donde almacenar el contenido del archivo, recuerda que es de tipo binario.
- A partir del búfer, lee el archivo (métdo read_to_end) y lo almacenamos en un vector (Vec), esto facilita leer todo el contenido del archivo (nuestro caso)
- utiliza el metodo deserialize de bincode, para convertir a struct (instancia de Person) el contenido del Vector.
- muestra la informacion leida del archivo y convertida nuevamente a struct.
fn leer_archivo() {
println!("leyendo archivo usuario.dat...",);
let f = File::open("usuario.dat").expect("Unable to read the file");
let mut buffer = BufReader::new(f);
let mut data = Vec::new();
buffer.read_to_end(&mut data).expect("Unable to read content");
let people: Person = bincode::deserialize(&data).unwrap();
println!("datos leidos: {:?}", &people);
}
si agreamos la llamada a la función leer_archivo en nuestra función main
// main.rs
fn main() {
...
leer_archivo();
}
La salida luce algo como:
❯ cargo run
leyendo archivo usuario.dat...
datos leidos: Person { name: "diniremix", age: 250 }
Pequeños ajustes
- un menú que muestre una lista de opciones, para leer ó crear un nuevo archivo.
- una opcion en el menú, para salir.
- que la función leer_archivo reciba como parámetro, el nombre del archivo a leer.
- que la función guardar_archivo reciba como parámetro adicional, el nombre del archivo a crear.
nuestro archivo main.rs, tendrá el siguiente aspecto, despues de estos cambios.
// main.rs
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::{self, BufReader, Read, Write};
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Person {
name: String,
age: u8,
}
fn leer_archivo(filename: &str) {
println!("leyendo {}...", filename);
let f = File::open(format!("{}", filename)).unwrap();
let mut buffer = BufReader::new(f);
let mut data = Vec::new();
buffer.read_to_end(&mut data).expect("Unable to read content");
let people: Person = bincode::deserialize(&data).unwrap();
println!("datos leidos: {:?}", &people);
}
fn guardar_archivo(filename: &str, people: &Person) {
let mut f = File::create(format!("{}", filename)).unwrap();
let bytes = bincode::serialize(&people).unwrap();
let result = f.write(bytes.as_slice()).expect("Error while writing to file");
if f.sync_all().is_ok() {
println!("archivo guardado");
} else {
println!("ocurrio un error al guardar el archivo");
}
}
fn main() {
println!("datos personales");
println!("1. nuevo registro");
println!("2. abrir registro");
println!("3. salir");
let opcion: u32 = get_input_default("opcion: ");
match opcion {
1 => {
println!();
println!("nuevo registro");
let name: String = get_input_default("nombre: ");
let age: u8 = get_input_default("edad: ");
let filename: String = get_input_default("nombre para el archivo: ");
println!();
let people = Person { name, age };
guardar_archivo(&filename, &people);
}
2 => {
println!();
println!("abrir registro");
let filename: String = get_input_default("nombre del archivo: ");
println!();
leer_archivo(&filename);
}
3 => {
println!("saliendo...");
}
_ => {
println!();
println!("opcion no valida, saliendo...");
}
}
}
Recuerda que la función get_input_default se explica con más detalle, por aquí.
al ejecutar nuestro programa (creando el nuevo registro struct)
❯ cargo run
datos personales
1. nuevo registro
2. abrir registro
3. salir
opcion: 1
nuevo registro
nombre: diniremix
edad: 250
nombre para el archivo: demo1.dat
archivo guardado
al ejecutar nuestro programa (leyendo el archivo)
❯ cargo run
datos personales
1. nuevo registro
2. abrir registro
3. salir
opcion: 2
abrir registro
nombre del archivo: demo1.dat
leyendo demo1.dat...
datos leidos: Person { name: "diniremix", age: 250 }
En la próxima entrada, agregaremos soporte para leer y escribir, arrays de registros structs y más!