Alias de tipos

Un alias para un tipo permite darle un nuevo nombre. Normalmente, se usan cuando se tiene un nombre de tipo muy largo que no se quiere escribir cada vez. También sirve para cuando se quiere dar un nombre más apropiado o fácil de recordar en el contexto de la aplicación. A continuación se muestran dos ejemplos de alias.

El siguiente ejemplo sirve para darle un nombre más fácil de comprender en el código:

type CharacterVec = Vec<char>;

fn main() {}

EL siguiente ejemplo sirve para mostrar un caso con un tipo difícil de entender:

// this return type is extremely long
fn returns<'a>(input: &'a Vec<char>) -> std::iter::Take<std::iter::Skip<std::slice::Iter<'a, char>>> {
    input.iter().skip(4).take(5)
}

fn main() {}

Con un alias queda mucho más claro:

type SkipFourTakeFive<'a> = std::iter::Take<std::iter::Skip<std::slice::Iter<'a, char>>>;

fn returns<'a>(input: &'a Vec<char>) -> SkipFourTakeFive {
    input.iter().skip(4).take(5)
}

fn main() {}

Lógicamente, también se puede importar un tipo para hacer las definiones más simples. El ejemplo anterior quedaría así:

use std::iter::{Take, Skip};
use std::slice::Iter;

fn returns<'a>(input: &'a Vec<char>) -> Take<Skip<Iter<'a, char>>> {
    input.iter().skip(4).take(5)
}

fn main() {}

Así que se puede decidir qué es mejor en cada caso.

Los alias no crean un tipo nuevo. Solo se trata de un nuevo nombre que representa al tipo ya existente. Por ello, si se escribe el siguiente código type File = String;, el compilador solo ve el tipo String. Por ello, imprimirá true:

type File = String;

fn main() {
    let my_file = File::from("I am file contents");
    let my_string = String::from("I am file contents");
    println!("{}", my_file == my_string);
}

¿Cómo se haría si realmente se quisiera un tipo nuevo? Lo más simple es usar el tipo de struct que representa tuplas incluyendo el tipo preexistente (Se trata de un uso idiomático de Rust que se denomina newtype).

struct File(String); // File es un envoltorio de String

fn main() {
    let my_file = File(String::from("I am file contents"));
    let my_string = String::from("I am file contents");
}

Ahora, el siguiente código ya no funciona debido a que son dos tipos diferentes:

struct File(String); // File es un envoltorio de String

fn main() {
    let my_file = File(String::from("I am file contents"));
    let my_string = String::from("I am file contents");
    println!("{}", my_file == my_string);  // ⚠️ no se puede comparar File con String
}

Para poder compararlos, hay que recuperar la cadena de texto incluida en File con my_file.0:

struct File(String);

fn main() {
    let my_file = File(String::from("I am file contents"));
    let my_string = String::from("I am file contents");
    println!("{}", my_file.0 == my_string); // my_file.0 es String, así que imprime true
}

El nuevo tipo creado, no tienen ningún rasgo, pero se pueden implementar como siempre.

#![allow(unused)]
fn main() {
#[derive(Clone, Debug)]
struct File(String);
}

En el código anterior, el tipo File se puede clonar y depurar (imprimir sus valores), pero no dispone de los demás rasgos de String. Quien sí sigue teniendo dichos rasgos es la cadena interior .0. Si este código se usara como librería en otros programas, esta cadena interior no estaría accesible, salvo en el caso de que se marcara como pub. Por eso, en esta clase de tipos es habitual el uso del rasgo Deref. Ambos conceptos, pub y Deref, se verán más adelante.

Importar y redenominar dentro de una función

Normalmente, se escribe use al inicio del programa, de esta forma:

use std::cell::{Cell, RefCell};

fn main() {}

Pero es posible, como se ha visto, usarlo en cualquier parte del código. Especialmente en funciones con enumerados que tienen nombres largos. Por ejemplo:

enum MapDirection {
    North,
    NorthEast,
    East,
    SouthEast,
    South,
    SouthWest,
    West,
    NorthWest,
}

fn main() {}

fn give_direction(direction: &MapDirection) {
    match direction {
        MapDirection::North => println!("You are heading north."),
        MapDirection::NorthEast => println!("You are heading northeast."),
        // Hay que teclear mucho... Y seguir tecleando el resto de opciones
    }
}

Sin embargo, es posible simplificar escribiendo un use dentro de la propia función.

enum MapDirection {
    North,
    NorthEast,
    East,
    SouthEast,
    South,
    SouthWest,
    West,
    NorthWest,
}

fn main() {}

fn give_direction(direction: &MapDirection) {
    use MapDirection::*; // Se importa todo en MapDirection
    let m = "You are heading";

    match direction {
        North => println!("{} north.", m),
        NorthEast => println!("{} northeast.", m),
        // Es un poco menos a escribir
        // ⚠️
    }
}

Se ha visto ya que ::* significa que "se importa todo lo que venga después de ::". En este caso, significa que se importa North, NorthEast, ... y así hasta NorthWest. También se puede hacer la importar código de terceros. Esto puede dar lugar a problemas cuando parte de nuestro código tiene nomenclatura idéntica a la de otras librerías. Por eso, no es recomendable usar ::* a menos que se esté seguro de ello.

Algunas veces se observará que existe una sección denominada prelude en el código de otras librerías. Por convenio, esta es la forma de agrupar los elementos que se necesitarán de forma habitual. En este caso, la forma de importarlo recomendada, sí suele ser con *, así: name::prelude::*. Se hablará más de ello en las secciones dedicadas a modules y crates.

También se puede usar as para cambiar los nombres. Por ejemplo, se puede dar el caso de estar utilizando el código de otro desarrollador y no se pueden cambiar los nombres en un enum:

enum FileState {
    CannotAccessFile,
    FileOpenedAndReady,
    NoSuchFileExists,
    SimilarFileNameInNextDirectory,
}

fn main() {}

Así que se puede importar todo y luego cambiarle los nombres.

enum FileState {
    CannotAccessFile,
    FileOpenedAndReady,
    NoSuchFileExists,
    SimilarFileNameInNextDirectory,
}

fn give_filestate(input: &FileState) {
    use FileState::{
        CannotAccessFile as NoAccess,
        FileOpenedAndReady as Good,
        NoSuchFileExists as NoFile,
        SimilarFileNameInNextDirectory as OtherDirectory
    };
    match input {
        NoAccess => println!("Can't access file."),
        Good => println!("Here is your file"),
        NoFile => println!("Sorry, there is no file by that name."),
        OtherDirectory => println!("Please check the other directory."),
    }
}

fn main() {}

Por lo que ahora se puede escribir OtherDirectory en lugar de FileState::SimilarFileNameInNextDirectory.