DEV Community

loading...
Cover image for Un tour rápido por Rust.

Un tour rápido por Rust.

psychecat profile image Psyche Cat Updated on ・33 min read

La foto fue tomada por Manuel Sardo y publicada en Unplash

Tabla de contenidos

Variables

Las variables son los pilares fundamentales para cualquier lenguaje de programación, pero que es una variable sin tipos primitivos, Rust es un lenguaje de tipado estricto lo que significa que cada variable debe tener asociada un tipo de dato específico, ahora veremos los tipos de datos primitivos presentes en Rust

Tipos primitivos

  • bool : Son tipos de datos booleanos que pueden ser true o false
  • char : Son tipos que representan caracteres como 'a'
  • integers: En los tipos de datos enteros podemos encontrar una gran variedad de tipos los cuales tienen un tamaño de bits y pueden ser con o sin signo

    • i8: Entero con signo de 8 bits
    • i16: Entero con signo de 16 bits
    • i32: Entero con signo de 32 bits
    • ì64: Entero con signo de 64 bits
    • i128: Entero con signo de 128 bits
    • u8: Entero sin signo de 8 bits
    • u16: Entero sin signo de 16 bits
    • u32: Entero sin signo de 32 bits
    • u64: Entero sin signo de 64 bits
    • u128: Entero sin signo de 128 bits
  • isize: Es un número entero con su tamaño basado en el tamaño del puntero en CPUs de 32bits es equivalente un - - i32 y en CPUs de 64bits es equivalente a un i64

  • f32: Es la representacion de un número decimal de 32 bits de acuerdo al estándar IEEE 754

  • f64: Es la representacion de un número decimal de 64bits

  • [T:N]: Es un array de tamaño fijo para un grupo de elementos del tipo T y una cantidad de espacios definidas N que debe ser positiva a tiempo de compilación

  • [T]: Una vista de tamaño dinámico con dentro de una secuencia contigua que contiene variables del tipo T

  • str: String Slices, típicamente se usan como referencias &str

  • (T, U, ..): Una secuencia finita (T, U, ..) donde T y U pueden ser variables de distintos tipos

  • fn(i32) -> i32: Una función que toma un valor de tipo i32 que devuelve una variable del tipo i32

Variables e Inmutabilidad

En Rust declaramos variables con la palabra reservada let y con un identificador, pero las variables en Rust son distintas.

En muchos lenguajes tenemos variables mutables e inmutables, por ejemplo en Python las colecciones de datos son tipos mutables, pero las tuplas son tipos inmutables por defecto, por lo que si intento cambiar el valor de una tupla probablemente me dé un error, Rust es distinto porque todas las variables que declaramos son inmutables por defecto lo que significa que su valor no puede ser cambiado en tiempo de ejecución a menos que se le indique de forma explícita, para hacer eso podemos poner la palabra mut luego de la palabra let

let target = "World"; // Variable inmutable
let mut greeting = "Hello"; // Variable mutable

println!("{} {}", greeting , target) // Hello World
Enter fullscreen mode Exit fullscreen mode

Como podemos ver este programa corre sin problema, pero ¿qué pasaría si intentamos cambiar el valor de la variable target?

target = "Hello";
println!("{}", target)

Enter fullscreen mode Exit fullscreen mode
error[E0384]: cannot assign twice to immutable variable `target`
 --> src/main.rs:7:5
  |
3 |     let target = "World"; // Variable inmutable
  |         ------
  |         |
  |         first assignment to `target`
  |         help: make this binding mutable: `mut target`
...
7 |     target = "Hello"
  |     ^^^^^^^^^^^^^^^^ cannot assign twice to immutable variable

error: aborting due to previous error; 2 warnings emitted

For more information about this error, try `rustc --explain E0384`.
error: could not compile `playground`

To learn more, run the command again with --verbose.
Enter fullscreen mode Exit fullscreen mode

En este caso nos envía un error, porque estamos intentando asignar otro valor a una variable inmutable, y en el caso de la variable greeting, ¿Qué pasaría si le cambiamos el valor?

greeting = "Hola";
println!("{}", greeting) // Hola
Enter fullscreen mode Exit fullscreen mode

Podriamos hacerlo sin problemas porque la variable greeting es una variable mutable, porque nosotros lo indicamos con la palabra mut que pusimos luego del let, por lo que podemos solucionar el error anterior haciendo mutable a nuestra variable target en el momento donde la declaramos y esto funcionaría

let mut target = "World"; // Variable mutable
let mut greeting = "Hello"; // Variable mutable

println!("{} {}", greeting , target);

// Podemos reasignar los valores de las variables mutables
target = "Programmer";
greeting = "Bye";

println!("{} {}", greeting, target) // Bye Programmer
Enter fullscreen mode Exit fullscreen mode

Funciones

Las funciones son una forma de extraer líneas de código que pueden resultar repetitivas, por ejemplo si tengo una función para sumar 2 números con módulo 511, para no repetir este proceso puedo escribir una función que lo haga cada vez que lo necesito:

fn mod_32_sum(a: i32, b: i32) -> i32 {
    (a + b) % 511
}
let e = mod_32_sum(6 << 21, 2 << 21);
let f = mod_32_sum(6 << 21, 8 << 31);
println!("e: {} f: {}", e, f); // e: 64 f: 48
Enter fullscreen mode Exit fullscreen mode

Primero lo que podemos ver es la palabra clave fn, la palabra reservada fn nos permite indicar al compilador de Rust que vamos a definir una función, y lo siguiente es el nombre de la función o identificador de la función que puede ser cualquier nombre, las guías de estilo de rust dicen que todos los nombres de identificadores deben ser definidos en minusculas y con guiones bajos para separar cada palabra a esto se le denomina snake_case, pero las convenciones de estilo seran discutidas al final de este artículo.

Luego viene la declaración de parámetros, estos parámetros son los nombres de las variables dentro de nuestra función, en este caso estamos pidiendo dos números enteros a y b del tipo i32 que es el tipo por defecto para los números enteros para en el compilador de rust, cada parámetro de la función tiene que ser anotada con un tipo de datos.

Por último tenemos el tipo de retorno, que en este caso es un i32, esto significa que lo que devuelve la función tiene que ser a fuerzas un valor de tipo i32 si no lo es vas a tener un error como el siguiente:

fn mod_32_sum(a: i32, b: i32) -> i32 {
    ((a + b) % 511) as i64
}
let e = mod_32_sum(6 << 21, 2 << 21);
let f = mod_32_sum(6 << 21, 8 << 31);
println!("e: {} f: {}", e, f);
Enter fullscreen mode Exit fullscreen mode
        ((a + b) % 511) as i64

        ^^^^^^^^^^^^^^^^^^^^^^ expected `i32`, found `i64`

    fn mod_32_sum(a: i32, b: i32) -> i32 {

                                     ^^^ expected `i32` because of return type

    mismatched types

    help: you can convert an `i64` to an `i32` and panic if the converted value doesn't fit

    (((a + b) % 511) as i64).try_into().unwrap()
Enter fullscreen mode Exit fullscreen mode

Luego tenemos el cuerpo de la función que en este caso corresponde a una línea de código, si nosotros no ponemos el ; (Punto y coma) en la ultima linea de codigo y tampoco la asignamos a una variable, el compilador reconoce como el valor de retorno de la función. En caso de que tengamos que devolver algo antes de terminar la función usamos la palabra return por ejemplo si tenemos una función que tiene que devolver un valor a través de una condicional:

fn is_even(a:u8) -> bool {
    if a % 2 == 0 {
        return true
    }
    false // el valor de retorno en todos los otros casos
}
Enter fullscreen mode Exit fullscreen mode
is_even(2) // true
Enter fullscreen mode Exit fullscreen mode

Si nosotros no especificamos un valor de retorno Rust por defecto va a retornar un tipo Unit o (), el cual se denota por un paréntesis vacío y no se debe indicar de manera explícita, esto es equivalente definir una función de tipo void en C++ o Java. Vamos a ver un ejemplo:

// Funcion con tipo de retorno Unit explicito
fn say_hello() -> () {
    println!("Hello")
say_hello(); //  Hello
}
Enter fullscreen mode Exit fullscreen mode

Es exactamente lo mismo que esto

fn say_hello() {
    println!("Hello")
}

say_hello(); //    Hello
Enter fullscreen mode Exit fullscreen mode

Ni una tiene una ventaja por sobre la otra.

Por último podemos pasar a la función un valor mutable pudiéndolo cambiar dentro de la función sin necesidad de tener que copiarlo o clonarlo hacia otra variable mutable. Si quiero hacer esto tendría que hacer algo como:

fn sum_2(a: i32) {
    let mut b = a + 2;
    println!("{}", b)
}
Enter fullscreen mode Exit fullscreen mode

Pero para poder reasignar a la variable que uso como argumento podria declararla como mutable:

fn sum_2(mut a: i32) {
    a = a + 2;
    println!("{}", a)
}
Enter fullscreen mode Exit fullscreen mode

Como acabamos de ver, lo que hicimos fue aumentar el valor del parámetro a directamente porque era una referencia mutable, de no haber sido deberiamos haber tenido que reasignar a a otra variable mutable. Luego de haber visto esto vamos a discutir un poco sobre las Closures.

Closures

Las closures, son funciones anónimas pueden ser asignadas a una variable, o también pueden ser pasadas como argumentos de una función, pero en un artículo futuro vamos a discutir más sobre ellas a detalle.

La closure mínima que podemos hacer es la siguiente let my_closure = || () esta closure no toma ni un parámetro y no hace nada, y podemos ejecutarla como si fuera una función normal my_closure(), pero no tenemos la necesidad de definirla con la palabra fn, podemos definir parámetos que reciba nuestra closure dentro de las barras verticales, por ejemplo |x|. Rust intentará inferir el tipo a partir del comportamiento de la variable dentro de la closure, pero hay veces que no es así y el compilador nos avisara con un mensaje de error, que necesita que especifiquemos el tipo del parámetro y se puede definir de igual forma que en las funciones |x: i32|.

Si nuestra closure ejecuta una operación simple podemos declarar el cuerpo de nuestra closure un una sola línea de la siguiente manera.

let doubler = |x| x * 2;
let a = 2;
let b = doubler(a);
println!("{}", b); // 4
Enter fullscreen mode Exit fullscreen mode

En caso de que debamos definir una con más de una instrucción debemos encerrarla en un bloque de código por ejemplo:

let sum_and_multiply = |a, b| {
    let z = a + b;
    z * a + b * 2
};
let c = sum_and_multiply(10, 2);
println!("{}", c); // 124
Enter fullscreen mode Exit fullscreen mode

Como hablamos anteriormente las closures también pueden ser vistas como parámetros para funciones de mayor orden. Las funciones de mayor orden son funciones que toman otras funciones como parámetros por ejemplo la función thread::spawn es una función que toma una closure como parámetro, o también para las sintaxis funcionales, como el caso de map y filter que toman una función para aplicarla a un iterador o filtrar de acuerdo a una closure que devuelva un bool, pero como esto es solo un resumen en otro articulo veremos las closures más a profundidad.

Cadenas de texto

Las cadenas de texto se encuentran en dos formas distintas, en forma de String que es una cadena de texto y el tipo &str o String Slice (Se habla mas de las string slices en la sección Slices), Rust acepta para el tipo String cualquier secuencia de caracteres válidos según el estándar UTF-8, tampoco son cadenas terminadas en NULL tambien como en C pueden tener Bytes nulos entre sus caracteres, ahora veamos los dos tipos en acción

let string = String::from("こんにちは"); // String
let slice = "Hello World"; // &str
let str_to_string: String = "Me".to_string();
println!("String:{}\nSlice: {}\nToString: {}", string, slice, str_to_string)
/*
    String:こんにちは
    Slice: Hello World
    ToString: Me
*/
Enter fullscreen mode Exit fullscreen mode

Los tipos String y &str tienen una referencia, en el caso de las String están alocadas en el heap y por lo general los tipos &str son referencias a cadenas de texto existentes que puede estar en el stack en el heap. El operador & es para crear un puntero apuntando variable ya existente de cualquier tipo de dato. Luego de inicializar las variables usamos la macro println!() para poder mostrar las variables por la salida estándar. En el futuro también pienso explicar más a fondo como funcionan las Cadenas de carácteres en Rust, por ahora movámonos a la siguiente sección.

Condicionales y Control de flujo

En Rust tenemos como en muchos lenguajes if y else las cuales son expresiones para tomar una decisión a partir de una variable o un dato de entrada

let is_true = true;
if is_true {
    println!("Is true");
} else {
    println!("Isn't true");
}
// Is true
Enter fullscreen mode Exit fullscreen mode

A diferencia de otros lenguajes el constructo if en Rust no es una declaración sino una expresion, la diferencia entre estos dos conceptos es que una declaración, no devuelve ni un valor, y las expresiones como todos sabemos devuelven un valor, por ejemplo 5 es una expresión que devuelve el valor 5, en este caso el constructo if - else devuelven un valor de un tipo específico, o un valor de tipo Unit y pueden ser asignados directamente a una variable de la siguiente manera.

let number = 10;
let even_or_odd = if number % 2 == 0 {
    String::from("Even")
} else {
    String::from("Odd")
};

println!("Even Or Odd: {}", even_or_odd);
// Even Or Odd: Even
Enter fullscreen mode Exit fullscreen mode

Por esta razón en Rust no se puede tener una expresión if sin un else si lo estamos asignando directamente a una variable a menos que el tipo que retorne el if sea siempre el mismo esto pasa con el tipo Unit, por lo que podemos hacer lo siguiente.

let number = 10;
let a = if number % 2 == 0 {
    println!("Is Even")
};

println!("a is a unit: {:?}", a);
/*
    Is Even
    a is a unit: ()
*/
Enter fullscreen mode Exit fullscreen mode

Nota: Cuando ponemos el placeholder {:?} dentro de una cadena en un println! significa que estamos diciendole a la macro que queremos que nos muestre el valor en modo debug, y esto pasa porque el objeto no tiene el trait Display declarado pero eso lo veremos en un articulo futuro

Pero si estamos retornando algo, para asignarlo a una variable no podemos obviar la cláusula else, y podremos ver el siguiente error:

let number = 10;
let a = if number % 2 == 0 {
    number + 1
};
println!("a: {}", number);
Enter fullscreen mode Exit fullscreen mode
    let a = if number % 2 == 0 {
        number + 1
    };
             expected `()`, found integer

number + 1
        ^^^^^^^^^^ found here
    `if` may be missing an `else` clause
    help: consider adding an `else` block that evaluates to the expected type
Enter fullscreen mode Exit fullscreen mode

Y eso pasa porque Rust al ver que no hay una clausula else está infiriendo el tipo de la variable a como un Unit esta recibiendo un número de tipo i32 lo cual causa un conflico con los tipos, no podemos asignar un valor de tipo i32 a una variable de tipo Unit, para evitar que esto pase podemos poner una cláusula else con un valor por defecto, en este caso podríamos hacer lo siguiente:

let number = 10;
let a = if number % 2 == 0 {
    number + 1
} else {
    number
};

println!("a: {}", a) //     a: 11
Enter fullscreen mode Exit fullscreen mode

Rust además del if tiene una expresión que sirve para evaluar condiciones múltiples y es bastante poderosa la cual se llama match, la cual tiene bastantes usos, y es el próximo tema a tratar.

Expresión Match

En Rust hay una expresión que se ve con bastante frecuencia la cual se llama expresion match, la cual es bastante cómodo usar para evaluar multi-condicionales, es como una expresión switch en Java o C++ pero en esteroides, primero veamosla en acción.

// Simulamos un request
fn request() -> i32 {
    200
}

let value = match request() {
    200 => String::from("The request was sucessfull."),
    404 => String::from("The request failed."),
    other => format!("Request failed with the code: {}", other)
};

println!("{}", value)

//  The request was sucessfull.
Enter fullscreen mode Exit fullscreen mode

Lo que está pasando aquí es que la expresion match devuelve un valor dependiendo el valor que estemos evaluando, en este caso es el retorno de nuestra funcion request, cada una de las expresiones con la forma valor => <expresion o bloque de codigo> se llaman brazos, y cada brazo puede devolver un valor o un Unit que será asignado a la variable en este caso value.

Al igual que el if y else, tenemos que cubrir todos los posibles estados para el valor que estamos evaluando con la misma, pero imagina hacer un brazo para cada valor posible de un i32. Convenientemente tenemos una cláusula que se denomina catch all que puede ser denotado con un identificador o si no la vamos a usar puede ser denotada con un _, en este caso nuestra cláusula catch all el identificador other al que se le va a asignar cualquier valor que no sea el de los brazos especificados y podemos ocuparla dentro del bloque de código o expresión de nuestro caso, vamos a ver que pasa si devolvemos otro valor:

// Simulamos un request
fn request() -> i32 {
    303
}

let value = match request() {
    200 => String::from("The request was sucessfull."),
    404 => String::from("The request failed."),
    other => format!("Request failed with the code: {}", other)
};

println!("{}", value)
//    Request failed with the code: 303
Enter fullscreen mode Exit fullscreen mode

En este caso el brazo que se está ejecutando es el último.
Las expresiones match pueden hacer aun más cosas, pero las vamos a discutir un artículo futuro. Ahora vamos a discutir la siguiente herramienta en nuestra caja, los bucles.

Bucles

Rust tiene 3 tipos de bucles que típicamente se ven en otros lenguajes de programación y tienen sentencias de control como es típico, las cuales son break para romper el bucle y el continue que es para continar con la siguiente iteración y no ejecutar las sentencias que siguen en nuestro bucle, vamos a ver el primer tipo de bucle que tal y como lo dice el nombre en inglés se denomina loop

Loop

La palabra loop se usa para denotar un bucle infinito, es decir no tiene una condicional para detenerse, y su detención se hace de manera manual con la palabra reservada break o return si se está dentro de una función, pero veamos un ejemplo:

let mut number = 0;
loop {
    number += 1;
    if number % 6 == 0 { break; };
    println!("{}", number)
}
//    1
//    2
//    3
//    4
//    5
Enter fullscreen mode Exit fullscreen mode

En este caso lo que hacemos es definir una variable mutable que se llama number y lo aumentamos por cada iteración y si ese valor es múltiplo de 6 vamos a romper el bucle con la sentencia break.

El constructo loop tambien tiene una particularidad bastante útil y es la capacidad de retornar siempre y cuando lo especifiquemos el valor de retorno luego de la palabra break, por ejemplo imagina que queremos hacer una búsqueda lineal (de manera muy ineficiente) sobre un vector, podríamos retornar el índice de la siguiente manera:

let vector = [5, 6, 7, 8];
let element = 1;
let mut index: i8 = 0;
let result = loop {
        if vector[index as usize] == element {
            break index
        } else if index as usize >= vector.len() - 1 {
            break -1
        }

        index += 1;
    }; 


println!("The result is: {}", result) // The result is: -1
Enter fullscreen mode Exit fullscreen mode

En este caso estamos usando el break como un return de una función para retornar un valor cuando se rompe el loop, pero también tenemos algo aún más interesante y es que podemos asignar etiquetas a los loops, y poder romper un bucle exterior desde uno interior.

fn ejemplo_de_etiquetas() {
    let mut number = 0;
    'outer: loop {
        let mut number2 = 0;
        number += 1;
        loop {
            number2 += 1;
            if number2 % 50 == 0 {
                // Rompemos el loop exterior
                break 'outer;
            }
            if number2 >= number {
                break;
            }
        }
    }

    println!("{}", number)
}

