quarta-feira, 31 de março de 2021

Um programa de exemplo usando structs em Rust

Para entender quando podemos usar structs, vamos escrever um programa que calcule a área de um retângulo. Começaremos com variáveis ​​únicas e, em seguida, refatoraremos o programa até usarmos structs.

Vamos fazer um novo projeto binário com Cargo chamado retângulos que pegará a largura e a altura de um retângulo especificado em pixels e calculará a área do retângulo. A Listagem 5-8 mostra um programa curto com uma maneira de fazer exatamente isso no src/main.rs do nosso projeto.

Nome do arquivo: src/main.rs

fn main() {
    let largura1 = 30;
    let altura1 = 50;

    println!(
        "A área do retângulo é de {} pixels quadrados.",
        area(largura1, altura1)
    );
}

fn area(largura: u32, altura: u32) -> u32 {
    largura * altura
}

Listagem 5-8: Calculando a área de um retângulo especificado por variáveis separadas de largura e altura

Agora, execute este programa usando cargo run:

$ cargo run
   Compiling structs v0.1.0 (file:///projects/structs)
    Finished dev [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/structs`
A área do retângulo é de 1500 pixels quadrados.

Embora a Listagem 5-8 funcione e descubra a área do retângulo chamando a função area com cada dimensão, podemos fazer melhor. A largura e a altura estão relacionadas entre si porque, juntas, elas descrevem um retângulo.

O problema com este código é evidente na assinatura de area:

fn main() {
    let largura1 = 30;
    let altura1 = 50;

    println!(
        "A área do retângulo é de {} pixels quadrados.",
        area(largura1, altura1)
    );
}

fn area(largura: u32, altura: u32) -> u32 {
    largura * altura
}

A função area deve calcular a área de um retângulo, mas a função que escrevemos tem dois parâmetros. Os parâmetros estão relacionados, mas não são expressos em nenhum lugar do nosso programa. Seria mais legível e gerenciável agrupar largura e altura. Já discutimos uma maneira de fazer isso na seção “O tipo tupla” do Capítulo 3: usando tuplas.

Refatorando com Tuplas

A Listagem 5-9 mostra outra versão de nosso programa que usa tuplas.

Nome do arquivo: src/main.rs

fn main() {
    let retangulo1 = (30, 50);

    println!(
        "A área do retângulo é de {} pixels quadrados.",
        area(retangulo1)
    );
}

fn area(dimensoes: (u32, u32)) -> u32 {
    dimensoes.0 * dimensoes.1
}

Listagem 5-9: Especificando a largura e altura do retângulo com uma tupla

Por um lado, este programa é melhor. As tuplas nos permitem adicionar um pouco de estrutura e agora estamos passando apenas um argumento. Mas, por outro lado, esta versão é menos clara: as tuplas não nomeiam seus elementos, então nosso cálculo se tornou mais confuso porque temos que indexar nas partes da tupla.

Não importa se misturamos largura e altura para o cálculo da área, mas se quisermos desenhar o retângulo na tela, isso faria diferença! Teríamos que ter em mente que largura é o índice da tupla 0 e altura é o índice da tupla 1. Se outra pessoa trabalhou neste código, ela teria que descobrir isso e mantê-lo em mente também. Seria fácil esquecer ou misturar esses valores e causar erros, porque não transmitimos o significado de nossos dados em nosso código.

Refatorando com Structs: Adicionando Mais Significado

Usamos estruturas para adicionar significado ao rotular os dados. Podemos transformar a tupla que estamos usando em um tipo de dados com um nome para o todo e também nomes para as partes, conforme mostrado na Listagem 5-10.

Nome do arquivo: src/main.rs

struct Retangulo {
    largura: u32,
    altura: u32,
}

fn main() {
    let retangulo1 = Retangulo {
        largura: 30,
        altura: 50,
    };

    println!(
        "A área do retângulo é de {} pixels quadrados.",
        area(&retangulo1)
    );
}

fn area(retangulo: &Retangulo) -> u32 {
    retangulo.largura * retangulo.altura
}

Listagem 5-10: Definindo uma estrutura Retangulo

Aqui, definimos uma estrutura e a nomeamos Retangulo. Dentro das chaves, definimos os campos como largura e altura, ambos com tipo u32. Em seguida em main, criamos uma instância específica de Retangulo que tem uma largura de 30 e uma altura de 50.

Nossa função area agora é definida com um parâmetro, que nomeamos retangulo, cujo tipo é um empréstimo imutável de uma instância da struct Retangulo. Conforme mencionado no Capítulo 4, queremos emprestar a estrutura em vez de assumir a propriedade dela. Dessa forma, main mantém sua propriedade e pode continuar usando retangulo1, razão pela qual usamos & na assinatura da função e onde chamamos a função.

A função area acessa os campos largura e altura da instância Retangulo. Nossa assinatura da função area agora diz exatamente o que queremos dizer: calcular a área de Retangulo, usando seus campos largura e altura. Isso indica que a largura e a altura estão relacionadas entre si e dá nomes descritivos aos valores em vez de usar os valores de índice de tupla de 0 e 1. Esta é uma vitória para maior clareza.

Adicionando Funcionalidade Útil com Características Derivadas

Seria bom poder imprimir uma instância de Retangulo enquanto estamos depurando nosso programa e ver os valores de todos os seus campos. A Listagem 5-11 tenta usar a macro println! como usamos nos capítulos anteriores. Isso não funcionará, no entanto.

Nome do arquivo: src/main.rs

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

struct Retangulo {
    largura: u32,
    altura: u32,
}

fn main() {
    let retangulo1 = Retangulo {
        largura: 30,
        altura: 50,
    };

    println!("retangulo1 is {}", retangulo1);
}

Listagem 5-11: Tentativa de imprimir uma Retangulo instância

Quando compilamos este código, obtemos um erro com esta mensagem principal:

error[E0277]: `Retangulo` doesn't implement `std::fmt::Display`

A macro println! pode fazer muitos tipos de formatação e, por padrão, as chaves indicam para println! usar a formatação conhecida como Display: saída destinada ao consumo direto do usuário final. Os tipos primitivos que vimos até agora implementam Display por padrão, porque há apenas uma maneira de mostrar um 1 ou qualquer outro tipo primitivo para um usuário. Mas com structs, a forma como a saída de println! deve ser formatada é menos clara porque há mais possibilidades de exibição: Você quer vírgulas ou não? Você quer imprimir as chaves? Todos os campos devem ser mostrados? Devido a essa ambiguidade, Rust não tenta adivinhar o que queremos e as estruturas não têm uma implementação fornecida de Display.

Se continuarmos lendo os erros, encontraremos esta nota útil:

   = help: the trait `std::fmt::Display` is not implemented for `Retangulo`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

Vamos tentar! A chamada da macro println! agora será semelhante a println!("retangulo1 is {:?}", retangulo1);. Colocar o especificador :? dentro das chaves indica para a macro println! que queremos usar um formato de saída chamado Debug. A característica Debug nos permite imprimir nossa estrutura de uma forma que seja útil para desenvolvedores, para que possamos ver seu valor enquanto estamos depurando nosso código.

Compile o código com esta mudança. Droga! Ainda recebemos um erro:

error[E0277]: `Retangulo` doesn't implement `Debug`

Mas, novamente, o compilador nos dá uma nota útil:

   = help: the trait `Debug` is not implemented for `Retangulo`
   = note: add `#[derive(Debug)]` or manually implement `Debug`

Rust faz incluir a funcionalidade para imprimir informações de depuração, mas nós temos que explicitamente fazer essa funcionalidade disponível para a nossa struct. Para fazer isso, adicionamos a anotação #[derive(Debug)] logo antes da definição da estrutura, conforme mostrado na Listagem 5-12.

Nome do arquivo: src/main.rs

#[derive(Debug)]
struct Retangulo {
    largura: u32,
    altura: u32,
}

fn main() {
    let retangulo1 = Retangulo {
        largura: 30,
        altura: 50,
    };

    println!("retangulo1 é {:?}", retangulo1);
}

Listagem 5-12: Adicionar a anotação para derivar o traço Debug e imprimir a instância Retangulo usando formatação de depuração

Agora, quando executarmos o programa, não obteremos nenhum erro e veremos a seguinte saída:

$ cargo run
   Compiling structs v0.1.0 (file:///projects/structs)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/structs`
retangulo1 é Retangulo { largura: 30, altura: 50 }

Legal! Não é a saída mais bonita, mas mostra os valores de todos os campos dessa instância, o que definitivamente ajudaria durante a depuração. Quando temos estruturas maiores, é útil ter uma saída um pouco mais fácil de ler; nesses casos, podemos usar {:#?} em vez de {:?} na string println!. Quando usamos o estilo {:#?} no exemplo, a saída será semelhante a esta:

$ cargo run
   Compiling structs v0.1.0 (file:///projects/structs)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/structs`
retangulo1 é Retangulo {
    largura: 30,
    altura: 50,
}

Rust forneceu uma série de características para usarmos com a anotação derive que podem adicionar um comportamento útil aos nossos tipos personalizados. Essas características e seus comportamentos estão listados no Apêndice C. Cobriremos como implementar essas características com comportamento personalizado e também como criar suas próprias características no Capítulo 10.

Nossa função area é muito específica: ela apenas calcula a área dos retângulos. Seria útil vincular esse comportamento mais de perto à nossa estrutura Retangulo, porque ela não funcionará com nenhum outro tipo. Vamos ver como podemos continuar a refatorar esse código, transformando a função area em um método area definido em nosso tipo Retangulo.

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

Licença

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