terça-feira, 23 de fevereiro de 2021

Usando Structs para Estruturar Dados Relacionados

Uma struct, ou estrutura, é um tipo de dados personalizado que permite nomear e empacotar vários valores relacionados que formam um grupo significativo. Se você está familiarizado com uma linguagem orientada a objetos, uma struct é como os atributos de dados de um objeto. Neste capítulo, vamos comparar e contrastar tuplas com structs, demonstrar como usar structs e discutir como definir métodos e funções associadas para especificar o comportamento associado aos dados. Structs e enums (discutidos no Capítulo 6) são os blocos de construção para a criação de novos tipos no domínio do seu programa para aproveitar ao máximo a verificação de tipo de tempo de compilação do Rust.

Definindo e instanciando structs

As structs são semelhantes às tuplas, que foram discutidas no Capítulo 3. Como as tuplas, as partes de uma structs podem ser de tipos diferentes. Ao contrário das tuplas, você nomeará cada parte dos dados para que fique claro o que os valores significam. Como resultado desses nomes, as structs são mais flexíveis do que as tuplas: você não precisa depender da ordem dos dados para especificar ou acessar os valores de uma instância.

Para definir uma structs, inserimos a palavra-chave struct e nomeamos toda a estrutura. O nome de uma struct deve descrever a importância das partes dos dados que estão sendo agrupados. Em seguida, dentro das chaves, definimos os nomes e tipos dos dados, que chamamos de campos. Por exemplo, a Listagem 5-1 mostra uma struct que armazena informações sobre uma conta de usuário.

struct Usuario {
    nome_usuario: String,
    email: String,
    contagem_login: u64,
    ativo: bool,
}

fn main() {}

Listagem 5-1: Usuario definição de struct

Para usar uma structs depois de defini-la, criamos uma instância dessa estrutura especificando valores concretos para cada um dos campos. Criamos uma instância informando o nome da estrutura e, em seguida, adicionamos chaves contendo os pares chave: valor, onde as chaves são os nomes dos campos e os valores são os dados que queremos armazenar nesses campos. Não precisamos especificar os campos na mesma ordem em que os declaramos na estrutura. Em outras palavras, a definição da structs é como um modelo geral para o tipo, e as instâncias preenchem esse modelo com dados específicos para criar valores do tipo. Por exemplo, podemos declarar um determinado Usuario conforme mostrado na Listagem 5-2.

struct Usuario {
    nome_usuario: String,
    email: String,
    contagem_login: u64,
    ativo: bool,
}

fn main() {
    let user1 = Usuario {
        email: String::from("someone@example.com"),
        nome_usuario: String::from("someusername123"),
        ativo: true,
        contagem_login: 1,
    };
}

Listagem 5-2: Criando uma instância da struct Usuario

Para obter um valor específico de uma estrutura, podemos usar a notação de ponto. Se quiséssemos apenas o endereço de e-mail desse usuário, poderíamos usar user1.emailonde quisermos usar esse valor. Se a instância for mutável, podemos alterar um valor usando a notação de ponto e atribuindo a um campo específico. A Listagem 5-3 mostra como alterar o valor no emailcampo de uma Usuarioinstância mutável.

struct Usuario {
    nome_usuario: String,
    email: String,
    contagem_login: u64,
    ativo: bool,
}

fn main() {
    let mut user1 = Usuario {
        email: String::from("someone@example.com"),
        nome_usuario: String::from("someusername123"),
        ativo: true,
        contagem_login: 1,
    };

    user1.email = String::from("anotheremail@example.com");
}

Listagem 5-3: Mudando o valor no campo email de uma instância de Usuario

Observe que toda a instância deve ser mutável; Rust não nos permite marcar apenas alguns campos como mutáveis. Como acontece com qualquer expressão, podemos construir uma nova instância da estrutura como a última expressão no corpo da função para retornar implicitamente essa nova instância.

A Listagem 5-4 mostra uma função build_user que retorna uma instância Usuario com o e-mail e nome de usuário fornecidos. O campo ativo obtém o valor true e o contagem_login obtém o valor 1.

struct Usuario {
    nome_usuario: String,
    email: String,
    contagem_login: u64,
    ativo: bool,
}

fn build_user(email: String, nome_usuario: String) -> Usuario {
    Usuario {
        email: email,
        nome_usuario: nome_usuario,
        ativo: true,
        contagem_login: 1,
    }
}

fn main() {
    let user1 = build_user(
        String::from("someone@example.com"),
        String::from("someusername123"),
    );
}

