O Tipo Slice

Beginner

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

Introdução

Bem-vindo(a) ao The Slice Type. Este laboratório faz parte do Rust Book. Você pode praticar suas habilidades em Rust no LabEx.

Neste laboratório, resolveremos um problema de programação escrevendo uma função que recebe uma string de palavras separadas por espaços e retorna a primeira palavra que encontra nessa string. Em seguida, discutiremos as limitações de usar índices para representar substrings e a solução para este problema usando string slices em Rust.

O Tipo Slice

Slices permitem que você referencie uma sequência contígua de elementos em uma coleção, em vez da coleção inteira. Um slice é um tipo de referência, portanto, não possui ownership (propriedade).

Aqui está um pequeno problema de programação: escreva uma função que recebe uma string de palavras separadas por espaços e retorna a primeira palavra que encontra nessa string. Se a função não encontrar um espaço na string, a string inteira deve ser uma palavra, então a string inteira deve ser retornada.

Vamos analisar como escreveríamos a assinatura desta função sem usar slices, para entender o problema que os slices resolverão:

fn first_word(s: &String) -> ?

A função first_word tem um &String como parâmetro. Não queremos ownership, então isso está correto. Mas o que devemos retornar? Realmente não temos como falar sobre parte de uma string. No entanto, poderíamos retornar o índice do final da palavra, indicado por um espaço. Vamos tentar isso, como mostrado na Listagem 4-7.

Nome do arquivo: 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()
}

Listagem 4-7: A função first_word que retorna um valor de índice de byte para o parâmetro String

Como precisamos percorrer o elemento String por elemento e verificar se um valor é um espaço, converteremos nossa String em um array de bytes usando o método as_bytes [1].

Em seguida, criamos um iterador sobre o array de bytes usando o método iter [3]. Discutiremos iteradores com mais detalhes no Capítulo 13. Por enquanto, saiba que iter é um método que retorna cada elemento em uma coleção e que enumerate envolve o resultado de iter e retorna cada elemento como parte de uma tupla. O primeiro elemento da tupla retornada de enumerate é o índice, e o segundo elemento é uma referência ao elemento. Isso é um pouco mais conveniente do que calcular o índice nós mesmos.

Como o método enumerate retorna uma tupla, podemos usar padrões para desestruturar essa tupla. Discutiremos padrões com mais detalhes no Capítulo 6. No loop for, especificamos um padrão que tem i para o índice na tupla e &item para o byte único na tupla [2]. Como obtemos uma referência ao elemento de .iter().enumerate(), usamos & no padrão.

Dentro do loop for, procuramos o byte que representa o espaço usando a sintaxe literal de byte [4]. Se encontrarmos um espaço, retornamos a posição. Caso contrário, retornamos o comprimento da string usando s.len() [5].

Agora temos uma maneira de descobrir o índice do final da primeira palavra na string, mas há um problema. Estamos retornando um usize por conta própria, mas é apenas um número significativo no contexto do &String. Em outras palavras, como é um valor separado do String, não há garantia de que ainda será válido no futuro. Considere o programa na Listagem 4-8 que usa a função first_word da Listagem 4-7.

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

    let word = first_word(&s); // word will get the value 5

    s.clear(); // this empties the String, making it equal to ""

    // word still has the value 5 here, but there's no more string that
    // we could meaningfully use the value 5 with. word is now totally invalid!
}

Listagem 4-8: Armazenando o resultado da chamada da função first_word e, em seguida, alterando o conteúdo da String

Este programa compila sem erros e também o faria se usássemos word após chamar s.clear(). Como word não está conectado ao estado de s de forma alguma, word ainda contém o valor 5. Poderíamos usar esse valor 5 com a variável s para tentar extrair a primeira palavra, mas isso seria um bug porque o conteúdo de s mudou desde que salvamos 5 em word.

Ter que se preocupar com o índice em word ficando fora de sincronia com os dados em s é tedioso e propenso a erros! Gerenciar esses índices é ainda mais frágil se escrevermos uma função second_word. Sua assinatura teria que ser assim:

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

Agora estamos rastreando um índice inicial e um final, e temos ainda mais valores que foram calculados a partir de dados em um estado específico, mas não estão vinculados a esse estado de forma alguma. Temos três variáveis não relacionadas flutuando por aí que precisam ser mantidas em sincronia.

Felizmente, Rust tem uma solução para este problema: string slices.

String Slices

Um string slice é uma referência a parte de uma String, e se parece com isto:

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

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

Em vez de uma referência à String inteira, hello é uma referência a uma porção da String, especificada no bit extra [0..5]. Criamos slices usando um intervalo dentro de colchetes, especificando [índice_inicial..índice_final], onde índice_inicial é a primeira posição no slice e índice_final é um a mais que a última posição no slice. Internamente, a estrutura de dados do slice armazena a posição inicial e o comprimento do slice, que corresponde a índice_final menos índice_inicial. Portanto, no caso de let world = &s[6..11];, world seria um slice que contém um ponteiro para o byte no índice 6 de s com um valor de comprimento de 5.

A Figura 4-6 mostra isso em um diagrama.

Figura 4-6: String slice referenciando parte de uma String

