Utilizando ficheros

Ahora que se está usando Rust en el propio ordenador, se puede comenzar a trabajar con ficheros. Ahora se van a ver muchos Result en el código. Esto es debido a que muchas cosas pueden fallar. El fichero puede no existir, o el ordenador no lo puede leer.

Se puede usar el operador ? que espera recibir un Result de la función a la que se aplique. Si no se recuerda el error que se devuelve, se dejar vacío y dejar que el compilador lo indique. El programa siguiente contiene una función que intenta obtener un número con .parse().

// ⚠️
fn give_number(input: &str) -> Result<i32, ()> {
    input.parse::<i32>()
}

fn main() {
    println!("{:?}", give_number("88"));
    println!("{:?}", give_number("5"));
}

El compilador dice lo que hay que hacer:

error[E0308]: mismatched types
 --> src\main.rs:4:5
  |
3 | fn give_number(input: &str) -> Result<i32, ()> {
  |                                --------------- expected `std::result::Result<i32, ()>` because of return type
4 |     input.parse::<i32>()
  |     ^^^^^^^^^^^^^^^^^^^^ expected `()`, found struct `std::num::ParseIntError`
  |
  = note: expected enum `std::result::Result<_, ()>`
             found enum `std::result::Result<_, std::num::ParseIntError>`

Así, el compilador indica cuál es el tipo de retorno esperado y se puede corregir:

use std::num::ParseIntError;

fn give_number(input: &str) -> Result<i32, ParseIntError> {
    input.parse::<i32>()
}

fn main() {
    println!("{:?}", give_number("88"));
    println!("{:?}", give_number("5"));
}

Ahora el programa funciona:

Ok(88)
Ok(5)

Ahora se quiere utilizar el operador ? para que se devuelva el valor recuperado o el error en caso de que lo haya habido, pero este código no funciona:

// ⚠️
use std::num::ParseIntError;

fn give_number(input: &str) -> Result<i32, ParseIntError> {
    input.parse::<i32>()
}

fn main() {
    println!("{:?}", give_number("88")?);
    println!("{:?}", give_number("5")?);
}

El compilador dice:

error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `std::ops::Try`)
  --> src\main.rs:8:22
   |
7  | / fn main() {
8  | |     println!("{:?}", give_number("88")?);
   | |                      ^^^^^^^^^^^^^^^^^^ cannot use the `?` operator in a function that returns `()`
9  | |     println!("{:?}", give_number("5")?);
10 | | }
   | |_- this function should return `Result` or `Option` to accept `?`

Pero main() si puede devolver un Result, como cualquier otra función. Si la función va bien, no se debe devolver nada en main, pero si falla, debe dar el mismo error. Por lo tanto, el código queda así:

use std::num::ParseIntError;

fn give_number(input: &str) -> Result<i32, ParseIntError> {
    input.parse::<i32>()
}

fn main() -> Result<(), ParseIntError> {
    println!("{:?}", give_number("88")?);
    println!("{:?}", give_number("5")?);
    Ok(())
}

Es importante ahora añadir el Ok(()) como valor de retorno final de main. Esto es muy común en Rust. Significa Ok, dentro del cual está () que es el valor de retorno. Ahora imprime:

88
5

Esto no es muy útil cuando solo se usa .parse(), pero lo será con ficheros. Esto de debe a que ? también modifica los tipos de error. Según indica la página del operador ?:

If you get an `Err`, it will get the inner error. Then `?` does a conversion using `From`. With that it can change specialized errors to more general ones. The error it gets is then returned.

Es decir, si el operador obtiene un Err, se recuperará el error interno. Así con From, ? realiza la conversión de errores especializados a más general. El error que obtiene así, es el que se devuelve.

Además, Rust tiene un tipo Result especializado para los Files (ficheros) o similar. Es el tipo std::io::Result y es lo típico que se observará como retorno de una función main() cuando se están manipulando ficheros y usando ?. Este tipo Result es un alias de:

#![allow(unused)]
fn main() {
type Result<T> = Result<T, Error>;
}

Es un Result<T, Error>, pero no es necesario escribir la parte del errro, solo Result<T>.

A continuación, se presenta el primer ejemplo que trabaja con ficheros. std::fs es el módulo que contiene los métodos para trabajar con ficheros. std::io::Write permite escribir contenido a ficheros. Se puede usar el método .write_all() para escribir valores a un ficehro.

use std::fs;
use std::io::Write;

fn main() -> std::io::Result<()> {
    let mut file = fs::File::create("myfilename.txt")?; // Crea un fichero con este nombre.
                                                        // CUIDADO: si existe un fichero previamente con ese nombre,
                                                        // se eliminará todo su contenido previo.
    file.write_all(b"Let's put this in the file")?;     // No se debe olvidar la b delante de ". Esto se debe a que un fichero recibe bytes.
    Ok(())
}

Este código consiste en: intentar crear un fichero y comprobar si ha funcionado. Si funciona, utiliza .write_all() y comprueba que haya funcionado bien.

De hecho, existe una función que hace las dos cosas juntas. Crea un fichero y escribe el contenido que se indique. Se denomina std::io::write. Se le pasa como parámetros el nombre del fichero deseado y el contenido que se quiere incorporar. Como antes, es importante tener cuidado ya que si el fichero existiera previamente, se borraría todo su contenido anterior. Además, permite escribir un &str sin necesidad de utilizar b delante. Esto se debe a que la función se define así:

