Rust 0x07 = Estruturas, Métodos & HashMap;

Gil Mendes
7 min readApr 2, 2017

--

Já passou algum tempo desde a publicação do último artigo sobre Rust, mas na verdade muito coisa aconteceu entretanto, o que me impediu de continuar esta série. Ah, finalmente sou mestre em Engenharia Informática! Podem ver mais sobre o meu projeto, criado no âmbito da tese de mestrado em stellar-framework.com.

Comecemos então o assunto de hoje. Até agora estivemos a falar de conceitos soltos, particularidades do Rust e a criar exemplos pouco aproximados do mundo real. Vamos tentar mudar isso a partir deste artigo, introduzindo o conceito de Structs acompanhado por exemplos mais realistas.

O problema

Uma empresa de musica quer manter registo de toda a sua biblioteca de musica. Esta quer manter registo dos seus álbuns e faixas, de uma forma digital.

Depois do levantamento de requisitos chegou-se à conclusão que o melhor modelo de dados para o pedido seria o seguinte:

  • Album
  • Titulo (string)
  • Ano de lançamento (inteiro)
  • Editora (string)
  • Artista (string)
  • Numero de Faixas (inteiro)
  • Faixas (array de faixas)
  • Faixa
  • Titulo (string)
  • Duração (inteiro)

Structs

Agora que temos o nosso problema definido, podemos começar a implementação da nossa solução. Uma vez que precisamos de armazenar múltiplos álbuns vamos precisar de um array, mas, e quanto a estrutura álbum, como vamos resolver isso? Bem, o Rust contem o conceito Structs (ou estrutura), que nos permite declarar tipos de dados complexos, é muito semelhante às estruturas do C, mas estas também podem conter funções, à semelhança das classes em outras linguagens e alto nível, como JAVA, C++, etc.

Depois de se criar um novo projeto usando o cargo, cargo new music_library --bin. Vamos criar a nossa estrutura no ficheiro main.rs.

/// Representa uma faixa do album.
struct Track {
title: String,
duration: u16
}

/// Representa um album
struct Album {
title: String,
artist: String,
year: u16,
publisher: String,
number_of_tracks: u8,
tracks: [Track; 3]
}

Acima, estão representadas as estruturas para uma faixa e para um álbum, respetivamente. Por agora só podemos guardar três faixas no álbum, mas vamos resolver isso mais tarde. Pode correr o comando cargo run para ver que compila sem qualquer problema, não dê atenção às mensagem de aviso, apenas estão a informar que as estruturas declaradas não estão a ser usadas.

Agora vamos instanciar um álbum com duas baixas. Iremos fazer isso diretamente na função main:

// cria um conjunto de três faixas
let track_1 = Track {
title: "Red Diamond".to_string(),
duration: 376
};
let track_2 = Track {
title: "Black Eyes".to_string(),
duration: 240
};
let track_3 = Track {
title: "Mirrorball".to_string(),
duration: 264
};

// cria um array com as faixas para o disco
let tracks: [Track; 3] = [track_1, track_2, track_3];

let album: Album = Album {
title: "Highway Moon".to_string(),
artist: "Best Youth".to_string(),
year: 2015,
publisher: "n/a".to_string(),
number_of_tracks: 3,
tracks: tracks
};

O Rust obriga-nos a preencher por completo o nosso array de faixas, por isso é que criamos um array antes da criação efetiva do álbum.

Se adicionar o Trait Debug às duas estruturas iremos ser capazes de imprimir o nosso álbum para a consola com apenas a macro println!(). Note que os Trails são adicionados às estruturas usando o derive, como pode ser abaixo:

// (...)#[derive(Debug)]
struct Track {
// (...)#[derive(Debug)]
struct Album {

// (...)

Agora se colocar a linha a seguinte após a criação da faixa, irá ver o conteúdo da nossa estrutura:

println!("{:#?}", album);

O ponto de interrogação (?) diz que queremos usar o trait debug, para fazer a impressão da nossa estrutura. Adicionando um cardinal (#) antes, permite formatar o output, agora em vez de termos todos os dados em apenas uma linha, temos uma representação mais perceptível dos campos da estrutura e dos seus valores:

Album {
title: "Highway Moon",
artist: "Best Youth",
year: 2015,
publisher: "n/a",
number_of_tracks: 3,
tracks: [
Track {
title: "Red Diamond",
duration: 376
},
Track {
title: "Black Eyes",
duration: 240
},
Track {
title: "Mirrorball",
duration: 264
}
]
}

Cópia de Dados

Parando um pouco o desenvolvimento da nossa solução, para a gestão de álbuns. Existe um operador (..) que pode ser usado para copiar dados entre estruturas de uma forma muito simples. Por exemplo:

struct Coordenada3d {
x: i32,
y: i32,
z: i32,
}

let mut ponto = Coordenada3d { x: 0, y: 0, z: 0 };
ponto = Coordenada3d { y: 1, .. point };

Acima, criamos um novo ponto em que y tem um novo valor, mas os outros dois campos, mantêm os seus valores iniciais, os do ponto original. Não é necessário que as estruturas sejam iguais, pode-se usar este operador com tipos diferentes.

Tuplos como Estruturas

O Rust tem outro tipo de dados que é um híbrido entre um tuplo e uma estrutura, é conhecido como “tuple struct”. Neste tipo, a estrutura tem um nome, mas os campos não. Eles são criados com a a palavra reservada struct e os campos a seguir como um tuplo:

struct Track(String, u16);

let track_1 = Track("Nome da Faixa".to_string(), 439);

Como já visto no artigo anterior, também aqui, podemos usar patterns para decompor a nossa “tuple struct”:

let Track(name, _) = track_1;

println!("Nome da Faixa: {}", name);

Também podemos usar a sintaxe normal dos tuplos, para aceder aos campos:

let name = track_1.0;

println!("Nome da Faixa: {}", name);

Alocação Dinâmica de Faixas

A fim de tornar o nosso código mais dinâmico iremos alocar as faixas de forma dinâmica. Ao mesmo tempo vamos criar uma função que permita adicionar faixas a um álbum.

Em primeiro lugar, vamos substituir o nosso array de faixas por um HashMap. Trata-se de uma estrutura chave-valor de tamanho variável, o que é excelente para o nosso problema.

Nota: não é importante para o nosso caso, mas a implementação do HashMap usa um algoritmo de hash que fornece resistência a ataques de HashDoS.

No topo do ficheiro adicionamos:

// Importa a estrutura HashMap.
use std::collections::HashMap;

Isto irá importar o tipo HashMap para o nosso scope. Depois adicionamos um método à nossa estrutura:

impl Album {
/// Cria e adiciona uma nova faixa ao Album.
///
/// ## Parametros
/// - `position`: posição da faixa no album.
/// - `title`: titulo da faixa.
/// - `duration`: duração da faixa, em segundo.
pub fn add_track(&mut self, position: u8, title: String, duration: u16) {
self.tracks.insert(position, Track {
title,
duration
});
}
}

Note que deve trocar o tipo da propriedade tracks para “HashMap<u8, Track>”. Em que u8 é o tipo da chave e o Track é o vido do valor associado a uma determinada chave.

Os métodos permitem adicionar comportamentos as nossas estruturas e também fazer alterações aos nossos dados, de uma forma mais limpa. A estrutura de um método é a seguinte:

/// A keyword `impl` permite criar uma área para definir os 
/// métodos para um dado tipo.
impl NomeDaEstrutura {
/// Trata-se de um método que apenas permite aceder aos
/// dados da estrutura, sem editar os seus valores.
fn nome_do_metodo(&self) { }

/// Este método permite editar os dados da estrutura, já
/// que este recebe uma referência mutável para a
/// estrutura.
fn nome_do_metodo_mut(&mut self) { }
}

Agora podemos remover a propriedade number_of_tracks do Àlbum, uma vez que conseguimos saber o numero de elementos a partir da nossa HashMap. Basta adicionar o seguinte método a Struct:

/// Obtem o numero de faixas no album.
///
/// ## Retorna
/// O número de faixas.
pub fn size(&self) -> u8 {
self.tracks.len() as u8
}

Note que usamos a palavra reservada pub, nos dois últimos métodos, isto porque queremos que eles estejam acessíveis fora o bloco definido pelo impl.

Após estas modificações podemos simplificar a associação das faixas ao álbum, para isso, removemos todas a linhas que tratavam dessa tarefa e adicionamos o código abaixo, depois da criação do álbum.

// Adiciona três faixas ao album
album.add_track(1, "Red Diamond".to_string(), 376);
album.add_track(2, "Black Eyes".to_string(), 240);
album.add_track(3, "Mirrorball".to_string(), 264);

Muito mais limpo não? Bem, eu pessoalmente gosto mais.

Funções Associativas

Por fim, vamos associar uma função a estrutura Álbum, sem que esta receba o parâmetro self. Este tipo de funções são úteis para fazer inicialização à estrutura. Neste caso, criar uma função associativa para a criação de uma nova instancia de Álbum.

Nota: Outras linguagens chamam as funções associativas de métodos estáticos.

/// Cria uma nova instancia de Album.
///
/// ## Parametros
/// - `title`: Titulo do album.
/// - `artist`: Artista ou branda que elaborou o album.
/// - `year`: Ano de lançamento do album.
/// - `publisher`: Editora do album.
///
/// ## Retorno
/// Uma nova instancia de Album.
fn new(title: String, artist: String, year: u16, publisher: String) -> Self {
Album {
title,
artist,
year,
publisher,
tracks: HashMap::new()
}
}

Este é um padrão muito comum em Rust. Por norma, as estruturas não estáticas, fornecem um método chamado new para a criação de uma instância sua.

Notou que a função devolve algo do tipo Self? Mas o que é isto? O Self é algo especial em Rust, aponta sempre para o tipo de dados do impl em que está inserido.

Agora podemos simplificar a criação do álbum.

// cria um novo album
let mut album: Album = Album::new(
"Highway Moon".to_string(),
"Best Youth".to_string(),
2015, "n/a".to_string()
);

Este artigo chega agora ao fim, mas pode ainda fazer algumas alterações para passar a suportar o armazenamento de múltiplos álbuns. Uma dica, fique com este exercício em mente 😉

Por agora é tudo, fiquem em paz e boas codificações! 🙂

--

--