Listagem 5-4: A função build_user que pega um e-mail e nome de usuário e retorna uma instância de Usuario

Faz sentido para citar os parâmetros da função com o mesmo nome que os campos da struct, mas ter de repetir os nomes de campo e variáveis email e nome_usuario é um pouco tedioso. Se a struct tivesse mais campos, repetir cada um deles seria ainda mais irritante. Felizmente, existe uma abreviatura conveniente!

Usando o atalho de inicialização de campo quando variáveis ​​e campos têm o mesmo nome

Como os nomes dos parâmetros e os nomes dos campos da struct são exatamente os mesmos na Listagem 5-4, podemos usar a sintaxe abreviada do campo init para reescrever build_user de modo que se comporte exatamente da mesma forma, mas não tenha a repetição de email e nome_usuario, conforme mostrado na Listagem 5-5.

struct Usuario {
    nome_usuario: String,
    email: String,
    contagem_login: u64,
    ativo: bool,
}

fn build_user(email: String, nome_usuario: String) -> Usuario {
    Usuario {
        email,
        nome_usuario,
        ativo: true,
        contagem_login: 1,
    }
}

fn main() {
    let user1 = build_user(
        String::from("someone@example.com"),
        String::from("someusername123"),
    );
}

Listagem 5-5: A função build_user usa abreviatura de campo init porque os parâmetros email e nome_usuario têm o mesmo nome que campos os campos da estrutura

Aqui, estamos criando uma nova instância da struct Usuario, que tem um campo chamado email. Queremos definir o valor do campo email para o valor no parâmetro email da função build_user. Como o campo email e o parâmetro email têm o mesmo nome, precisamos apenas escrever, email em vez de email: email.

Criação de instâncias de outras instâncias com sintaxe de atualização de struct

Muitas vezes é útil criar uma nova instância de uma struct que usa a maioria dos valores de uma instância antiga, mas altera alguns. Você fará isso usando a sintaxe de atualização de struct.

Primeiro, a Listagem 5-6 mostra como criamos uma nova instância de Usuario, user2, sem a sintaxe de atualização. Definimos novos valores para email e nome_usuario mas de outra forma usamos os mesmos valores de user1 que criamos na Listagem 5-2.

struct Usuario {
    nome_usuario: String,
    email: String,
    contagem_login: u64,
    ativo: bool,
}

fn main() {
    let user1 = Usuario {
        email: String::from("someone@example.com"),
        nome_usuario: String::from("someusername123"),
        ativo: true,
        contagem_login: 1,
    };

    let user2 = Usuario {
        email: String::from("another@example.com"),
        nome_usuario: String::from("anotherusername567"),
        ativo: user1.ativo,
        contagem_login: user1.contagem_login,
    };
}

Listagem 5-6: Criando uma nova de instância Usuario usando alguns dos valores de user1

Usando a sintaxe de atualização de structs, podemos obter o mesmo efeito com menos código, conforme mostrado na Listagem 5-7. A sintaxe .. especifica que os campos restantes não definidos explicitamente devem ter o mesmo valor que os campos na instância fornecida.

struct Usuario {
    nome_usuario: String,
    email: String,
    contagem_login: u64,
    ativo: bool,
}

fn main() {
    let user1 = Usuario {
        email: String::from("someone@example.com"),
        nome_usuario: String::from("someusername123"),
        ativo: true,
        contagem_login: 1,
    };

    let user2 = Usuario {
        email: String::from("another@example.com"),
        nome_usuario: String::from("anotherusername567"),
        ..user1
    };
}

Listagem 5-7: Usando a sintaxe de atualização de struct para definir novos valores emaile nome_usuariopara uma Usuarioinstância, mas use o restante dos valores dos campos da instância na user1variável

O código na Listagem 5-7 também cria uma instância em user2 que possui um valor diferente para email e nome_usuario mas possui os mesmos valores para os campos ativo e contagem_login de user1.

Usando estruturas de tupla sem campos nomeados para criar tipos diferentes

Você também pode definir structs que se parecem com tuplas, chamadas de estruturas de tupla. As structs de tupla têm o significado adicionado que o nome da estrutura fornece, mas não têm nomes associados a seus campos; em vez disso, eles têm apenas os tipos dos campos. As estruturas de tupla são úteis quando você deseja dar um nome a toda a tupla e torná-la um tipo diferente de outras tuplas, e nomear cada campo como uma estrutura regular seria prolixo ou redundante.