#![allow(unused)]
fn main() {
pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()>
}

El segundo parámetro, el contenido, debe implementar un array de binarios AsRef<u8>. Por eso se puede pasar un &str sin hacer conversión explícita a binario.

Se puede ver en el siguiente ejemplo:

use std::fs;
fn main() -> std::io::Result<()> {
    fs::write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...")?;

    Ok(())
}

En este programa se crea un fichero que será el que se usará en los siguientes ejemplos, es una conversión de un personaje de un libro de comic llamado Calvin que habla con su padre, que no se toma la pregunta en serio. Con este código, se puede crera un fichero que se usará en varios ejemplos.

Abrir un fichero, leer de él, es tan sencillo como crearlo. Solo se necesita usar open() en lugar de create(). Después de esto, si el fichero existía, se pueden usar funciones como read_to_string(). Para esto es necesario disponer de una mut String y leer del fichero a ella. Por ejemplo:

use std::fs;
use std::fs::File;
use std::io::Read; // Este módulo es necesario para disponer de la función .read_to_string()

fn main() -> std::io::Result<()> {
     fs::write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...")?;


    let mut calvin_file = File::open("calvin_with_dad.txt")?; // Abre el fichero recién creado
    let mut calvin_string = String::new(); // Esta String guardará el contenido
    calvin_file.read_to_string(&mut calvin_string)?; // Lee el fichero a la String

    calvin_string.split_whitespace().for_each(|word| print!("{} ", word.to_uppercase())); // Pasa a mayúsculas e imprime cada palabra de la cadena de caracteres

    Ok(())
}

El resultado del programa es:

CALVIN: DAD, HOW COME OLD PHOTOGRAPHS ARE ALWAYS BLACK AND WHITE? DIDN'T THEY HAVE COLOR FILM BACK THEN? DAD: SURE THEY DID. IN 
FACT, THOSE PHOTOGRAPHS *ARE* IN COLOR. IT'S JUST THE *WORLD* WAS BLACK AND WHITE THEN. CALVIN: REALLY? DAD: YEP. THE WORLD DIDN'T TURN COLOR UNTIL SOMETIMES IN THE 1930S...

Si se quiere crear un fichero, pero solo si no existe otro con el mismo nombre, existe una struct denominada OpenOptions. En realidad, ya se ha estado usando este struct. Por ejemplo, la definición de File::open dice:

#![allow(unused)]
fn main() {
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<File> {
        OpenOptions::new().read(true).open(path.as_ref())
    }
}

Esta definición parece seguir el patrón del constructor que se aprendió antes. Lo mismo sucede para File::create:

#![allow(unused)]
fn main() {
pub fn create<P: AsRef<Path>>(path: P) -> io::Result<File> {
        OpenOptions::new().write(true).create(true).truncate(true).open(path.as_ref())
    }
}

Si se consulta página de OpenOptions, se pueden ver todos los métodos que existen. La mayoría necesitan un valor bool:

  • append(): este método indica que se añada al contenido anterior, en lugar de borrarlo.
  • create(): crea un fichero.
  • create_new(): solo crea el fichero si no existe previamente.
  • read(): para poder leer del fichero, se necesario ponerlo a true.
  • truncate(): si se pone a true se elimina todo el contenido del fichero cuando se abre.
  • write(): permite escribir en un fichero.

Como último método se usa .open() con el nombre del fichero que devuelve un Result. Por ejemplo:

// ⚠️
use std::fs;
use std::fs::OpenOptions;

fn main() -> std::io::Result<()> {
     fs::write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...")?;

    let calvin_file = OpenOptions::new().write(true).create_new(true).open("calvin_with_dad.txt")?;

    Ok(())
}

En primer lugar se crea un OpenOptions con new. Después se le indica que se puede escribir con write. Después de eso create_new() a true e intenta abrir el fichero. En este caso no funciona ya que ya existe el fichero por lo que lanza el siguiente error:

#![allow(unused)]
fn main() {
Error: Os { code: 80, kind: AlreadyExists, message: "The file exists." }
}

Para que sí funcione, se debe usar .append() para este caso. Para escribir al fichero, se puede usar .write_all(), que intenta escribir todo lo que se le pase.

También se va a usar la macro write! para hacer lo mismo. Esta macro ya se ha usado cuando se usó impl Display para construir struct. Ahora se usará en un fichero en lugar de un buffer.

use std::fs;
use std::fs::OpenOptions;
use std::io::Write;

fn main() -> std::io::Result<()> {
    fs::write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...")?;

    let mut calvin_file = OpenOptions::new()
        .append(true) // Ahora se puede escribir sin borrar el contenido anterior
        .read(true)
        .open("calvin_with_dad.txt")?;
    calvin_file.write_all(b"And it was a pretty grainy color for a while too.\n")?;
    write!(&mut calvin_file, "That's really weird.\n")?;
    write!(&mut calvin_file, "Well, truth is stranger than fiction.")?;

    println!("{}", fs::read_to_string("calvin_with_dad.txt")?);

    Ok(())
}

Que imprime:

Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...And it was a pretty grainy color for a while too.
That's really weird.
Well, truth is stranger than fiction.