El Tipo de Rebanada

Beginner

This tutorial is from open-source community. Access the source code

Introducción

Bienvenido a El Tipo Slice. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.

En esta práctica, resolveremos un problema de programación escribiendo una función que tome una cadena de palabras separadas por espacios y devuelva la primera palabra que encuentre en esa cadena. Luego, discutiremos las limitaciones de usar índices para representar subcadenas y la solución a este problema usando rebanadas de cadena en Rust.

El Tipo Slice

Las rebanadas te permiten referirte a una secuencia contigua de elementos en una colección en lugar de la colección completa. Una rebanada es un tipo de referencia, por lo que no tiene propiedad.

Aquí hay un pequeño problema de programación: escribe una función que tome una cadena de palabras separadas por espacios y devuelva la primera palabra que encuentre en esa cadena. Si la función no encuentra un espacio en la cadena, toda la cadena debe ser una sola palabra, por lo que se debe devolver la cadena completa.

Veamos cómo escribir la firma de esta función sin usar rebanadas para entender el problema que resolverán las rebanadas:

fn first_word(s: &String) ->?

La función first_word tiene un parámetro de tipo &String. No queremos la propiedad, por lo que esto está bien. Pero ¿qué debemos devolver? Realmente no tenemos forma de hablar de parte de una cadena. Sin embargo, podríamos devolver el índice del final de la palabra, indicado por un espacio. Intentemos eso, como se muestra en la Lista 4-7.

Nombre del archivo: src/main.rs

fn first_word(s: &String) -> usize {
  1 let bytes = s.as_bytes();

    for (2 i, &item) in 3 bytes.iter().enumerate() {
      4 if item == b' ' {
            return i;
        }
    }

  5 s.len()
}

Lista 4-7: La función first_word que devuelve un valor de índice de byte en el parámetro String

Debido a que necesitamos recorrer el elemento String uno por uno y comprobar si un valor es un espacio, convertiremos nuestro String en una matriz de bytes usando el método as_bytes [1].

A continuación, creamos un iterador sobre la matriz de bytes usando el método iter [3]. Discutiremos los iteradores en más detalle en el Capítulo 13. Por ahora, sabe que iter es un método que devuelve cada elemento en una colección y que enumerate envuelve el resultado de iter y devuelve cada elemento como parte de una tupla en su lugar. El primer elemento de la tupla devuelta por enumerate es el índice y el segundo elemento es una referencia al elemento. Esto es un poco más conveniente que calcular el índice nosotros mismos.

Debido a que el método enumerate devuelve una tupla, podemos usar patrones para desestructurar esa tupla. Discutiremos los patrones más en el Capítulo 6. En el bucle for, especificamos un patrón que tiene i para el índice en la tupla y &item para el único byte en la tupla [2]. Debido a que obtenemos una referencia al elemento de .iter().enumerate(), usamos & en el patrón.

Dentro del bucle for, buscamos el byte que representa el espacio usando la sintaxis de literales de bytes [4]. Si encontramos un espacio, devolvemos la posición. De lo contrario, devolvemos la longitud de la cadena usando s.len() [5].

Ahora tenemos una forma de encontrar el índice del final de la primera palabra en la cadena, pero hay un problema. Estamos devolviendo un usize por sí solo, pero solo es un número significativo en el contexto del &String. En otras palabras, como es un valor separado del String, no hay garantía de que seguirá siendo válido en el futuro. Considere el programa de la Lista 4-8 que usa la función first_word de la Lista 4-7.

// src/main.rs
fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s); // word obtendrá el valor 5

    s.clear(); // esto vacía la String, haciéndola igual a ""

    // word todavía tiene el valor 5 aquí, pero ya no hay más cadena que
    // podamos usar significativamente con el valor 5. ¡word ahora es completamente inválido!
}

Lista 4-8: Almacenar el resultado de llamar a la función first_word y luego cambiar el contenido de la String

