sábado, 20 de fevereiro de 2021

Referências e empréstimos em Rust

O problema com o código da tupla na Listagem 4-5 é que temos que retornar a String para a função de chamada para que ainda possamos usar a String depois de chamar a função calcula_comprimento, porque a String foi movido para calcula_comprimento.

Aqui está como você definiria e usaria a função calcula_comprimento que tem uma referência a um objeto como parâmetro em vez de assumir a propriedade do valor:

Nome do arquivo: src/main.rs

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

    let len = calcula_comprimento(&s1);

    println!("O comprimento de '{}' é {}.", s1, len);
}

fn calcula_comprimento(s: &String) -> usize {
    s.len()
}

Primeiro, observe que todo o código da tupla na declaração da variável e o valor de retorno da função se foram. Em segundo lugar, observe que passamos &s1 para calcula_comprimento e, em sua definição, recebemos &String em vez de String.

Esses e comerciais são referências e permitem que você se refira a algum valor sem se apropriar dele. A Figura 4-5 mostra um diagrama.

Figura 4-5: Um diagrama de &String s aponta para String s1

Nota: O oposto de referenciar usando & é desreferenciar, que é realizado com o operador desreferenciar, *. Veremos alguns usos do operador de desreferenciação no Capítulo 8 e discutiremos os detalhes da desreferenciação no Capítulo 15.

Vamos dar uma olhada mais de perto na chamada da função aqui:

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

    let len = calcula_comprimento(&s1);

    println!("O comprimento de '{}' é {}.", s1, len);
}

fn calcula_comprimento(s: &String) -> usize {
    s.len()
}

A sintaxe &s1 nos permite criar uma referência que se refere ao valor de s1 mas não o possui. Por não ser o proprietário dele, o valor para o qual ele aponta não será descartado quando a referência sair do escopo.

Da mesma forma, a assinatura da função usa & para indicar que o tipo do parâmetro s é uma referência. Vamos adicionar algumas anotações explicativas:

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

    let len = calcula_comprimento(&s1);

    println!("O comprimento de '{}' é {}.", s1, len);
}

fn calcula_comprimento(s: &String) -> usize { // s é uma referência para uma String
    s.len()
} // Aqui, s sai do escopo. Mas porque não tem propriedade
  // daquilo a que se refere, nada acontece.

O escopo no qual a variável s é válida é o mesmo que o escopo de qualquer parâmetro de função, mas não descartamos o que a referência aponta quando sai do escopo porque não temos propriedade. Quando as funções têm referências como parâmetros em vez dos valores reais, não precisamos retornar os valores para devolver a propriedade, porque nunca tivemos propriedade.

Chamamos as referências como empréstimo de parâmetros de função. Como na vida real, se uma pessoa possui algo, você pode pegá-lo emprestado. Quando terminar, você tem que devolvê-lo.

Então, o que acontece se tentarmos modificar algo que pegamos emprestado? Experimente o código da Listagem 4-6. Alerta de spoiler: não funciona!

Nome do arquivo: src/main.rs

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

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

    mudar(&s);
}

fn mudar(alguma_string: &String) {
    alguma_string.push_str(", world");
}

Listagem 4-6: Tentativa de modificar um valor emprestado

Aqui está o erro:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*alguma_string` as mutable, as it is behind a `&` reference
 --> src/main.rs:8:5
  |
7 | fn mudar(alguma_string: &String) {
  |                        ------- help: consider changing this to be a mutable reference: `&mut String`
8 |     alguma_string.push_str(", world");
  |     ^^^^^^^^^^^ `alguma_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable

error: aborting due to previous error

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

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

Assim como as variáveis são imutáveis por padrão, as referências também o são. Não temos permissão para modificar algo que temos apenas a referência.

Referências mutáveis

Podemos corrigir o erro no código da Listagem 4-6 com apenas um pequeno ajuste:

Nome do arquivo: src/main.rs

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

    mudar(&mut s);
}

fn mudar(alguma_string: &mut String) {
    alguma_string.push_str(", world");
}

Primeiro, tivemos que mudar s para ser mut. Então, tivemos que criar uma referência mutável com &mut s e aceitar uma referência mutável com alguma_string: &mut String.

Mas as referências mutáveis têm uma grande restrição: você pode ter apenas uma referência mutável para uma parte específica dos dados em um escopo específico. Este código falhará:

Nome do arquivo: src/main.rs

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

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

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{}, {}", r1, r2);
}

Aqui está o erro:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 | 
7 |     println!("{}, {}", r1, r2);
  |                        -- first borrow later used here

error: aborting due to previous error

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

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

Esta restrição permite a mutabilidade, mas de uma forma muito controlada. É algo com que os novos Rustáceos lutam, porque a maioria das linguagens permite que você faça mudanças sempre que desejar.

A vantagem de ter essa restrição é que o Rust pode evitar disputas de dados em tempo de compilação. Uma disputas de dados é semelhante a uma condição de corrida e acontece quando esses três comportamentos ocorrem:

  • Dois ou mais ponteiros acessam os mesmos dados ao mesmo tempo.
  • Pelo menos um dos ponteiros está sendo usado para gravar os dados.
  • Nenhum mecanismo está sendo usado para sincronizar o acesso aos dados.