Para definir uma estrutura de tupla, comece com a palavra-chave struct e o nome da estrutura seguido pelos tipos na tupla. Por exemplo, aqui estão as definições e usos de duas structs de tupla chamadas Color e Point:

fn main() {
    struct Color(i32, i32, i32);
    struct Point(i32, i32, i32);

    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

Observe que os valores black e origin são tipos diferentes, porque são instâncias de diferentes estruturas de tupla. Cada estrutura que você define é seu próprio tipo, embora os campos dentro da struct tenham os mesmos tipos. Por exemplo, uma função que recebe um parâmetro do tipo Color não pode aceitar o tipo Point como argumento, embora os dois tipos sejam compostos de três valores i32. Caso contrário, as instâncias de struct de tupla se comportam como tuplas: você pode desestruturá-las em suas partes individuais, pode usar o . seguido pelo índice para acessar um valor individual e assim por diante.

Estruturas semelhantes a unidades sem quaisquer campos

Você também pode definir estruturas que não possuem campos! Eles são chamados de estruturas semelhantes a unidades porque se comportam de maneira semelhante a (), o tipo de unidade. Estruturas semelhantes a unidades podem ser úteis em situações nas quais você precisa implementar uma característica em algum tipo, mas não tem nenhum dado que deseja armazenar no próprio tipo. Discutiremos as características no Capítulo 10.

Propriedade de Struct Data

Na definição da struct Usuario na Listagem 5-1, usamos o tipo String de propriedade em vez do tipo de fatia de string &str. Esta é uma escolha deliberada porque queremos que as instâncias desta estrutura possuam todos os seus dados e que esses dados sejam válidos enquanto a estrutura inteira for válida.

É possível que os structs armazenem referências a dados pertencentes a outra coisa, mas para fazer isso requer o uso de tempos e vida, um recurso Rust que discutiremos no Capítulo 10. Os tempos de vida garantem que os dados referenciados por uma struct sejam válidos por tanto tempo como a estrutura é. Digamos que você tente armazenar uma referência em uma estrutura sem especificar tempos de vida, como este, o que não funcionará:

Nome do arquivo: src / main.rs

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

struct Usuario {
    nome_usuario: &str,
    email: &str,
    contagem_login: u64,
    ativo: bool,
}

fn main() {
    let user1 = Usuario {
        email: "someone@example.com",
        nome_usuario: "someusername123",
        ativo: true,
        contagem_login: 1,
    };
}

O compilador reclamará que precisa de especificadores vitalícios:

$ cargo run
   Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
 --> src/main.rs:2:15
  |
2 |     nome_usuario: &str,
  |               ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 | struct Usuario<'a> {
2 |     nome_usuario: &'a str,
  |

error[E0106]: missing lifetime specifier
 --> src/main.rs:3:12
  |
3 |     email: &str,
  |            ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 | struct Usuario<'a> {
2 |     nome_usuario: &str,
3 |     email: &'a str,
  |

error: aborting due to 2 previous errors

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

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

No Capítulo 10, discutiremos como corrigir esses erros para que você possa armazenar referências em structs, mas, por enquanto, corrigiremos erros como esses usando tipos próprios como em Stringvez de referências como &str.

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

Licença

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

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

Compreendendo propriedade em Rust

A propriedade é o recurso mais exclusivo do Rust e permite que ele faça garantias de segurança de memória sem precisar de um coletor de lixo. Portanto, é importante entender como funciona a propriedade no Rust. Neste capítulo, falaremos sobre propriedade, bem como vários recursos relacionados: empréstimo, fatias e como o Rust coloca os dados na memória.

O que é propriedade?

A característica central do Rust é a propriedade. Embora o recurso seja simples de explicar, ele tem implicações profundas para o resto da linguagem.

Todos os programas precisam gerenciar a maneira como usam a memória do computador durante a execução. Algumas linguagens têm coleta de lixo que constantemente procura por memória não mais usada enquanto o programa é executado; em outras linguagens, o programador deve alocar e liberar explicitamente a memória. O Rust usa uma terceira abordagem: a memória é gerenciada por meio de um sistema de propriedade com um conjunto de regras que o compilador verifica no momento da compilação. Nenhum dos recursos de propriedade torna seu programa lento durante a execução.

Como propriedade é um conceito novo para muitos programadores, leva algum tempo para se acostumar. A boa notícia é que quanto mais experiente você se torna com o Rust e as regras do sistema de propriedade, mais você será capaz de desenvolver código naturalmente seguro e eficiente. Continue assim!

Ao compreender a propriedade, você terá uma base sólida para compreender os recursos que tornam o Rust único. Neste capítulo, você aprenderá a propriedade trabalhando com alguns exemplos que se concentram em uma estrutura de dados muito comum: strings.

Pilha e heap

Em muitas linguagens de programação, você não precisa pensar sobre a pilha e o heap com muita frequência. Mas em uma linguagem de programação de sistemas como Rust, se um valor está na pilha ou no heap tem mais efeito em como a linguagem se comporta e por que você deve tomar certas decisões. Partes da propriedade serão descritas em relação à pilha e ao heap posteriormente neste capítulo, portanto, aqui está uma breve explicação na preparação.

A pilha e o heap são partes da memória que estão disponíveis para seu código usar em tempo de execução, mas são estruturados de maneiras diferentes. A pilha armazena valores na ordem em que os obtém e remove os valores na ordem oposta. Isso é conhecido como último a entrar, primeiro a sair (last in, first out - LIFO.). Pense em uma pilha de pratos: ao adicionar mais pratos, você os coloca no topo da pilha e, quando precisa de um prato, tira um do topo. Adicionar ou remover pratos do meio ou do fundo não funcionaria tão bem! Adicionar dados é chamado de empurrar para a pilha e remover dados é chamado de retirar da pilha.

Todos os dados armazenados na pilha devem ter um tamanho fixo conhecido. Dados com tamanho desconhecido em tempo de compilação ou um tamanho que pode mudar devem ser armazenados no heap. O heap é menos organizado: quando você coloca dados no heap, você solicita uma certa quantidade de espaço. O alocador de memória encontra um ponto vazio na pilha que é grande o suficiente, marca-o como estando em uso e retorna um ponteiro, que é o endereço desse local. Esse processo é chamado de alocação no heap e às vezes é abreviado como apenas alocação. Colocar valores na pilha não é considerado alocação. Como o ponteiro tem um tamanho fixo conhecido, você pode armazená-lo na pilha, mas quando quiser os dados reais, deve seguir o ponteiro.

Pense em estar sentado em um restaurante. Ao entrar, você declara o número de pessoas em seu grupo, e a equipe encontra uma mesa vazia que cabe todos e leva você até lá. Se alguém do seu grupo chegar atrasado, ele pode perguntar onde você estava sentado para encontrá-lo.

Enviar para a pilha é mais rápido do que alocar no heap, porque o alocador nunca precisa procurar um local para armazenar novos dados; esse local está sempre no topo da pilha. Comparativamente, a alocação de espaço no heap requer mais trabalho, porque o alocador deve primeiro encontrar um espaço grande o suficiente para conter os dados e, em seguida, realizar a contabilidade para se preparar para a próxima alocação.

Acessar dados no heap é mais lento do que acessar dados na pilha porque você tem que seguir um ponteiro para chegar lá. Os processadores contemporâneos são mais rápidos se eles saltam menos na memória. Continuando a analogia, considere um garçom em um restaurante recebendo pedidos de várias mesas. É mais eficiente obter todos os pedidos em uma mesa antes de passar para a próxima. Receber um pedido da mesa A, depois um pedido da mesa B, depois um de A novamente e, em seguida, um de B novamente seria um processo muito mais lento. Da mesma forma, um processador pode fazer seu trabalho melhor se trabalhar com dados próximos a outros dados (pois estão na pilha) em vez de mais distantes (como pode estar no heap). Alocar uma grande quantidade de espaço no heap também pode levar algum tempo.

Quando seu código chama uma função, os valores passados para a função (incluindo, potencialmente, ponteiros para dados no heap) e as variáveis locais da função são colocados na pilha. Quando a função termina, esses valores são retirados da pilha.

Acompanhar quais partes do código estão usando quais dados no heap, minimizar a quantidade de dados duplicados no heap e limpar os dados não utilizados no heap para que você não fique sem espaço são problemas que a propriedade aborda. Depois de entender a propriedade, você não precisará pensar sobre a pilha e o heap com frequência, mas saber que o gerenciamento de dados do heap é o motivo pelo qual a propriedade existe pode ajudar a explicar por que funciona da maneira que funciona.

Regras de propriedade

Primeiro, vamos dar uma olhada nas regras de propriedade. Mantenha essas regras em mente enquanto trabalhamos com os exemplos que as ilustram:

  • Cada valor em Rust tem uma variável que é chamada de proprietário.
  • Só pode haver um proprietário por vez.
  • Quando o proprietário sai do escopo, o valor é descartado.

Escopo Variável

Vimos um exemplo de um programa Rust já no Capítulo 2. Agora que passamos da sintaxe básica, não incluiremos todo o fn main() {código nos exemplos, então, se você estiver acompanhando, terá que colocar os exemplos a seguir dentro de uma mainfunção manualmente. Como resultado, nossos exemplos serão um pouco mais concisos, permitindo que nos concentremos nos detalhes reais em vez de no código clichê.

Como primeiro exemplo de propriedade, veremos o escopo de algumas variáveis. Um escopo é o intervalo dentro de um programa para o qual um item é válido. Digamos que temos uma variável semelhante a esta:

let s = "hello";

A variável s se refere a um literal de string, onde o valor da string é codificado no texto de nosso programa. A variável é válida do ponto em que é declarada até o final do escopo atual. A Listagem 4-1 tem comentários anotando onde a variável s é válida.


{                      // s não é valido aqui, ainda não foi declarado
    let s = "hello";   // s é valido a partir daqui

    // fazer coisas com s
}                      // esse escopo acabou, s não é mais valido a partir daqui
    

Listagem 4-1: Uma variável e o escopo em que é válida

Em outras palavras, existem dois pontos importantes no tempo aqui:

  • Quando s entra no escopo, é válido.
  • Ele permanece válido até que saia do escopo.

Neste ponto, a relação entre escopos e quando as variáveis são válidas é semelhante àquela em outras linguagens de programação. Agora vamos construir sobre esse entendimento, introduzindo o tipo String.

O tipo String

Para ilustrar as regras de propriedade, precisamos de um tipo de dados que seja mais complexo do que os que abordamos na seção "Tipos de dados" do Capítulo 3. Os tipos cobertos anteriormente são todos armazenados na pilha e retirados da pilha quando seu escopo acabou, mas queremos examinar os dados armazenados no heap e explorar como Rust sabe quando limpar esses dados.

Usaremos String como exemplo aqui e nos concentraremos nas partes das String relacionadas à propriedade. Esses aspectos também se aplicam a outros tipos de dados complexos, sejam eles fornecidos pela biblioteca padrão ou criados por você. Discutiremos String com mais detalhes no Capítulo 8.

Já vimos literais de string, onde um valor de string é codificado em nosso programa. Literais de string são convenientes, mas não são adequados para todas as situações em que desejamos usar texto. Um dos motivos é que eles são imutáveis. Outra é que nem todo valor de string pode ser conhecido quando escrevemos nosso código: por exemplo, e se quisermos pegar a entrada do usuário e armazená-la? Para essas situações, Rust tem um segundo tipo de string, String. Esse tipo é alocado no heap e, como tal, é capaz de armazenar uma quantidade de texto que não conhecemos em tempo de compilação. Você pode criar uma a String partir de um literal de string usando a função from, assim:

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

Os dois pontos duplos (::) é um operador que nos permite definir o namespace dessa função from específica sob o tipo String, em vez de usar algum tipo de nome como string_from. Discutiremos essa sintaxe mais na seção “Sintaxe do método” do Capítulo 5 e quando falarmos sobre namespacing com módulos em “Caminhos para referir-se a um item na árvore de módulos” no Capítulo 7.

Este tipo de string pode sofrer mutação:

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

    s.push_str(", world!"); // push_str() adiciona um literal a uma String

    println!("{}", s); // Isso vai imprimir 'hello, world!'
}

Então, qual é a diferença aqui? Por que String pode ser alterado, mas os literais não? A diferença é como esses dois tipos lidam com a memória.

Memória e Alocação

No caso de um literal de string, sabemos o conteúdo no momento da compilação, portanto, o texto é codificado diretamente no executável final. É por isso que os literais de string são rápidos e eficientes. Mas essas propriedades vêm apenas da imutabilidade do literal da string. Infelizmente, não podemos colocar um bloco de memória no binário para cada pedaço de texto cujo tamanho é desconhecido no momento da compilação e cujo tamanho pode mudar durante a execução do programa.

Com o tipo String, a fim de oferecer suporte a um trecho de texto mutável e expansível, precisamos alocar uma quantidade de memória no heap, desconhecida no momento da compilação, para armazenar o conteúdo. Isso significa:

  • A memória deve ser solicitada do alocador de memória em tempo de execução.
  • Precisamos de uma maneira de retornar essa memória para o alocador quando terminarmos com nosso String.

Essa primeira parte é feita por nós: quando chamamos String::from, sua implementação requer a memória de que precisa. Isso é quase universal em linguagens de programação.

No entanto, a segunda parte é diferente. Em linguagens com coletor de lixo (GC), o GC rastreia e limpa a memória que não está mais sendo usada, e não precisamos pensar sobre isso. Sem um GC, é nossa responsabilidade identificar quando a memória não está mais sendo usada e chamar o código para retorná-la explicitamente, assim como fizemos para solicitá-la. Fazer isso corretamente sempre foi um problema de programação difícil. Se esquecermos, vamos perder memória. Se fizermos isso muito cedo, teremos uma variável inválida. Se fizermos isso duas vezes, também será um bug. Precisamos emparelhar exatamente um alocador com exatamente um desalocador.

Rust segue um caminho diferente: a memória é retornada automaticamente assim que a variável que a possui sai do escopo. Aqui está uma versão do nosso exemplo de escopo da Listagem 4-1 usando uma String literal em vez de uma string:

{
    let s = String::from("hello"); // s é valido desse ponto em diante

    // faça coisa com esse
}                                  // o escopo termina aqui e s não é mais válido

Há um ponto natural em que podemos retornar a memória de que nossa String precisa para o alocador: quando s sai do escopo. Quando uma variável sai do escopo, Rust chama uma função especial para nós. Essa função é chamada drop, e é onde o autor de String pode colocar o código para retornar a memória. Rust chama drop automaticamente na chave de fechamento.

Nota: Em C++, esse padrão de desalocação de recursos no final da vida útil de um item às vezes é chamado de Aquisição de Recursos é Inicialização (RAII). A função drop em Rust será familiar para você se você tiver usado padrões RAII.

Esse padrão tem um impacto profundo na maneira como o código Rust é escrito. Pode parecer simples agora, mas o comportamento do código pode ser inesperado em situações mais complicadas, quando queremos que várias variáveis ​​usem os dados que alocamos no heap. Vamos explorar algumas dessas situações agora.

Maneiras como variveis e dados interegem: Movimento

Várias variáveis podem interagir com os mesmos dados de maneiras diferentes no Rust. Vejamos um exemplo usando um inteiro na Listagem 4-2.

let x = 5;
let y = x;

Listagem 4-2: Atribuindo o valor inteiro da variável x a y

Provavelmente podemos adivinhar o que isso está fazendo: “vincular o valor 5 a x; em seguida, faça uma cópia do valor de e vincule-o a y.” Agora temos duas variáveis, x e y, e ambas iguais 5. Isso é realmente o que está acontecendo, porque inteiros são valores simples com um tamanho fixo conhecido e esses dois valores 5 são colocados na pilha.

Agora vamos dar uma olhada na versão String:

let s1 = String::from("hello");
let s2 = s1;

Isso se parece muito com o código anterior, então podemos supor que a maneira como funciona seria a mesma: ou seja, a segunda linha faria uma cópia do valor em s1 e o vincularia a s2. Mas não é bem isso o que acontece.

Dê uma olhada na Figura 4-1 para ver o que está acontecendo com String sob os panos. A String é composto de três partes, mostradas à esquerda: um ponteiro para a memória que contém o conteúdo da string, um comprimento e uma capacidade. Este grupo de dados é armazenado na pilha. À direita está a memória na pilha que contém o conteúdo.

Figura 4-1: Representação na memória de uma String guardando o valor "hello" vinculado a s1

O comprimento é a quantidade de memória, em bytes, que o conteúdo da String está usando atualmente. A capacidade é a quantidade total de memória, em bytes, que String recebeu do alocador. A diferença entre comprimento e capacidade é importante, mas não neste contexto, então, por enquanto, não há problema em ignorar a capacidade.

Quando atribuímos s1 a s2, os dados String são copiados, o que significa que copiamos o ponteiro, o comprimento e a capacidade que estão na pilha. Não copiamos os dados no heap ao qual o ponteiro se refere. Em outras palavras, a representação dos dados na memória se parece com a Figura 4-2.

Figura 4-2: Representação na memória da variável s2 que tem uma cópia do ponteiro, comprimento e capacidade de s1

A representação não se parece com a Figura 4-3, que é a aparência da memória se o Rust também copiasse os dados do heap. Se Rust fizesse isso, a operação s2 = s1 poderia ser muito cara em termos de desempenho de tempo de execução se os dados no heap fossem grandes.

Figura 4-3: Outra possibilidade do que s2 = s1 faria se Rust também copiasse os dados do heap

Anteriormente, dissemos que, quando uma variável sai do escopo, Rust automaticamente chama a função drop e limpa a memória heap dessa variável. Mas a Figura 4-2 mostra os dois indicadores de dados apontando para o mesmo local. Este é um problema: quando s2 e s1estiverem fora do escopo, os dois tentarão liberar a mesma memória. Isso é conhecido como erro duplo livre e é um dos bugs de segurança de memória que mencionamos anteriormente. Libertar memória duas vezes pode causar corrupção de memória, o que pode levar a vulnerabilidades de segurança.

Para garantir a segurança da memória, há mais um detalhe do que acontece nessa situação no Rust. Em vez de tentar copiar a memória alocada, o Rust considera que s1 não é mais válido e, portanto, não precisa liberar nada quando s1 sai do escopo. Verifique o que acontece quando você tenta usar s1 depois de criado s2; não vai funcionar:

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

let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);

