sábado, 20 de fevereiro de 2021

O tipo slice em Rust

Outro tipo de dados que não possui propriedade é a slice. As slices permitem que você faça referência a uma sequência contígua de elementos em uma coleção, em vez de toda a coleção.

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

Vamos pensar sobre a assinatura desta função:

fn primeira_palavra(s: &String) -> ?

Esta função primeira_palavra, tem &String como parâmetro. Não queremos propriedade, então tudo bem. Mas o que devemos devolver? Não temos como falar sobre parte de uma string. No entanto, podemos retornar o índice do final da palavra. Vamos tentar isso, conforme mostrado na Listagem 4-7.

Nome do arquivo: src/main.rs

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

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

    s.len()
}

fn main() {}

Listagem 4-7: A primeira_palavrafunção que retorna um valor de índice de bytes para o Stringparâmetro

Como precisamos percorrer a String elemento por elemento e verificar se um valor é um espaço, converteremos nossa String em uma matriz de bytes usando o método as_bytes:

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

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

    s.len()
}

fn main() {}

Em seguida, criamos um iterador sobre a matriz de bytes usando o método iter:

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

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

    s.len()
}

fn main() {}

Discutiremos os 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 iter e retorna cada elemento como parte de uma tupla. O primeiro elemento da tupla retornado por enumerate é o índice e o segundo elemento é uma referência ao elemento. Isso é um pouco mais conveniente do que calcular o índice por nós mesmos.

Como o método enumerate retorna uma tupla, podemos usar padrões para desestruturar essa tupla, assim como em qualquer outro lugar em Rust. Portanto, no loop for, especificamos um padrão que tem i para o índice na tupla e &item para o único byte na tupla. 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 do byte. Se encontrarmos um espaço, retornamos a posição. Caso contrário, retornamos o comprimento da string usando s.len():

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

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

    s.len()
}

fn main() {}

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 de &String. Em outras palavras, por ser um valor separado de String, não há garantia de que ainda será válido no futuro. Considere o programa da Listagem 4-8 que usa a função primeira_palavra na Listagem 4-7.

Nome do arquivo: src/main.rs

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

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

    s.len()
}

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

    let palavra = primeira_palavra(&s); // palavra vai receber o valor 5

    s.clear(); // isso esvazia a string, fazendo igual a ""

    // palavra ainda tem o valor 5 aqui, mas não há mais string com a qual
    // possamos usar o valor 5 de forma significativa. palavra agora é totalmente inválida!
}

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

Este programa compila sem erros e também o faria se o usássemos word após a chamada s.clear(). Porque word não está conectado ao estado de s, 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 de word ficar fora de sincronia com os dados em s é entediante e sujeito a erros! Gerenciar esses índices é ainda mais frágil se escrevermos uma função segunda_palavra. Sua assinatura teria que ser assim:

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

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

Felizmente, Rust tem uma solução para esse problema: slices de barbante.

Slices de strings

Um slice de string é uma referência a parte de uma String e tem a seguinte aparência:

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

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

Isso é semelhante a tomar uma referência a String toda mas com um pedaço extra [0..5]. Em vez de uma referência a String toda, é uma referência a uma parte da String.

Podemos criar slices usando um intervalo entre colchetes, especificando [index_inicio..index_fim], onde index_inicio é a primeira posição no slice e index_fim é 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 index_fim menos index_inicio. Portanto, no caso de let world = &s[6..11];, worldseria um slice que contém um ponteiro para o 7º byte (contando a partir de 1) de s com um valor de comprimento 5.

A Figura 4-6 mostra isso em um diagrama.

mundo contendo um ponteiro para o 6º byte de String se um comprimento 5

Figura 4-6: Slice de string referindo-se a parte de uma String

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