Disputas de dados causam comportamento indefinido e podem ser difíceis de diagnosticar e consertar quando você tenta rastreá-los em tempo de execução; Rust evita que esse problema aconteça porque ele nem mesmo compilará o código com disputas de dados!

Como sempre, podemos usar chaves para criar um novo escopo, permitindo várias referências mutáveis, mas não simultâneas :

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

    {
        let r1 = &mut s;
    } // r1 sai do escopo aqui, então podemos fazer uma nova referência sem problemas.

    let r2 = &mut s;
}

Uma regra semelhante existe para combinar referências mutáveis e imutáveis. Este código resulta em um erro:

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

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

    let r1 = &s; // sem problema
    let r2 = &s; // sem problema
    let r3 = &mut s; // grande problema

    println!("{}, {}, and {}", r1, r2, r3);
}

Aqui está o erro:

$ 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:6:14
  |
4 |     let r1 = &s; // sem problema
  |              -- immutable borrow occurs here
5 |     let r2 = &s; // sem problema
6 |     let r3 = &mut s; // grande problema
  |              ^^^^^^ mutable borrow occurs here
7 | 
8 |     println!("{}, {}, and {}", r1, r2, r3);
  |                                -- 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.

Uau! Nós também não podemos ter uma referência mutável, enquanto a variável é imutável. Os usuários de uma referência imutável não esperam que os valores mudem repentinamente abaixo deles! No entanto, várias referências imutáveis são aceitáveis porque ninguém que está apenas lendo os dados tem a capacidade de afetar a leitura dos dados por outra pessoa.

Observe que o escopo de uma referência começa de onde foi introduzido e continua até a última vez que a referência foi usada. Por exemplo, este código será compilado porque o último uso das referências imutáveis ocorre antes da referência mutável ser introduzida:

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

    let r1 = &s; // sem problema
    let r2 = &s; // sem problema
    println!("{} e {}", r1, r2);
    // r1 e r2 não são mais usados após este ponto

    let r3 = &mut s; // sem problema
    println!("{}", r3);
}

Os escopos das referências imutáveis r1 e r2 terminam depois de println! onde foram usadas pela última vez, que é antes da referência mutável r3 ser criada. Esses escopos não se sobrepõem, portanto, esse código é permitido.

Mesmo que erros de empréstimo possam ser frustrantes às vezes, lembre-se de que é o compilador Rust apontando um bug em potencial no início (em tempo de compilação, e não em tempo de execução) e mostrando exatamente onde está o problema. Assim, você não precisa rastrear por que seus dados não são o que você pensava.

Referências pendentes

Em linguagens com ponteiros, é fácil criar erroneamente um ponteiro pendente, um ponteiro que faz referência a uma localização na memória que pode ter sido fornecida a outra pessoa, liberando alguma memória enquanto preserva um ponteiro para essa memória. Em Rust, ao contrário, o compilador garante que as referências nunca serão referências pendentes: se você tiver uma referência a alguns dados, o compilador garantirá que os dados não sairão do escopo antes da referência aos dados.

Vamos tentar criar uma referência pendente, que o Rust evitará com um erro em tempo de compilação:

Nome do arquivo: src/main.rs

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

fn main() {
    let referencia_para_nada = oscilar();
}

fn oscilar() -> &String {
    let s = String::from("hello");

    &s
}

Aqui está o erro:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn oscilar() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
5 | fn oscilar() -> &'static String {
  |                ^^^^^^^^

error: aborting due to previous error

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

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

Essa mensagem de erro se refere a um recurso que ainda não abordamos: vidas úteis. Discutiremos os tempos de vida em detalhes no Capítulo 10. Mas, se você desconsiderar as partes sobre os tempos de vida, a mensagem contém a chave para explicar por que esse código é um problema:

this function's return type contains a borrowed value, but there is no value for it to be borrowed from.
(o tipo de retorno dessa função contém um valor emprestado, mas não há valor para ser emprestado.)

Vamos dar uma olhada em exatamente o que está acontecendo em cada estágio do nosso código oscilar:

Nome do arquivo: src/main.rs

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

fn main() {
    let referencia_para_nada = oscilar();
}

fn oscilar() -> &String { // oscilar retorna uma referência para uma String

    let s = String::from("hello"); // s is a new String

    &s // nos retornamos uma referência para uma String, s
} // Aqui, s sai do escopo, e é descatado. A memória é liberada.
  // Perigo!

Porque s é criado dentro de oscilar, quando o código de oscilar for concluído, s será desalocado. Mas tentamos retornar uma referência a ele. Isso significa que essa referência estaria apontando para uma String inválida. Isso não é bom! Rust não nos deixa fazer isso.

A solução aqui é devolver a String diretamente:

fn main() {
    let string = no_oscilar();
}

fn no_oscilar() -> String {
    let s = String::from("hello");

    s
}

Isso funciona sem problemas. A propriedade é removida e nada é desalocado.

As Regras de Referências

Vamos recapitular o que discutimos sobre referências:

  • A qualquer momento, você pode ter uma referência mutável ou qualquer número de referências imutáveis.
  • As referências sempre devem ser válidas.

A seguir, veremos um tipo diferente de referência: fatias.

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

Licença

0 comentários:

Postar um comentário