Introducción

La programación de sistemas es aquella cuya finalidad es la de programar software que utilizarán otros programas. Un sistema operativo, un servidor HTTP o un motor de juegos son ejemplos de sistemas.

Este taller de Rust para absolutos novatos presenta y explica las características de Rust tratando de no compararlo con otros lenguajes. El texto, no obstante, asume que alguna vez has programado, aunque haya sido con lenguajes dinámicos o Web.

Comencemos con un sencillo programa de ejemplo para enumerar algunas características de la sintaxis de Rust.

fn main() {
    let name: String = "Salva".to_string();
    let greeting_length = greeting(name);
    println!("{}", greeting_length);
}

fn greeting(message: String) -> usize {
    println!("Hello, {}", message);
    7 + message.chars().count()
}

En Rust, las funciones comienzan con la palabra clave fn y la función llamada main es el punto de entrada.

Las variables se declaran con let acompañado de un identificador y una inicialización obligatoria. El tipo String, indicado entre los dos puntos : y el igual = es opcional.

Antes de continuar, no puedo dejar de enfatizar que los sistemas de tipos no existen para hacer la programación más complicada, sino que son herramientas de diseño: nos exigen coherencia en el software que construímos.

Rust es un lenguaje con un sistema de tipos modernos que permite omitir casi cualquier anotación de tipos. Rust intentará adivinar el tipo por el contexto, aplicando una técnica llamada inferencia de tipos como ocurre con greeting_length. Sólo en la signaturas de las funciones, Rust exige que los tipos se especifiquen explícitamente.

fn main() {
    let v = vec!();  // Rust no puede deducir de qué clase de vector se trata.
    v.push(1);       // Pero aquí, Rust sabe que v es un vector de enteros.
    v.push("mundo"); // Y esto falla: "mismatched types".
}

Ademas, ante el error, el compilador de Rust siempre intentará ayudarnos, emitiendo la suficiente información para contextualizar el error, explicarlo y solucionarlo. En Rust hay que acostumbrarse a leer los mensajes de error y colaborar con el compilador:

error[E0308]: mismatched types
 --> src/main.rs:5:12
  |
5 |     v.push("mundo"); // Y esto falla: "mismatched types".
  |            ^^^^^^^ expected integral variable, found reference
  |
  = note: expected type `{integer}`
             found type `&'static str`

Volviendo a la sintaxsis, podemos llamar métodos sobre valores usando el operador . como ocurre en .to_string() o en .chars().count(). La llamada a .to_string() es necesaria porque los literales de cadena tienen otro tipo por defecto. En los ejemplos también te encontrarás esto escrito format!("Literal de cadena").

El identificador println! no es una función al uso sino una macro. Una macro es un tipo de función que se ejecuta en tiempo de compilación y produce código Rust que reemplaza a la llamada.

Sin embargo, greeting sí que es una función más, como main. En la signatura de la función, los tipos son obligatorios. Debemos indicar qué tipo de datos se esperan como parámetros y el tipo de salida.

El valor de retorno de una función puede expresarse de dos maneras: o bien explícitamente mediante la palabra clave return, o bien, de forma más común omitiendo el punto y coma al final de la última línea.

En Rust, el punto y coma ; es un operador especial, que sólo puede omitirse al final de la última expresión, y que, de estar presente, anula el resultado de dicha expresión.

Tutorial para principiantes

Uno de los principales recursos de un ordenador es la memoria. La memoria es un espacio direccionable en el que se almacenan tanto los propios programas en ejecución como los datos con los que dicho programas van a trabajar.

Los programas en ejecución conviven juntos en memoria y es posible que un programa malintencionado invada el espacio de otro con el fin de controlarlo. En Rust, esta situación es imposible y por eso se dice de él que es un lenguaje de programación seguro.

Además, la memoria es un bien escaso. Los lenguajes de sistemas tratan de ocupar poco espacio para que sea el software al que dan servicio el que disfrute del grueso de la misma.

Para lograr estos objetivos, Rust posee los siguientes mecanismos, que exploraremos en esta primera parte del tutorial: propiedad, préstamo y control de la mutabilidad.

Prueba a realizar el ejercicio 1 para ir familiarizándote con el lenguaje.

Propiedad

La propiedad (u ownership, en inglés) representa el los derechos de lectura y modificación; así como el deber de destruir un objeto. La propiedad la tiene la función que crea el objeto y, por defecto, la propiedad se adquiere sólo con permiso de lectura pero no de escritura.

A menos que se especifique lo contrario, cuando pasamos un dato como parámetro, transferimos la propiedad del dato a la función invocada, de forma que ya no podremos volver a acceder al mismo dato desde la función invocante. A esto se le llama "mover un valor":

fn main() {
    let name: String = "Salva".to_string();
    greeting(name);
    yell(name);      // error, "use of moved value: `name`"
}