Com a sintaxe de intervalo .. do Rust, se você quiser começar no índice 0, pode remover o valor antes dos dois pontos. Em outras palavras, estes são iguais:

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

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

Da mesma forma, se seu slice incluir o último byte da String, você pode remover o número final. Isso significa que estes são iguais:

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

let len = s.len();

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

Você também pode remover ambos os valores para obter um slice da string inteira. Então, estes são iguais:

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

let len = s.len();

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

Nota: Os índices de intervalo de string slice devem ocorrer em limites de caracteres UTF-8 válidos. Se você tentar criar um string slice no meio de um caractere multibyte, seu programa sairá com um erro. Para fins de introdução de string slices, estamos assumindo apenas ASCII nesta seção; uma discussão mais completa sobre o tratamento de UTF-8 está em "Armazenando Texto Codificado em UTF-8 com Strings".

Com todas essas informações em mente, vamos reescrever first_word para retornar um slice. O tipo que significa "string slice" é escrito como &str:

Nome do arquivo: 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[..]
}

Obtemos o índice para o final da palavra da mesma forma que fizemos na Listagem 4-7, procurando a primeira ocorrência de um espaço. Quando encontramos um espaço, retornamos um string slice usando o início da string e o índice do espaço como os índices inicial e final.

Agora, quando chamamos first_word, recebemos um único valor que está vinculado aos dados subjacentes. O valor é composto por uma referência ao ponto de partida do slice e o número de elementos no slice.

Retornar um slice também funcionaria para uma função second_word:

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

Agora temos uma API direta que é muito mais difícil de bagunçar porque o compilador garantirá que as referências na String permaneçam válidas. Lembre-se do bug no programa na Listagem 4-8, quando obtivemos o índice para o final da primeira palavra, mas depois limpamos a string para que nosso índice fosse inválido? Esse código era logicamente incorreto, mas não mostrava nenhum erro imediato. Os problemas apareceriam mais tarde se continuássemos tentando usar o índice da primeira palavra com uma string vazia. Slices tornam esse bug impossível e nos permitem saber que temos um problema com nosso código muito mais cedo. Usar a versão slice de first_word lançará um erro de tempo de compilação:

Nome do arquivo: 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}");
}

Aqui está o erro do 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

Lembre-se das regras de borrowing (empréstimo) que, se tivermos uma referência imutável a algo, também não podemos ter uma referência mutável. Como clear precisa truncar a String, ele precisa obter uma referência mutável. O println! após a chamada para clear usa a referência em word, então a referência imutável ainda deve estar ativa nesse ponto. Rust proíbe a referência mutável em clear e a referência imutável em word de existirem ao mesmo tempo, e a compilação falha. Rust não apenas tornou nossa API mais fácil de usar, mas também eliminou toda uma classe de erros em tempo de compilação!

Literais de String como Slices

Lembre-se de que falamos sobre literais de string sendo armazenados dentro do binário. Agora que sabemos sobre slices, podemos entender corretamente os literais de string:

let s = "Hello, world!";

O tipo de s aqui é &str: é um slice apontando para aquele ponto específico do binário. É também por isso que os literais de string são imutáveis; &str é uma referência imutável.

String Slices como Parâmetros

Sabendo que você pode obter slices de literais e valores String, isso nos leva a mais uma melhoria em first_word, e essa é sua assinatura:

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

Um Rustacean mais experiente escreveria a assinatura mostrada na Listagem 4-9 em vez disso, porque ela nos permite usar a mesma função em valores &String e valores &str.

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

Listagem 4-9: Melhorando a função first_word usando um string slice para o tipo do parâmetro s

Se tivermos um string slice, podemos passá-lo diretamente. Se tivermos uma String, podemos passar um slice da String ou uma referência à String. Essa flexibilidade aproveita as coerções de deref (desreferência), um recurso que abordaremos em "Coerções de Deref Implícitas com Funções e Métodos".

Definir uma função para receber um string slice em vez de uma referência a uma String torna nossa API mais geral e útil sem perder nenhuma funcionalidade:

Nome do arquivo: src/main.rs

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

    // `first_word` funciona em slices de `String`s, sejam parciais
    // ou inteiros
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` também funciona em referências a `String`s, que
    // são equivalentes a slices inteiros de `String`s
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` funciona em slices de literais de string,
    // sejam parciais ou inteiros
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Como os literais de string *já são* string slices,
    // isso também funciona, sem a sintaxe de slice!
    let word = first_word(my_string_literal);
}

Outros Slices

String slices, como você pode imaginar, são específicos para strings. Mas também existe um tipo de slice mais geral. Considere este array:

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

Assim como podemos querer nos referir a parte de uma string, podemos querer nos referir a parte de um array. Faríamos isso assim:

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

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

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

Este slice tem o tipo &[i32]. Ele funciona da mesma forma que os string slices, armazenando uma referência ao primeiro elemento e um comprimento. Você usará esse tipo de slice para todos os tipos de outras coleções. Discutiremos essas coleções em detalhes quando falarmos sobre vetores no Capítulo 8.

Resumo

Parabéns! Você concluiu o laboratório do Tipo Slice. Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.