Você receberá um erro como este porque Rust o impede de usar a referência invalidada:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:28
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 | 
5 |     println!("{}, world!", s1);
  |                            ^^ value borrowed here after move

error: aborting due to previous error

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

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

Se você já ouviu os termos cópia superficial e cópia profunda enquanto trabalha com outras linguagens, o conceito de copiar o ponteiro, comprimento e capacidade sem copiar os dados provavelmente soa como fazer uma cópia superficial. Mas como Rust também invalida a primeira variável, em vez de ser chamada de cópia superficial, é conhecido como movimento. Neste exemplo, diríamos que s1 foi movido para s2. Portanto, o que realmente acontece é mostrado na Figura 4-4.

Figura 4-4: Representação na memória após s1 ter sido invalidada

Isso resolve nosso problema! Com apenas s2 válido, quando sair do escopo, só ele vai liberar a memória, e pronto.

Além disso, há uma escolha de design que está implícita nisso: o Rust nunca criará automaticamente cópias “profundas” de seus dados. Portanto, qualquer cópia automática pode ser considerada econômica em termos de desempenho de tempo de execução.

Maneiras como variveis e dados interegem: Clonar

Se quisermos copiar profundamente os dados do heap da String, não apenas os dados da pilha, podemos usar um método comum chamado clone. Discutiremos a sintaxe dos métodos no Capítulo 5, mas como os métodos são um recurso comum em muitas linguagens de programação, você provavelmente já os viu antes.