#![allow(unused)]
fn main() {
let s = String::from("hello");

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

Da mesma forma, se sua slice incluir o último byte de String, você pode descartar o número final. Isso significa que são iguais:


#![allow(unused)]
fn main() {
let s = String::from("hello");

let len = s.len();

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

Você também pode descartar os dois valores para obter um slice de toda a string. Então, eles são iguais:


#![allow(unused)]
fn main() {
let s = String::from("hello");

let len = s.len();

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

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

Com todas essas informações em mente, vamos reescrever primeira_palavrapara retornar um slice. O tipo que significa “slice de string” é escrito como &str:

Nome do arquivo: src/main.rs

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

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

    &s[..]
}

fn main() {}

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

Agora, quando chamamos primeira_palavra, obtemos um único valor que está vinculado aos dados subjacentes. O valor é feito de uma referência ao ponto inicial da slice e ao número de elementos da slice.

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

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

Agora temos uma API simples que é muito mais difícil de bagunçar, porque o compilador garantirá que as referências na String permaneçam válidas. Lembra-se do bug no programa da Listagem 4-8, quando obtivemos o índice no final da primeira palavra, mas depois limpamos a string de forma que nosso índice era inválido? Esse código estava logicamente incorreto, mas não mostrou 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 informam que temos um problema com nosso código muito antes. Usar a versão slice de primeira_palavra lançará um erro de tempo de compilação:

Nome do arquivo: src/main.rs

Esse código não compila Esse código não compila.

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

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

    &s[..]
}

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

    let palavra = primeira_palavra(&s);

    s.clear(); // error!

    println!("A primeira palavra é: {}", word);
}

Aqui está o erro do compilador:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:18:5
   |
16 |     let palavra = primeira_palavra(&s);
   |                           -- immutable borrow occurs here
17 | 
18 |     s.clear(); // error!
   |     ^^^^^^^^^ mutable borrow occurs here
19 | 
20 |     println!("a primeira palavra é: {}", palavra);
   |                                          ------- immutable borrow later used here

error: aborting due to previous error

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

To learn more, run the command again with --verbose.

Lembre-se das regras de empréstimo que, se temos uma referência imutável para algo, não podemos também tomar uma referência mutável. Como clear precisa truncar a String, ele precisa obter uma referência mutável. Rust não permite isso e a compilação falha. O Rust não apenas tornou nossa API mais fácil de usar, mas também eliminou uma classe inteira de erros em tempo de compilação!

Literais de string são slices

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


#![allow(unused)]
fn main() {
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.

Slices de string como parâmetros

Saber que você pode pegar slices de literais e valores de String nos leva a mais uma melhoria em primeira_palavra, e essa é sua assinatura:

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

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

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

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

    &s[..]
}

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

    // primeira_palavra funciona em slices de `String`s
    let palavra = primeira_palavra(&my_string[..]);

    let my_string_literal = "hello world";

    // primeira_palavra funciona em slices de string literals
    let palavra = primeira_palavra(&my_string_literal[..]);

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

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

Se tivermos um slice de string, podemos passá-la diretamente. Se tivermos uma String, podemos passar um slice de toda String. Definir uma função para obter um slice de string em vez de uma referência a String torna nossa API mais geral e útil sem perder nenhuma funcionalidade:

Nome do arquivo: src/main.rs

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

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

    &s[..]
}

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

    // primeira_palavra funciona em slices de 'Strings'
    let palavra = primeira_palavra(&my_string[..]);

    let my_string_literal = "hello world";

    // primeira_palavra funciona em slices de literais de string
    let palavra = primeira_palavra(&my_string_literal[..]);

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

Outros slices

Os slices de string, como você pode imaginar, são específicas para strings. Mas também existe um tipo de slice mais geral. Considere esta matriz:


#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];
}

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


#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];

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

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

Resumo

Os conceitos de propriedade, empréstimo e slices garantem a segurança da memória em programas Rust em tempo de compilação. A linguagem Rust dá a você controle sobre o uso de memória da mesma forma que outras linguagens de programação de sistemas, mas ter a propriedade dos dados limpando automaticamente esses dados quando a propriedade sai do escopo significa que você não precisa escrever e depurar código extra para obter esse controle.

A propriedade afeta o modo como muitas outras partes do Rust funcionam, então falaremos sobre esses conceitos mais adiante no restante do livro. Vamos prosseguir para o Capítulo 5 e ver como agrupar peças de dados numa struct.

Traduzido por Acervo Lima. O original pode ser acessado aqui.

Licença

0 comentários:

Postar um comentário