fn greeting(message: String) {
    println!("Hello, {}", message);
}

fn yell(message: String) {
    println!("{}!!!", message);
}

El compilador de Rust es bastante claro al indicar qué ha pasado:

error[E0382]: use of moved value: `name`
 --> src/main.rs:4:10
  |
3 |     greeting(name);
  |               ---- value moved here
4 |     yell(name);
  |          ^^^^ value used here after move
  |
  = note: move occurs because `name` has type `std::string::String`, which does not implement the `Copy` trait

Para que el código anterior compile, en lugar de mover el valor en la línea 3, podemos mover una copia del valor haciendo uso del método .clone():

fn main() {
    let name: String = "Salva".to_string();
    greeting(name.clone()); // Ya no movemos `name` sino una copia de `name`.
    yell(name);
}

fn greeting(message: String) {
    println!("Hello, {}", message);
}

fn yell(message: String) {
    println!("{}!!!", message);
}

Conviene hacer notar que igual que movemos un valor hacia el interior de una función, también es posible mover el valor hacia el exterior, basta con usar el valor como parte de la expresión de retorno:

fn main() {
    let name: String = "Salva".to_string();
    let former_name = greeting(name);
    yell(former_name);
}

fn greeting(message: String) -> String {
    println!("Hello, {}", message);
    message
}

fn yell(message: String) -> String {
    println!("{}!!!", message);
    message
}

Antes de terminar, existe una notable excepción al comportamiento por defecto de Rust. Observa como no es necesario llamar a .clone() sobre un número:

fn main() {
    let number = 42;
    greeting(number); // Funciona.
    yell(number);     // También funciona.
}

fn greeting(message: i32) {
    println!("Hello, {}", message);
}

fn yell(message: i32) {
    println!("{}!!!", message);
}

En Rust, algunos tipos pueden marcarse como Copy, que significa que cada vez que se pasan, se realiza una copia implícita.

Echa un vistazo a las diapositivas de este tema, las cuales ilustran lo que ocurren en caso de movimiento, copia (clone) y copia implícita (copy) de un valor.

Practica lo que has aprendido con el ejercicio 2. Recuerda que el compilador de Rust es tu aliado, lee los mensajes de error con atención para obtener pistas acerca de cómo completar el ejercicio.

Préstamo

Existe una forma de evitar el movimiento de un valor y aun así no tener que copiarlo, simplificando el uso de nuestras funciones. Para ello Rust permite "dar en préstamo" un valor. Puedes pensar que "dar en préstamo" es realmente "conceder permisos de lectura o escritura, temporalmente". Esta concesión se realiza por medio de referencias y, a menos que es especifique explícitamente, el acceso resultante es de sólo lectura:

fn main() {
    let name: String = "Salva".to_string();
    greeting(&name); // Pasamos una referencia a la función.
    yell(&name);     // Pasamos otra referencia a la función.
                     // El valor en `name` NUNCA se movió.
}

fn greeting(message: &String) {
    println!("Hello, {}", message);
}

fn yell(message: &String) {
    println!("{}!!!", message);
}

Fíjate en la sintáxis: el tipo &String significa "referencia a String" y el mismo ampersand & acompañado de un valor como en &name significa "tomar una referencia al valor". Al pasar, devolver o asignar referencias estamos "prestando el valor".

Las referencias son parte del tipo. String y &String son tipos distintos de la misma forma que las expresiones "Salva".to_string() y &"Salva".to_string() devuelven valores de tipos distintos.

Los préstamos evitan el uso de copias sin tener que mover el valor de vuelta, lo que resulta en usos más naturales de las funciones.

Además los préstamos no transfieren la propiedad. El encargado de destruir el dato creado lo mantiene la función desde la que se tomó el préstamo. El préstamo termina cuando la referencia se destruye, normalmente al final de la función.

Puedes pensar en una referencia como en un "portal mágico" por el que mirar a otro lugar. El portal a un valor se obtiene prefijando el valor con &. La función que crea el portal pasa una copia del portal a las funciones llamadas pero no toca el lugar original. Cuando la función que ha usado el portal termina, también destruye su copia del portal lo que no afecta a aquello que se viera a través de él.

Trata de adaptar tú el siguiente ejemplo.

Es importante recordar que el acceso por defecto de Rust es de sólo lectura. Esto quiere decir que, a menos que se especifique explícitamente, no podemos alterar el valor de una variable...

fn main() {
    let name = "Salva".to_string();
    name.push('!'); // error, "cannot borrow immutable local variable `name` as mutable"
}

...ni de una referencia:

fn main() {
    let name = "Salva".to_string();
    emphasis(&name);
}

fn emphasis(message: &String) {
    message.push('!'); // error, cannot borrow immutable borrowed content `*message` as mutable
}