Aqui está um exemplo do método clone em ação:

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

let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

Isso funciona muito bem e produz explicitamente o comportamento mostrado na Figura 4-3, em que os dados do heap são copiados.

Ao ver uma chamada para clone, você sabe que algum código arbitrário está sendo executado e que esse código pode ser caro. É um indicador visual de que algo diferente está acontecendo.

Dados somente da pilha: Copiar

Há outro detalhe sobre o qual ainda não falamos. Este código usando números inteiros - parte dos quais foi mostrado na Listagem 4-2 - funciona e é válido:

let x = 5;
let y = x;

println!("x = {}, y = {}", x, y);

Mas este código parece contradizer o que acabamos de aprender: não temos uma chamada para clone, mas x ainda é válido e não foi movido para y.

O motivo é que tipos como inteiros que têm um tamanho conhecido no momento da compilação são armazenados inteiramente na pilha, de modo que as cópias dos valores reais são rápidas de fazer. Isso significa que não há motivo para impedirmos x de ser válido depois de criar a variável y. Em outras palavras, não há diferença entre cópia profunda e superficial aqui, então chamar clone não faria nada diferente da cópia superficial usual e podemos deixá-la de fora.

Rust tem uma anotação especial chamada de característica Copy que podemos colocar em tipos como inteiros que são armazenados na pilha (falaremos mais sobre características no Capítulo 10). Se um tipo implementa a característica Copy, uma variável mais antiga ainda pode ser usada após a atribuição. Rust não nos deixa anotar um tipo com a característica Copy se o tipo, ou qualquer de suas partes, implementou a característica drop. Se o tipo precisar que algo especial aconteça quando o valor sair do escopo e adicionarmos a anotação Copy a esse tipo, obteremos um erro em tempo de compilação. Para saber como adicionar a anotação Copy ao seu tipo para implementar a característica, consulte “Características Deriváveis” no Apêndice C.