Este programa se compila sin errores y también lo haría si usáramos word después de llamar a s.clear(). Debido a que word no está conectado al estado de s en absoluto, word todavía contiene el valor 5. Podríamos usar ese valor 5 con la variable s para intentar extraer la primera palabra, pero esto sería un error porque el contenido de s ha cambiado desde que guardamos 5 en word.

Tener que preocuparse por que el índice en word se desincronice con los datos en s es tedioso y propenso a errores. Manejar estos índices es aún más frágil si escribimos una función second_word. Su firma tendría que verse así:

fn second_word(s: &String) -> (usize, usize) {

Ahora estamos rastreando un índice de inicio y un índice de final, y tenemos aún más valores que se calcularon a partir de datos en un estado particular pero no están vinculados a ese estado en absoluto. Tenemos tres variables no relacionadas que flotan y que deben mantenerse en sincronía.

Por suerte, Rust tiene una solución a este problema: las rebanadas de cadena.

Rebanadas de Cadena

Una rebanada de cadena es una referencia a una parte de un String, y se ve así:

let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

En lugar de una referencia a todo el String, hello es una referencia a una parte del String, especificada en la parte adicional [0..5]. Creamos rebanadas usando un rango dentro de corchetes especificando [índice_de_inicio..índice_de_fin], donde índice_de_inicio es la primera posición en la rebanada y índice_de_fin es uno más que la última posición en la rebanada. Internamente, la estructura de datos de la rebanada almacena la posición de inicio y la longitud de la rebanada, que corresponde a índice_de_fin menos índice_de_inicio. Entonces, en el caso de let world = &s[6..11];, world sería una rebanada que contiene un puntero al byte en el índice 6 de s con un valor de longitud de 5.

La Figura 4-6 muestra esto en un diagrama.

Figura 4-6: Rebanada de cadena que se refiere a una parte de un String

Con la sintaxis de rango .. de Rust, si quieres comenzar en el índice 0, puedes omitir el valor antes de los dos puntos. En otras palabras, estos son equivalentes:

let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];

Por el mismo motivo, si tu rebanada incluye el último byte del String, puedes omitir el número final. Eso significa que estos son equivalentes:

let s = String::from("hello");

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];

También puedes omitir ambos valores para tomar una rebanada de toda la cadena. Entonces estos son equivalentes:

let s = String::from("hello");

let len = s.len();

let slice = &s[0..len];
let slice = &s[..];

Nota: Los índices de rango de rebanadas de cadena deben ocurrir en los límites válidos de caracteres UTF-8. Si intentas crear una rebanada de cadena en medio de un carácter de varios bytes, tu programa saldrá con un error. Con el propósito de introducir las rebanadas de cadena, estamos asumiendo solo ASCII en esta sección; una discusión más detallada del manejo de UTF-8 se encuentra en "Almacenar texto codificado en UTF-8 con cadenas".

Con toda esta información en mente, vamos a reescribir first_word para devolver una rebanada. El tipo que significa "rebanada de cadena" se escribe como &str:

Nombre del archivo: src/main.rs

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

Obtenemos el índice para el final de la palabra de la misma manera que lo hicimos en la Lista 4-7, buscando la primera aparición de un espacio. Cuando encontramos un espacio, devolvemos una rebanada de cadena usando el inicio de la cadena y el índice del espacio como los índices de inicio y fin.

Ahora cuando llamamos a first_word, obtenemos un solo valor que está vinculado a los datos subyacentes. El valor está compuesto por una referencia al punto de inicio de la rebanada y el número de elementos en la rebanada.

Devolver una rebanada también funcionaría para una función second_word:

fn second_word(s: &String) -> &str {

Ahora tenemos una API directa que es mucho más difícil de desordenar porque el compilador asegurará que las referencias al String permanezcan válidas. Recuerda el error en el programa de la Lista 4-8, cuando obtuvimos el índice al final de la primera palabra pero luego limpiamos la cadena y nuestro índice quedó invalido? Ese código era lógicamente incorrecto pero no mostraba errores inmediatos. Los problemas aparecerían más tarde si seguíamos intentando usar el índice de la primera palabra con una cadena vaciada. Las rebanadas hacen imposible este error y nos permiten saber que tenemos un problema con nuestro código mucho antes. Usar la versión de rebanada de first_word generará un error de compilación:

Nombre del archivo: src/main.rs

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s);

    s.clear(); // error!

    println!("the first word is: {word}");
}