ejemplo_de_etiquetas() // 50
Enter fullscreen mode Exit fullscreen mode

While

Como en muchos lenguajes tenemos un bucle que se llama while y no tiene nada muy distinto al while de los otros lenguajes de programación, funciona de la misma manera, vamos a verlo en acción:

let mut number = 10;
while number >= 0 {
    println!("{}", number);
    number -= 1 
}
/* 
   10
    9
    8
    7
    6
    5
    4
    3
    2
    1
    0
*/
Enter fullscreen mode Exit fullscreen mode

A diferencia de la sentencia loop si intentamos retornar un valor al hacer un break nos va a lanzar un error

let mut number = 20;
let mut result = while number <= 0 {
    if number == 10 {
        break "Reached 10";
    } 
    number -= 1
};

println!("{:?}", result)
Enter fullscreen mode Exit fullscreen mode
            break "Reached 10";
            ^^^^^^^^^^^^^^^^^^ can only break with a value inside `loop` or breakable block
    `break` with value from a `while` loop
    help: instead, use `break` on its own without a value inside this `while` loop
    break

Enter fullscreen mode Exit fullscreen mode

Bucle For

El bucle for a diferencia de otros lenguajes de programación, solo itera sobre un iterador. Más adelante en otro artículo veremos que es exactamente un iterador. La expresion range devuelve un iterador un rango de valores que especificamos con la forma <start>..<stop> si lo definimos de esta manera excluye el último valor, si queremos incluir que el último valor s usar la siguiente forma <start>..=<stop>, ahora veamos el bucle for en acción