Então, quais tipos implementam a característica Copy? Você pode verificar a documentação para o tipo fornecido para ter certeza, mas como regra geral, qualquer grupo de valores escalares simples pode implementar Copy e nada que requeira alocação ou seja alguma forma de recurso pode implementar Copy. Aqui estão alguns dos tipos que implementam Copy:

  • Todos os tipos inteiros, como u32.
  • O tipo booleano, bool, com valores true e false.
  • Todos os tipos de ponto flutuante, como f64.
  • O tipo de caracteres, char.
  • Tuplas, se contiverem apenas tipos que também implementam Copy. Por exemplo, (i32, i32) implementam Copy, mas (i32, String) não.

Propriedade e funções

A semântica para passar um valor para uma função é semelhante àquela para atribuir um valor a uma variável. Passar uma variável para uma função irá mover ou copiar, assim como a atribuição. A Listagem 4-3 tem um exemplo com algumas anotações mostrando onde as variáveis ​​entram e saem do escopo.

Nome do arquivo: src/main.rs

fn main() {
    let s = String::from("hello");  // s entra no escopo

    obtem_propriedade(s);           // O valor de s move-se para a função e,
                                    // portanto, não é mais válido aqui

    let x = 5;                      // x entra no escopo

    fazer_copia(x);                 // x se moveria para a função,
                                    // mas i32 é Copy, então você pode usar x depois disso
} // Aqui, x sai do escopo, então s
  // Mas porque o valor de s foi movido, nada de especial acontece.