Sintáxis de alto nivel y eficiencia de bajo nivel

Además de un lenguaje de sistemas, Rust es un lenguaje moderno, de alto nivel por lo que ofrece estructuras sintácticas avanzadas al tiempo que evita copias o reservas de memoria.

Es el caso con las porciones (comunmente conocidas como slices), en las que nos referimos a un subrango de un vector:

fn main() {
    let name: String = "Salva".to_string();
    greeting(&name);
    greeting(&name[1..]);    // Esto es una porción o slice, desde el segundo
                             // elemento en adelante.
}

fn greeting(message: &str) { // Hemos cambiado la signatura de la función aquí.
    println!("Hello, {}", message);
}

O también de las iteraciones:

fn main() {
    let names: String = "Salva Fernando Juan".to_string();
    greeting(&names);
}

fn greeting(message: &str) {
    for name in message.split(' ') { // No hay copia alguna de los datos.
      println!("Hello, {}", name);
    }
}

Ninguno de los ejemplos anteriores involucra copias de ningún modo. Puedes repasar las diapositivas de este tema que incluyen representaciones de la memoria en estos casos para comprobar que las copias no son necesarias.

Cuando creas conveniente, practica con el siguiente ejercicio.

Préstamos mutables

Es posible pasar una referencia mutable a una función. O visto de otra forma, ¿recuerdas el "portal mágico" de la lección anterior? Pues es posible pasar un portal a través del cual podamos acceder físicamente y manipular el objeto al otro lado.

fn main() {
    let name = "Salva".to_string();
    complete(&mut name);
    println!("Complete name: {}", name);
}

fn complete(s: &mut String) {
    s.push_str("dor");
}

Fíjate en la nueva sintáxis: ahora el tipo esperado en la función complete() es &mut String o "referencia mutable a String", y de la misma forma, la expresión name se precede de &mut que obtiene una referencia mutable al valor.

El ejemplo anterior no funciona. El compilador de Rust dice:

error[E0596]: cannot borrow immutable local variable `name` as mutable
 --> src/main.rs:4:19
  |
3 |     let name = "Salva".to_string();
  |         ---- consider changing this to `mut name`
4 |     complete(&mut name);
  |                   ^^^^ cannot borrow mutably

Esto es así porque si realizamos un préstamo mutable, debemos aceptar que el valor name (del que aun tenemos la propiedad) pueda cambiar y debemos indicarlo explícitamente:

fn main() {
    let mut name = "Salva".to_string(); // fíjate en que ahora name es mutable.
    complete(&mut name);
    println!("Complete name: {}", name);
}

fn complete(s: &mut String) {
    s.push_str("dor");
}

Antes de continuar, repasa los préstamos sin mutabilidad haciendo que este ejemplo compile.

Preservando la seguridad

Prestar valores indistintamente para lectura o escritura entraña diversos riesgos: en un escenario concurrente donde las lecturas y escrituras de varias funciones pueden entremezclarse, es posible que los valores se modifiquen justo después de que las lecturas se produzcan y se tomen decisiones en función de datos desactualizados, llevando a falsas asunciones y a resultados incorrectos.

En un escenario sin concurrencia, la alteración de algunos valores puede llevar a recolocarlos en la memoria haciendo que otras referencias a los mismos queden desactualizadas.

Para evitar estas situaciones, Rust establece una regla:

  • No puede haber, jamás, un préstamo de lectura y otro de escritura simultaneos.

Si tal situación pudiera producirse, Rust no compilará el programa. Conviene recordar que la obtención de una referencia mutable confiere también un permiso de lectura por lo que no pueden producirse dos préstamos mutables al mismo tiempo. En términos de mutabilidad, esta regla se traduce en dos:

  1. Un único préstamo mutable...
  2. ...o múltiples préstamos inmutables y ninguno mutable.

Por ejemplo:

fn main() {
    let mut name = "Salva".to_string();
    let end = &name[1..];
    complete(&mut name);
    println!("Hello, {}!", name);
}

fn complete(name: &mut String) {
    name.push_str("dor");
}

El código anterior falla al compilar porque "no se puede prestar name como mutable porque también se ha prestado como inmutable":

error[E0502]: cannot borrow `name` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:19
  |
3 |     let end = &name[1..];
  |                ---- immutable borrow occurs here
4 |     complete(&mut name);
  |                   ^^^^ mutable borrow occurs here
5 |     println!("Hello, {}!", name);
6 | }
  | - immutable borrow ends here

Las diapositivas sobre mutabilidad explican con ejemplos visuales una situación en la que un valor crece hasta el punto de tener que ser recolocado en memoria.

Primero completa el ejercicio de esta lección y luego trata de responder a la pregunta planteada antes de probar a compilar bajo el nuevo supuesto.

results matching ""

    No results matching ""