Aquí está el error del compilador:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
immutable
  --> src/main.rs:18:5
   |
16 |     let word = first_word(&s);
   |                           -- immutable borrow occurs here
17 |
18 |     s.clear(); // error!
   |     ^^^^^^^^^ mutable borrow occurs here
19 |
20 |     println!("the first word is: {word}");
   |                                   ---- immutable borrow later used here

Recuerda las reglas de préstamo de que si tenemos una referencia inmutable a algo, no podemos también tomar una referencia mutable. Debido a que clear necesita truncar el String, necesita obtener una referencia mutable. La llamada a println! después de la llamada a clear usa la referencia en word, por lo que la referencia inmutable debe todavía estar activa en ese momento. Rust no permite que la referencia mutable en clear y la referencia inmutable en word existan al mismo tiempo, y la compilación falla. No solo Rust ha hecho que nuestra API sea más fácil de usar, sino que también ha eliminado una clase completa de errores en tiempo de compilación.

Literales de Cadena como Rebanadas

Recuerda que hablamos de que los literales de cadena se almacenan dentro del binario. Ahora que sabemos sobre las rebanadas, podemos entender adecuadamente los literales de cadena:

let s = "Hello, world!";

El tipo de s aquí es &str: es una rebanada que apunta a ese punto específico del binario. Esto también es por lo que los literales de cadena son inmutables; &str es una referencia inmutable.

Rebanadas de Cadena como Parámetros

Sabendo que se pueden tomar rebanadas de literales y valores de String nos lleva a una mejora más en first_word, y es su firma:

fn first_word(s: &String) -> &str {

Un Rustaceano más experimentado escribiría la firma mostrada en la Lista 4-9 en su lugar porque nos permite usar la misma función tanto en valores de &String como en valores de &str.

fn first_word(s: &str) -> &str {

Lista 4-9: Mejora de la función first_word usando una rebanada de cadena para el tipo del parámetro s

Si tenemos una rebanada de cadena, podemos pasarla directamente. Si tenemos un String, podemos pasar una rebanada del String o una referencia al String. Esta flexibilidad aprovecha las coerciones de dereferencia, una característica que cubriremos en "Coerciones de Dereferencia Implícitas con Funciones y Métodos".

Definir una función para tomar una rebanada de cadena en lugar de una referencia a un String hace que nuestra API sea más general y útil sin perder ninguna funcionalidad:

Nombre del archivo: src/main.rs

fn main() {
    let my_string = String::from("hello world");

    // `first_word` funciona en rebanadas de `String`s, ya sea parciales
    // o completas
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` también funciona en referencias a `String`s, que
    // son equivalentes a rebanadas completas de `String`s
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` funciona en rebanadas de literales de cadena,
    // ya sea parciales o completas
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Debido a que los literales de cadena *son* rebanadas de cadena ya,
    // esto también funciona, sin la sintaxis de rebanada!
    let word = first_word(my_string_literal);
}

Otras Rebanadas

Como puedes imaginar, las rebanadas de cadena son específicas de las cadenas. Pero también hay un tipo de rebanada más general. Considera este array:

let a = [1, 2, 3, 4, 5];

Al igual que podríamos querer referirnos a una parte de una cadena, también podríamos querer referirnos a una parte de un array. Lo haríamos de la siguiente manera:

let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);

Esta rebanada tiene el tipo &[i32]. Funciona de la misma manera que las rebanadas de cadena, almacenando una referencia al primer elemento y una longitud. Usarás este tipo de rebanada para todo tipo de otras colecciones. Discutiremos estas colecciones en detalle cuando hablemos de vectores en el Capítulo 8.

Resumen

¡Felicitaciones! Has completado el laboratorio Tipo de Rebanada. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.