fn obtem_propriedade(alguma_string: String) { // alguma_string entra no escopo
    println!("{}", alguma_string);
} // Aqui, alguma_string sai do escopo e `drop` é chamado.
  // A memória de apoio é liberada.

fn fazer_copia(algum_inteiro: i32) { // algum_inteiro entra no escopo
    println!("{}", algum_inteiro);
} // Aqui, algum_inteiro sai do escopo. Nada de especial acontece.

Listagem 4-3: Funções com propriedade e escopo anotados

Se tentássemos usar s após a chamada de obtem_propriedade, Rust geraria um erro em tempo de compilação. Essas verificações estáticas nos protegem de erros. Tente adicionar código a main que faz uso s e x e veja onde você pode usá-los e onde as regras de propriedade o impedem de fazer isso.

Valores de retorno e escopo

Valores de retorno também podem transferir propriedade. A Listagem 4-4 é um exemplo com anotações semelhantes às da Listagem 4-3.

Nome do arquivo: src/main.rs

fn main() {
    let s1 = concede_propriedade();     // concede_propriedade move
                                        // seu valor de retorno para s1

    let s2 = String::from("hello");     // s2 entra no escopo

    let s3 = recebe_e_devolve(s2);  // s2 é movido para recebe_e_devolve,
                                    // que também move seu valor de retorno para s3.
} // Aqui, s3 sai do escopo e é descartado.
  // s2 sai do escopo, mas foi movido, então nada acontece.
  // s1 sai do escopo e é descartado.