println!("Rango Exclusivo: ");
for i in 1..3 {
    println!("{}", i)
}
println!("Rango Inclusivo: ");
for i in 1..=3 {
    println!("{}", i)
}
/*
    Exclusivo: 
    1
    2
    Inclusivo: 
    1
    2
    3
*/
Enter fullscreen mode Exit fullscreen mode

Ahora vamos a hablar de otro tópico interesante para a en cuenta, que son los tipos de datos definidos por el usuario

Tipos definidos por el usuario

En Rust también podemos definir nuevos tipos de datos, y tenemos varias formas de hacerlo al igual que en otros lenguajes de programación. En Rust contamos con constructos como estructuras, enumeraciones y uniones o mejor conocidos como structs, enums y unions. Por convención cada uno de estos se deben definir en CameCase, en el caso de Rust los structs y enums son considerablemente más poderosos que en C, y va a ser el foco principal de este artículo, y todos estos constructos seran abordados a mayor profundidad en un artículo futuro. Por ahora nos enfocaremos en los enums y los structs.

Structs

En rust hay 3 formas de declarar structs la primera es el Unit Struct que es un struct que no tiene ni un tipo de datos asociado a él, y puede ser inicializado y declarado solo usando su nombre, veamos como funciona

struct IndexError;

let a = IndexError;
Enter fullscreen mode Exit fullscreen mode

Los struct unitarios como lo dijimos anteriormente son de tamaño unitario, es decir que no tienen ni un dato asociados a ellos mas que el identificador. como describir el nombre de un error que sea entendible y no se necesite mayor descripción para que el programador lo entienda, por ejemplo:

#[derive(Debug)]
struct UserNotFound;
let a = UserNotFound;
println!("{:?}", a); // UserNotFound
Enter fullscreen mode Exit fullscreen mode

Otro tipo de struct que es bastante común son las tuple structs que son estructuras que pueden contener valores sin ser nombrados, por ejemplo si quiero definir un struct para almacenar un punto puedo hacerlo con un tuple struct de la siguiente manera:

struct Point(u32, u32);

let p = Point(16, 32);
Enter fullscreen mode Exit fullscreen mode

Para acceder a los valores podemos usar el member access operator '.', que nos permite acceder a los miembros del struct de la siguiente manera:

let x = p.0;
let y = p.1;
println!("x: {}, y: {}", x, y) // x: 16, y: 32
Enter fullscreen mode Exit fullscreen mode

También podemos destructurar los valores de para extraer las variables de nuestra tuple struct de igual manera como lo haríamos con una tupla, pero vamos a ver la sintaxis y luego aclaramos lo que estamos haciendo exactamente:

let p = Point(10, 60);
let Point(x, y) = p;
println!("x: {}, y: {}", x, y); // x: 10, y: 60
Enter fullscreen mode Exit fullscreen mode

Para entender lo que está pasando aquí, tenemos que entender que Rust tiene que inferir exactamente el tipo de dato que estamos usando, y Rust no lo puede inferir con la sintaxis de destructuración que se podría ver en otros lenguajes como Python x, y = (10, 60), por lo que para solucionar eso hacemos un type casting, lo que significa que Rust leerá los tipos que tienen los campos de nuestro struct Point para que las variables x e y se definan como u32.

Al usar este patrón tambien podemos obviar un valor con un guion bajo, por ejemplo:

struct Address(String, u16);

let address = Address(String::from("Calle de la paz"), 5564);
let Address(name, _) = address;
println!("Street name: {}", name) // Street name: Calle de la paz
Enter fullscreen mode Exit fullscreen mode

Lo último a decir es que los valores del struct se asignan de acuerdo a los índices y la posición del valor, en el ejemplo anterior, a nuestra variable name se le asigna el valor en la posición 0 de nuestra tupla adrress.0.

Esta estructura es una buena opción para cuando necesitamos definir modelos de datos los cuales sus campos por ejemplo, una dirección de IP, un número de tarjeta de crédito entre otros modelos que sus campos no necesitan ser descritos de forma explícita. Pero esto no es en todos los casos así que la última forma de definir una estructura, es de la misma forma en la que se define en C o Golang, primero veámoslo en acción:

struct Person {
    name: String, 
    age: u8,
}

let person = Person {
    name: String::from("Josh"),
    age: 30,
};

println!("{}\nAge:{}", person.name, person.age);
/*
    Josh
    Age:30
*/
Enter fullscreen mode Exit fullscreen mode

En el código anterior definimos una struct como lo veniamos haciendo anteriormente, primero pusimos la palabra reservada struct seguida por el identificador Person, la diferencia ahora es que abrimos llaves y definimos un nombre y un tipo por cada dato que queremos almacenar separados por comas con la siguiente forma: <identifier>:<type>,.

En un struct los campos son inmutables por defecto si queremos mutar un valor dentro del struct en algún momento debemos definir el struct con las palabras reservadas let mut lo que permitirá que todos los campos sean mutables.

struct Person {
    name: String,
    age: u16
}

let mut person = Person {
    name: String::from("Josh"),
    age: 30
};

println!("{}\nAge:{}", person.name, person.age);

person.age = 29;
println!("{}\nAge:{}", person.name, person.age);
/*
    Josh
    Age:30
    Josh
    Age:29
*/
Enter fullscreen mode Exit fullscreen mode

Por último podemos usar un atajo de inicialización en el caso que nuestros valores estén encerrados en variables con el mismo nombres de los identificadores dentro de nuestro struct. Si es este el caso podríamos ponerlos directamente sin indicar el identificador al momento de inicializar el struct, por ejemplo:

let name = String::from("Carl");
let age: u16 = 16;

let person = Person {
    name,
    age
};

println!("{}\nAge:{}", person.name, person.age);
/*
    Carl
    Age:16
*/
Enter fullscreen mode Exit fullscreen mode

La ventaja de usar estas Structs es que cada campo puede tener un nombre significativo que sirva de descriptor para cada parámetro y nos permite inicializar los parámetros en cualquier orden. Algo importante a tener en cuenta es que el tamaño en memoria del struct es la suma del tamaño de sus campos, además que todos los parámetros tienen que tener un tamaño definido a tiempo de compilación, al igual que en los enums que será precisamente el tema al que pasaremos ahora.

Enums

Cuando necesitas modelar un dato que pueda tener distintos tipos o estados, los enums son la mejor herramienta con la que podemos proceder.

Para declarar un enum primero debemos escribir la palabra reservada enum seguido por un par de llaves. Dentro de las llaves debemos escribir todas las posibilidades que puede tomar el modelo que estamos tratando de definir. Estas son llamadas variantes. Esas variantes pueden ser definidas con o sin datos, los datos que pueden contener las variantes pueden ser tipos primitivos, estructuras (structs) o hasta otras enumeraciones (enums), pero sin embargo no puedes tener un enum movimiento que contenga una variante que tenga un tipo movimiento dentro porque o si no el compilador caerá en un error de recursión, para evitar eso podemos esconderlo detrás de un puntero (Box, Rc, u otros) esto evitara tener definiciones de tipos infinitas.

Nota: Esto se da porque Rust calcula el tamaño de las variables en tiempo de compilación, entonces cuando llegue a un caso como siguiente:

struct Node {
    value: String, 
    next: Node
}
Enter fullscreen mode Exit fullscreen mode

El compilador intentara crear espacio en la memoria para contener el tipo Node y al pasar eso en el momento que llega al identificador next, vuelve a preguntar el tamaño del objeto Node e intenta crear un espacio para la memoria y cuando llegue al identificado next va a intentar reservar espacio en la memoria para poder alocar el struct y asi hasta el infinito.
Pero algo que tanto el compilador y nosotros sabemos cuanto espacio necesita, es un puntero que entonces si contenemos nuestro identificador next como un puntero no tendremos problemas al definirlo.

Ahora veamos el enum en acción:

// Definimos un enum con las posibles direcciones
#[derive(Debug)]
enum Directions {
    Up,
    Down,
    Left,
    Right
}

// Definimos un enum con las posibles acciones del jugador
enum PlayerAction {
    Move {
        direction: Directions,
        speed: u8,
    }, 
    Wait, 
    Atack(Directions)
}

// Definimos una acción simulada
let player_action = PlayerAction::Move {
    direction: Directions::Down,
    speed: 2,
};

match player_action {
    // Si la acción es esperar
    PlayerAction::Wait => println!("Player is Waiting"),
    // Si la accion es moverse obtenemos el la velocidad y dirección
    PlayerAction::Move { direction, speed } => println!("Player is moving {:?} with a speed of {}", direction, speed),
    // Si la acción es atacar extraemos la dirección
    PlayerAction::Atack(direction) => print!("Player is atacking with {:?} direction", direction)
}
 // Player is moving Down with a speed of 2
Enter fullscreen mode Exit fullscreen mode

Lo que estamos haciendo es usar la expresión match para mostrar un mensaje de acuerdo a la variante del enum que estamos recibiendo, en el caso de las enums podemos destructurar los valores directamente para usarlos dentro del código que se ejecuta dentro del brazo que calza con la variante del enum que estamos pasando a la expresión.

Para un programador funcional, los structs y enums tambien son conocidos como Tipos de datos Algebraicos (ADTs) porque el rango de valores posibles pueden ser expresados de acuerdo con las reglas del álgebra. Entonces podemos llamar una enum como un SumType porque el rango de valores que puede contener es básicamente la suma del rango de las posibles variantes, mientras que los structs pueden ser llamados ProductTypes porque el rango de posibles valores es el producto cartesianos de sus campos individuales.

Funciones y metodos en tipos definidos por el usuario

Realmente nuestros tipos pueden ser un poco limitados si no podemos asociar funciones con ellos, por suerte en Rust tenemos los bloques de implemetación que pueden proveer métodos y implementaciones para un tipo definido por el usuario, estos bloques de implementación se denotan por la palabra reservada impl y pueden ser implementados tanto en structs como en enums, pero primero veamos como se implementan dentro de los structs.

Bloques de implementación en los structs

Gracias a los bloques de implementación podemos definir e imitar patrones de comportamientos de la programación orientada a objetos, como constructores, métodos estáticos, o métodos asociados a un struct, ahora veremos con detenimiento como funcionan cada uno de estos metodos, comenzando con los constructores, primero veámoslo en acción:

#[derive(Debug)]
struct Address {
    street: String,
    number: u16,
}

impl Address {
    fn new(street: &str, number: u16) -> Address {
        Address {
            street: String::from(street),
            number: number
        }
    }
}

let address = Address::new("Belfast", 5567);
println!("{:?}", address)
//    Address { street: "Belfast", number: 5567 }
Enter fullscreen mode Exit fullscreen mode

Aquí definimos un nuevo método llamado new que imita lo que hace un constructor de clase. En este caso toma dos variables una es la calle que es de tipo &str (string slice) y un numero de tipo u16 para luego devolver un nuevo objeto Address con el cual almacena el valor de la calle y el número.

Para acceder al método usamos el operador :: Namespace Resolution Operator para acceder a el método que acabamos de definir, usamos este operador porque no necesitamos que exista una instancia inicializada de nuestro struct para ejecutar el metodo new, por lo que se podría ver como un método estático que toma los valores street y number para devolver una nueva instancia de nuestro struct Address.

Nota: El operador :: tambien lo usamos para acceder a las funciones definidas dentro de otro archivo o crate, ya lo que hace es simplemente resolver el espacio de nombres para poder acceder a un metodo especifico dentro de un struct o crate. veremos mas sobre esta sintaxis en un articulo futuro.

También podemos definir un método asociado a un struct que use componentes del mismo para devolver un resultado, por ejemplo:

#[derive(Debug)]
struct Point(i32, i32);

impl Point {
    fn sum(&self, other: &Point) -> Point {
        Point (self.0 + other.0, self.1 + other.1)
    }
}

let point = Point(10, 20);
let point2 = Point(5, 10);
let point_sum = point.sum(&point2);

println!("{:?}", point_sum)

// Point(15, 30)
Enter fullscreen mode Exit fullscreen mode

En este caso definimos un método que se llama sum que toma una referencia al mismo y una referencia a otro punto, luego hace una suma de dos puntos y retorna un nuevo punto. Aqui podemos observar una diferencia, en este caso en lugar de usar el operador :: en cambio usamos el operador .. Espera ¿Por qué?.

Esto se debe a que la estructura tiene una instancia actualmente creada, el operador :: resuelve un nombre o método dentro de un espacio de nombres, esto se da porque los métodos no estan asociados a la estructura aun. Luego de crear una nueva instancia de la estructura el compilador asocia los métodos a la estructura como miembros de las mismas por eso usamos el operador de membresía ..

Nota: Nosotros usamos el operador & para pasar un valor por referencia, pero ¿por que necesitamos pasar una referencia?. esto es porque Rust al no tener un Garbage Collector cada vez que se llega al final de un metodo o función todos los valores se borran de la memoria RAM, por lo tanto, si nosotros pasamos un objeto directamente (sin una referencia) Rust al terminar la función eliminará el valor al final del metodo y ya no sera accesible fuera de la función, en cambio una referencia solo apunta a una dirección dentro de la memoria RAM, no contiene nuestro objeto en si, pero si la forma de localizarlo, por lo que si la referencia luego se borra, no afecta el valor fuera de la función.

Como explicamos anteriormente los campos de los structs son inmutables a menos que los hagamos explícitamente mutables, ¿pero como hacemos eso dentro de un bloque de implementación?, primero veámoslo en acción refactorizando nuestra función sum.

#[derive(Debug)]
struct Point(i32, i32);

impl Point {
    fn sum(&mut self, other: &Point) {
        self.0 += other.0;
        self.1 += other.1;
    }
}

let mut point1 = Point(16, 32);
let point2 = Point(2, 2);
point1.sum(&point2);
println!("{:?}", point1)

// Point(18, 34)
Enter fullscreen mode Exit fullscreen mode

En este caso tenemos algunas diferencias, la referencia a self en este caso se antepone con un &mut que es una referencia mutable, las referencias mutables son referencias que apuntan a un sitio específico de memoria y pueden leerlas normalmente y también pueden escribir a esa dirección en la memoria y eso es algo que no puede hacer un puntero normal, también tenemos que definir los campos como mutables con la sintaxis let mut, y esto se debe a que los campos en memoria deben también ser mutables, de otro modo no los podemos modificar, ahora veamos de manera más corta como funcionan los bloques de implementación en los enums.

Bloques de implementación en los enums

En los enums también podemos definir bloques de implementación, pero en estos se usan de una manera distinta, veámoslo en acción

enum Connection {
    Sucessfull,
    Failed, 
    TimedOut
}

impl Connection {
    fn get_status(&self) -> String {
        match self {
            Connection::Sucessfull => format!("200 OK"),
            Connection::Failed => format!("404 Not Found"),
            Connection::TimedOut => format!("Timed Out")
        }
    }
}

let connection_status = Connection::Sucessfull;
println!("Status: {}", connection_status.get_status())
// Status: 200 OK
Enter fullscreen mode Exit fullscreen mode

En este caso para nuestro enum el método que definimos se asocia como un miembro para cada una de las variantes y si llamamos el método a través de alguna de las variantes de nuestro enum se ejecuta el método get_status el cual recibe la variante como argumento y se le puede aplicar la expresión match para enviar un resultado según la variante de donde se llame.

Colecciones

Las colecciones son un conjunto de tipos de datos que usamos frecuentemente, realmente solucionan el problema de tener que agrupar una cantidad de datos de un tipo bajo una notación, es algo que como seres humanos hacemos todo el tiempo, por ejemplo cuando vamos al supermercado, tenemos una lista de supermercado, o aveces podemos agrupar cosas en un contenedor, como la cantidad de cosas que compre dentro de una bolsa, o agrupar una cantidad de objetos del mismo tipo en una clasificación que se llama limpieza.

Realmente las colecciones es solo una abstracción de las agrupaciones de cosas que solemos agrupar día a día de manera inconsciente. Rust tiene como la mayoría de lenguajes varios tipos de colecciones, y aquí veremos las más usadas, primero veremos los arreglos y las tuplas y luego veremos tipos que nos permiten agregar nuevos objetos de forma dinámica, como los vectores. Además de una estructura de datos bastante conocida que se llama Map o (Hashmap) y las veremos en acción en Rust, sin más que agregar, vamos allá.

Arreglo

Los Arrays o Arreglos, son constructos de tamaño fijo de valores del mismo tipo, se denotan como objetos [T, N] siendo T el tipo de datos que todos los objetos van a tener y N el número de elementos que queremos guardar, esto es bastante útil para cuando queremos crear una colección de objetos con un tamaño definido, como una lista de constantes para un algoritmo de criptografía, o un arreglo que siempre vaya a tener el mismo tamaño. Primero veámoslo en acción como ya es costumbre:

let numbers: [u8; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
println!("{}", numbers[5]) //  6
Enter fullscreen mode Exit fullscreen mode

Aquí estamos definiendo un arreglo de elementos de tipo u8 que va a tener 10 espacios y contiene los números del 1 al 10.

Nota: Los arreglos siempre son de tamaño fijo porque todos los elementos se guardan en la memoria de manera contigua, es decir, que cuando nosotros definimos en array [u8; 10] lo que realmente pasa es que el compilador reserva espacio para 10 elementos de tamaño u8. Asi cuando agregamos los elementos se ponen especificamente en la memoria uno al lado del otro, esto nos permite que la indexación y sustitución de un valor se haga a un tiempo constante O(1) lo que significa que indexar solo nos tarda 1 paso, mientras que en una lista tendriamos que recorrer los elementos hasta llegar al indice lo cual es mas lento.

También podemos intentar que el compilador infiera el tipo y tamaño del array, esto se puede hacer declarando una array sin especificar el tipo de datos:

let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
println!("{}", numbers[5])  //    6
Enter fullscreen mode Exit fullscreen mode

En este caso Rust define el array con tipos de datos por defecto, aqui los números de nuestro arreglo son de tipo i32, o un número entero de 32 bits, en caso de que el tipo de numero o dato que queremos usar sea especifico, podemos especificar el tipo de dato de nuestro array con la sintaxis anterior.

También podemos definir Arrays con un valor por defecto, por ejemplo si necesitamos un array que contenga 64 ceros para luego definir una matriz de identidad, podríamos hacerlo con la siguiente sintaxis

let zeros: [i8;64] = [0;64];
println!("Value at index 63: {}\nLenght of the array: {}", zeros[63], zeros.len())

/*
    Value at index 63: 0
    Lenght of the array: 64
*/
Enter fullscreen mode Exit fullscreen mode

Si intentamos acceder a un índice que sea mayor al maximo (arr.len() - 1), que en este caso es 64 el compilador nos mostrará el siguiente error

println!("Value at index 64: {}", zeros[64])
Enter fullscreen mode Exit fullscreen mode
    println!("Value at index 64: {}", zeros[64])
                                      ^^^^^^^^^ index out of bounds: the length is 64 but the index is 64
    this operation will panic at runtime
Enter fullscreen mode Exit fullscreen mode

También podemos hacer a nuestras arrays mutables declarándolas con let mut y modificar cierto índice a cierta posición de la siguiente manera:

let mut arr: [u8; 5] = [0;5];
arr[4] = 100;
println!("{:?}", arr)
// [0, 0, 0, 0, 100]
Enter fullscreen mode Exit fullscreen mode

Ahora vamos a ver otro tipo de colección bastante útil y esas son las tuplas, vamos allá

Tupla

Las tuplas son otro tipo de colecciones que a diferencia de los arreglos pueden contener tipos de datos distintos que deben ser especificados, las tuplas se indexan de la misma manera que las además de soportar destructuración al igual que las tuple structs , ahora veamos un ejemplo:

let tuple: (i8, &str) = (0, "Hello");

println!("{:?}", tuple)
// (0, "Hello")
Enter fullscreen mode Exit fullscreen mode

En el ejemplo anterior vimos que definimos una tupla con dos tipos de datos, un i8 y una referencia a una cadena e igual que en los arreglos podemos definir una tupla sin especificar los tipos y el compilador hará la tarea de inferir los tipos de datos correspondientes a esa tupla de la siguiente manera:

let tuple = (0, "Hello");
println!("{:?}", tuple) // (0, "Hello")
Enter fullscreen mode Exit fullscreen mode

Esta tupla se ve equivalente a la anterior, pero ciertamente el tipo de dato que Rust escogió para nuestro número fue i32 al igual que en el ejemplo anterior. En caso de que tengamos que tener más control del tipo de dato que se escoge como lo hicimos en el ejemplo del arreglo tenemos que definirlo de manera explícita.

También podemos indexar con el operador de membrecía . cada uno de los índices de la misma manera que en nuestra estructura:

let tuple = (0, "Hello");
println!("{}, programmer", tuple.1) // Hello, programmer
Enter fullscreen mode Exit fullscreen mode

Y como dijimos también podemos destructurar los valores separándolos por comas de manera similar a como lo haríamos con Python:

let tuple = (10, 20, 5);
let (x, y, z) = tuple;
println!("{} {} {}", x, y, z) //    10 20 5
Enter fullscreen mode Exit fullscreen mode

También como todos los tipos de datos en Rust lo podemos definir con let mut y mutar los valores de sus miembros:

let mut tuple = (10, 20, 5);
tuple.0 = 15;
println!("{:?}", tuple) //     (15, 20, 5)
Enter fullscreen mode Exit fullscreen mode

Ahora veremos una de las colecciones que más usaras en tu día a día y estos son los vectores.

Vectores

Los vectores en Rust son un tipo de colección con tamaño dinámico, es ideal para cuando necesitamos encerrar una cantidad de valores dentro de un vector el cual no conocemos el tamaño, o si necesitamos que el tamaño sea dinamico, es decir, que aumente o disminuya dependiendo de las necesidades de nuestro programa u algoritmo.

Por ejemplo si estoy haciendo Web Scrapping y requiero guardar los nombres de las casas de una página y sus precios, pero no estoy seguro de cuantos espacios necesito reservar, para ello podemos usar los vectores, podríamos hacer dos vectores distintos, uno para el precio y otro para el nombre e ir integrando los datos a medida que los vaya obteniendo.

Primero veamos como se crea un nuevo vector:

let mut vector: Vec<u8> = Vec::new();
vector.push(10);
println!("{:?}", vector) //     [10]
Enter fullscreen mode Exit fullscreen mode

Primero declaramos un nuevo vector de tipo u8 lo que significa es que cada valor que vamos a tener en nuestro vector tiene que ser de tipo u8, otra cosa a notar es que lo declaramos con la sintaxis let mut, y esto es porque si queremos integrar valores al vector el vector en si tiene que cambiar su estado interno por lo que si no lo declaramos como mutable nos lanzara un error al ejecutar un .push, por lo que casi siempre notaras que los vectores estan definidos como variables mutables.

Lo siguiente es que para que el vector se cree sin errores debemos definir el tipo de dato del vector si usamos el método estático new() para crearlo porque, si no el vector no puede saber el tamaño de cada dato que va a recibir y nos mostrará el siguiente error:

let mut vector = Vec::new();
Enter fullscreen mode Exit fullscreen mode
    let mut vector = Vec::new();
        ^^^^^^^^^^ consider giving `vector` the explicit type `Vec<T>`, where the type parameter `T` is specified
    let mut vector = Vec::new();
                     ^^^^^^^^ cannot infer type for type parameter `T`
    type annotations needed for `Vec<T>`
Enter fullscreen mode Exit fullscreen mode

Aquí el compilador nos está diciendo que no puede inferir el tipo de datos que va a recibir nuestro vector, por lo que es importante a notar que especificar el tipo de dato para un vector es obligatorio.

También podemos crear un vector con la macro vec! el cual si intentara inferir el tipo de dato a utilizar, más adelante hablaremos de que son y como usar las macros en Rust porque en lo personal es mi característica favorita del lenguaje Rust. Ahora veamos como definir un vector con la macro:

let mut vector = vec![10, 20, 50, 60];
println!("{:?}", vector) //     [10, 20, 50, 60]
Enter fullscreen mode Exit fullscreen mode

Aquí tenemos que hacer la observación que no significa que rust infiera el tipo de datos de nuestro vector solamente que los valores entre corchetes son pasados directamente a la macro y ahí se infiere el tipo de datos que queremos almacenar dentro de nuestro vector, en este caso los elementos de nuestro vector son i32.

La indexación dentro de nuestros vectores son de la misma manera que en las arrays:

println!("{:?}", vector[0]) // 10
Enter fullscreen mode Exit fullscreen mode

También podemos usar el método pop para obtener un valor de nuestro vector y excluirlo:

let i = vector.pop();
println!("Value: {}\nVector{:?}", i.unwrap(), vector)
/*
    Value: 60
    Vector[10, 20, 50]
*/
Enter fullscreen mode Exit fullscreen mode

El manejo de errores vamos a tratarlo a fondo en un artículo futuro, por ahora si sabes Rust .unwrap es un método para ignorar el error, y si se presenta algun error el programa termina en un panic. Ahora vamos a hablar de los hashmaps que son una colección que nos permite tener organizados nuestros datos con llave y valor.

HashMaps

Los HashMaps son un tipo de colección que nos permite asociar una llave a un valor, es decir, por ejemplo si quiero tener una colección que asocie la palabra limpieza a un vector con los códigos de barra de todos mis productos de limpieza, o los cereales con todos los códigos de barras de los cereales y así organizar tu estantería virtual, los hashmaps son la estructura de datos que puede facilitar esa tarea, ahora vamos a verlos en acción.

// Traemos dentro de nuestro archivo el nombre hashmap con la palabra use
use std::collections::HashMap; 

let mut map = HashMap::new();
map.insert("cleaning", vec!["A-0020-Z", "A-0030-Z", "A-0040-Z"]);
map.insert("cereal", vec!["A-0100-Z", "A-0090-Z"]);

println!("{:#?}", map)
/*

    {
        "cereal": [
            "A-0100-Z",
            "A-0090-Z",
        ],
        "cleaning": [
            "A-0020-Z",
            "A-0030-Z",
            "A-0040-Z",
        ],
    }
*/
Enter fullscreen mode Exit fullscreen mode

También podemos acceder a los vectores dentro de nuestro hasmap poniendo el nombre entre corchetes de la siguiente manera:

println!("{:?}", map["cereal"])
/*
    ["A-0100-Z", "A-0090-Z"]
*/
Enter fullscreen mode Exit fullscreen mode

Pero espera, ¿Cómo esto funciona?. Si sabes como funcionan los HasMaps te puedes saltar esta explicación.

Los HashMaps pueden serializar cualquier tipo de dato serializadle, por ejemplo en este caso estamos indexando como un &str, pero como es esto posible. Si miramos detrás del telón, la función insert, lo que está haciendo es convertir nuestro &str en un número, y ese número es siempre el mismo.

Esto es gracias a los hashes, el procedimiento que se hace cuando insertamos un dato se describe en los siguientes pasos:

  • Creamos un hash desde nuestra cadena con un algoritmo de hashing
  • Aplicamos módulo al numero final para obtener un numero en cierto rango de valores
  • Usamos nuestro numero final como indice de un array de vectores
  • E insertamos el valor

Este proceso siempre generara el mismo valor para nuestra cadena, ya que los hashes siempre generan la misma salida para la misma entrada, por lo que si queremos buscar nuestro valor el proceso es el siguiente:

  • Creamos un hash a partir de nuestra cadena con un algoritmo de hashing
  • Aplicamos modulo al numero final para objetener un numero en cierto rango de valores
  • Obtenemos el valor con el indice en nuestra array de vectores

Tambien los HashMaps tienen otros mecanismos para evitar las colisiones pero esos son detalles de bajo nivel, te recomiendo leer como funcionan los HashMaps si tienes curiosidad ya que es una solución bastante elegante para asociar una llave con un valor.

También podemos borrar una llave con el método .remove() el cual tambien borrara los valores dentro de nuestro Hashmap

map.remove("cleaning");
println!("{:#?}", map)
/*
    {
        "cereal": [
            "A-0100-Z",
            "A-0090-Z",
        ],
    }
*/
Enter fullscreen mode Exit fullscreen mode

Y también podemos iterar sobre las llaves y los valores de nuestro HashMap usando destructuración de la siguiente manera:

for (key, value) in &map {
    println!("Key: {}, Value: {:?}", key, value)
}
/*
    Key: cereal, Value: ["A-0100-Z", "A-0090-Z"]
*/
Enter fullscreen mode Exit fullscreen mode

Ahora vamos a pasar a una de las ultimas utilidades del lenguaje y estas son las slices que son interesantes porque nos proveen un motón de facilidades para hacer ciertas cosas que no podemos hacer con los arreglos y tambien su funcionamiento es bastante curioso. Vamos allá.

Slices

Los Slices son básicamente puntero apuntando al primer elemento de otra colección, pero no son tan solo un puntero normal, se denomina un Big Pointer porque tienen cierta información de la colección a la que apunta, de esta manera se pueden leer los elementos de la colección sin necesidad de pasar la propiedad de esa colección a otra variable, y también puede consultar los valores de la colección en modo lectura, es decir, los slices no contienen la información de la colección, solo apuntan a los valores de otra colección anteriormente creada, vamos a ver como funcionan:

    let my_array: [u32; 3] = [1, 2, 3];
    let my_slice: &[u32] = &my_array[..];

    println!("{:?}", my_slice) // [1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

El tamaño de un slice [u32] no puede ser calculado en tiempo de compilación. Por ello tenemos que almacenar los slices en punteros, entonces la solución es crear un apuntador que apunte a otro, en C++ como "Pointer to Pointer", por eso los slices se declaran como tipos por referencia.

Gracias a esto, tenemos la ventaja de poder especificar donde comienza y donde termina nuestro slice de la siguiente forma:

    let my_array: [u32; 3] = [1, 2, 3];
    let my_slice: &[u32] = &my_array[0..2];

    println!("{:?}", my_slice) // 1, 2
Enter fullscreen mode Exit fullscreen mode

En este caso el índice 2 de la colección es excluido, los String Slices &str funcionan de esta manera, son solo una referencia a una colección de caracteres que está almacenada en algún lugar de la memoria y lo que vemos es solo un puntero a los valores del string, y podemos crear un &str a partir de un String con el mismo método.

    let my_string: String = String::from("Hello");
    let string_slice: &str = &my_string[..];

    println!("{:?}", string_slice) // "Hello"
Enter fullscreen mode Exit fullscreen mode

Entonces nuestro &str es solo un big pointer hacia nuestro string original.

Cierre y agradecimientos

Si este artículo te pareció interesante, y lo leíste hasta el final, por favor te agradecería que lo compartieras con toda la gente que comprarta los intereses por profundizar o aprender sobre Rust, es verdad que dentro de este corto artículo se dejaron muchos cabos sueltos, así que gracias a sus comentarios podre saber si quieren que empiece con algo más básico o siga con los temas más avanzados, espera el siguiente articulo, puedes dejar tus revisiones y críticas. Nos leemos en el siguiente articulo

Discussion (0)

Forem Open with the Forem app