fn concede_propriedade() -> String {  // concede_propriedade irá mover seu valor de retorno
                                      // para a função que o chama

    let alguma_string = String::from("hello"); // alguma_string entra no escopo

    alguma_string // alguma_string é retornada e é movida para quem chamou a função
}

// recebe_e_devolve vai receber uma String e retornar uma String
fn recebe_e_devolve(uma_string: String) -> String { // uma_string entra no escopo

    uma_string  // uma_string é retornada e é movida para quem chamou a função
}

Listagem 4-4: Transferindo propriedade de valores de retorno

A propriedade de uma variável segue sempre o mesmo padrão: atribuir um valor a outra variável movendo-á. Quando uma variável que inclui dados no heap sai do escopo, o valor será limpo por drop, a menos que os dados tenham sido movidos para pertencer a outra variável.

Assumir a propriedade e depois devolver a propriedade de todas as funções é um pouco entediante. E se quisermos deixar uma função usar um valor, mas não assumir a propriedade? É muito chato que qualquer coisa que passamos também precise ser passada de volta se quisermos usá-la novamente, além de quaisquer dados resultantes do corpo da função que possamos querer retornar também.

É possível retornar vários valores usando uma tupla, conforme mostrado na Listagem 4-5.

Nome do arquivo: src/main.rs

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

    let (s2, comprimento) = calcula_comprimento(s1);

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

fn calcula_comprimento(s: String) -> (String, usize) {
    let comprimento = s.len(); // len() retorna o comprimento de uma String

    (s, comprimento)
}

Listagem 4-5: Retornando a propriedade dos parâmetros

Mas isso é muita cerimônia e muito trabalho para um conceito que deveria ser comum. Felizmente para nós, o Rust tem um recurso para esse conceito, chamado de referências.

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

Licença