Curso gratuito de Rust: A linguagem mais amada
Coleções Comuns: Vetores e Strings
Aprenda sobre coleções comuns: vetores e strings
Coleções Comuns: Vetores (Vec) e Strings (String)
Olá, futuros mestres de Rust! 👋 Nesta aula prática, vamos mergulhar em duas das coleções de dados mais fundamentais e frequentemente usadas em Rust: os vetores (Vec<T>) e as strings (String). Entender como elas funcionam e como interagir com elas é crucial para escrever programas eficientes e robustos em Rust.
Ambas as coleções são "growable" (redimensionáveis) e armazenam dados no heap, o que significa que seu tamanho pode mudar durante a execução do programa.
Vamos começar! 🚀
1. Vetores (Vec<T>)
Um Vec<T> é uma coleção que pode armazenar múltiplos valores do mesmo tipo em uma única estrutura de dados. Ele é implementado como um array redimensionável, o que significa que você pode adicionar ou remover elementos, e o vetor se ajustará dinamicamente.
Criação de Vetores
Existem algumas maneiras de criar um vetor:
a) Vec::new()
Cria um vetor vazio. Você precisará especificar o tipo dos elementos se o compilador não puder inferir.
// Cria um vetor vazio de inteiros de 32 bits
let v: Vec<i32> = Vec::new();
println!("Vetor vazio: {:?}", v);b) Macro vec![]
Uma forma mais conveniente de criar um vetor com valores iniciais. O compilador inferirá o tipo dos elementos.
// Cria um vetor com valores iniciais
let v = vec![1, 2, 3];
println!("Vetor com valores: {:?}", v);
// Exemplo com strings
let frutas = vec![String::from("Maçã"), String::from("Banana"), String::from("Pera")];
println!("Frutas: {:?}", frutas);Adicionando Elementos
Use o método push() para adicionar elementos ao final do vetor. O vetor deve ser mutável (mut).
let mut v = Vec::new(); // Note o 'mut'
v.push(5);
v.push(6);
v.push(7);
println!("Vetor após push: {:?}", v); // Saída: Vetor após push: [5, 6, 7]Acessando Elementos
Você pode acessar elementos de um vetor de duas maneiras:
a) Indexação []
Acessa o elemento diretamente pelo seu índice. Se o índice estiver fora dos limites do vetor, o programa entrará em pânico (panic).
let v = vec![1, 2, 3, 4, 5];
let terceiro_elemento: &i32 = &v[2]; // Índice 2 é o terceiro elemento (0-based)
println!("O terceiro elemento é: {}", terceiro_elemento);
// Isso causaria um pânico em tempo de execução se descomentado:
// let inexistente = &v[100];
// println!("Elemento inexistente: {}", inexistente);b) Método get()
Retorna um Option<&T>, que será Some(&T) se o elemento existir e None se o índice estiver fora dos limites. Esta é a forma mais segura de acessar elementos.
let v = vec![1, 2, 3, 4, 5];
match v.get(2) {
Some(terceiro) => println!("O terceiro elemento é: {}", terceiro),
None => println!("Não há terceiro elemento."),
}
match v.get(100) {
Some(inexistente) => println!("Elemento inexistente: {}", inexistente),
None => println!("Não há elemento no índice 100."),
}Propriedade e Empréstimo com Vetores
Assim como outras estruturas de dados em Rust, os vetores seguem as regras de propriedade e empréstimo. Isso significa que você não pode ter uma referência mutável e uma ou mais referências imutáveis ao mesmo vetor no mesmo escopo.
let mut v = vec![1, 2, 3];
let primeiro = &v[0]; // Empréstimo imutável
// ERRO! Não é possível emprestar 'v' como mutável porque já está emprestado como imutável.
// v.push(6);
println!("O primeiro elemento é: {}", primeiro); // O empréstimo imutável é liberado aqui.
// Agora podemos modificar o vetor
v.push(6);
println!("Vetor após push (depois do empréstimo): {:?}", v);Este comportamento é uma das garantias de segurança de memória do Rust, prevenindo bugs como data races que podem ocorrer quando um vetor é redimensionado e seus elementos são movidos na memória enquanto há referências a eles.
Iterando sobre Vetores
Você pode iterar sobre os elementos de um vetor usando um loop for.
a) Iteração Imutável
Para ler os valores sem modificá-los.
let v = vec![100, 32, 57];
println!("Iterando sobre vetor imutável:");
for i in &v {
println!(" {}", i);
}b) Iteração Mutável
Para modificar os valores de cada elemento. Você precisa de um empréstimo mutável (&mut v) e desreferenciar o elemento (*i) para alterá-lo.
let mut v = vec![100, 32, 57];
println!("Iterando sobre vetor mutável e modificando:");
for i in &mut v {
*i += 50; // Adiciona 50 a cada elemento
}
println!("Vetor modificado: {:?}", v); // Saída: Vetor modificado: [150, 82, 107]Armazenando Tipos Diferentes com Enum
Vetores só podem armazenar valores do mesmo tipo. Se você precisar armazenar uma coleção de itens que podem ser de tipos diferentes, mas relacionados, você pode usar um enum.
enum CelulaPlanilha {
Inteiro(i32),
Flutuante(f64),
Texto(String),
}
let linha = vec![
CelulaPlanilha::Inteiro(3),
CelulaPlanilha::Texto(String::from("azul")),
CelulaPlanilha::Flutuante(10.12),
];
println!("Linha da planilha:");
for celula in &linha {
match celula {
CelulaPlanilha::Inteiro(i) => println!(" Inteiro: {}", i),
CelulaPlanilha::Flutuante(f) => println!(" Flutuante: {}", f),
CelulaPlanilha::Texto(s) => println!(" Texto: {}", s),
}
}2. Strings (String)
Em Rust, as strings são um pouco mais complexas do que em outras linguagens devido à sua natureza de segurança de memória e ao suporte robusto a UTF-8. Existem dois tipos principais de strings em Rust:
String: É o tipo de string owned (de propriedade), mutável, growable e armazenado no heap. É o que você usará quando precisar de uma string que possa ser modificada ou cujo tamanho possa variar.&str: É um string slice (fatia de string), uma referência imutável a uma sequência de bytes codificados em UTF-8.&stré frequentemente usado para literais de string (ex:"hello") ou para emprestar partes de umaString.
Nesta aula, focaremos principalmente em String.
Criação de Strings
a) String::new()
Cria uma String vazia.
let mut s = String::new();
println!("String vazia: '{}'", s);b) to_string()
Converte um literal de string (&str) ou outros tipos que implementam o trait Display em uma String.
let data = "conteúdo inicial";
let s = data.to_string();
println!("String de literal: '{}'", s);
// Também funciona para literais diretamente
let s = "outro conteúdo".to_string();
println!("Outra string de literal: '{}'", s);c) String::from()
Uma forma comum de criar uma String a partir de um literal de string.
let s = String::from("olá, mundo!");
println!("String de String::from: '{}'", s);Atualizando Strings
Strings em Rust são growable, mas a concatenação e modificação têm algumas particularidades.
a) push_str()
Anexa um &str ao final de uma String.
let mut s1 = String::from("foo");
let s2 = "bar"; // &str
s1.push_str(s2);
println!("s1 após push_str: '{}'", s1); // Saída: s1 após push_str: 'foobar'
println!("s2 ainda é válido: '{}'", s2); // s2 não foi movidob) push()
Anexa um único caractere (char) ao final de uma String.
let mut s = String::from("lo");
s.push('l');
println!("String após push char: '{}'", s); // Saída: String após push char: 'lol'c) Concatenação com +
O operador + (ou std::ops::Add) para strings toma a propriedade da String do lado esquerdo e um empréstimo do &str do lado direito.
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1 foi MOVIDO aqui e não pode mais ser usado
println!("s3: '{}'", s3); // Saída: s3: 'Hello, world!'
// println!("s1: '{}'", s1); // ERRO! s1 foi movido
println!("s2 ainda é válido: '{}'", s2); // s2 é um empréstimo, ainda válidoPerceba que s1 foi movido. Isso ocorre porque o operador + usa a função add(self, other: &str) -> String, que toma self por valor.
d) Macro format!
A macro format! é uma maneira mais flexível e eficiente de concatenar strings, pois ela não toma a propriedade de nenhuma das strings de entrada. Ela retorna uma nova String.
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2, s3);
println!("String formatada: '{}'", s); // Saída: String formatada: 'tic-tac-toe'
println!("s1 ainda é válido: '{}'", s1); // s1, s2, s3 ainda são válidosIndexação em Strings (Atenção!) ⚠️
Este é um ponto crucial e que diferencia Rust de muitas outras linguagens. Rust não permite indexar strings diretamente usando s[i]. O motivo é a forma como as strings são armazenadas: elas são sequências de bytes codificados em UTF-8.
Um caractere UTF-8 pode ocupar de 1 a 4 bytes. Se você tentasse acessar s[0], o Rust não saberia se você quer o primeiro byte, o primeiro caractere escalar ou o primeiro cluster de grafema (o que o usuário percebe como um caractere).
let s1 = String::from("hello");
// let h = s1[0]; // ERRO de compilação!Para acessar partes de uma string, você deve pensar em termos de bytes, caracteres ou clusters de grafema.
a) Bytes
Você pode iterar sobre os bytes brutos da string.
let hello = String::from("Здравствуйте"); // 'З' é 2 bytes, 'д' é 2 bytes, etc.
println!("Bytes da string:");
for b in hello.bytes() {
print!("{} ", b);
}
println!(); // Saída (exemplo parcial): 208 155 208 180 209 130 208 176 ...b) Caracteres (Scalar Values)
Você pode iterar sobre os caracteres Unicode (também conhecidos como scalar values).
let hello = String::from("Здравствуйте");
println!("Caracteres da string:");
for c in hello.chars() {
print!("{} ", c);
}
println!(); // Saída: З д р а в с т в у й т еc) Slicing de Strings
Você pode criar fatias (&str) de uma String, mas deve ser muito cuidadoso para não cortar um caractere UTF-8 no meio. Se você tentar fatiar uma string em um ponto que não seja um limite de caractere válido, o programa entrará em pânico.
let hello = String::from("Здравствуйте");
// Isso é seguro porque 'З' tem 2 bytes
let s = &hello[0..2];
println!("Fatia [0..2]: {}", s); // Saída: З
// Isso também é seguro ('Зд')
let s = &hello[0..4];
println!("Fatia [0..4]: {}", s); // Saída: Зд
// Isso causaria pânico se descomentado, pois 1 é o meio do caractere 'З'
// let s = &hello[0..1];
// println!("Fatia [0..1]: {}", s);A melhor prática é iterar sobre chars() quando você precisa de caracteres individuais.
3. Exercícios Práticos 🧑💻
Agora é a sua vez de colocar a mão na massa! Complete os desafios abaixo.
Desafio 1: Análise Estatística Simples de Vetores
Dada uma lista de números inteiros, calcule a média e a mediana. (Dica: Para a mediana, você precisará ordenar o vetor.)
// TODO: Implemente a função `analisar_numeros`
fn analisar_numeros(numeros: &mut Vec<i32>) {
// 1. Calcule a média dos números
// 2. Calcule a mediana dos números (lembre-se de ordenar o vetor primeiro)
// 3. Imprima os resultados
}
fn main() {
let mut lista_numeros = vec![3, 1, 4, 1, 5, 9, 2, 6, 5, 3];
println!("Lista original: {:?}", lista_numeros);
analisar_numeros(&mut lista_numeros);
// Saída esperada (aproximada):
// Média: 3.9
// Mediana: 3.5 (ou 4, dependendo de como você lida com um número par de elementos)
}💡 Dica para a Mediana
Para a mediana, se o número de elementos for ímpar, é o elemento do meio. Se for par, é a média dos dois elementos do meio. Lembre-se de que o vetor precisa estar ordenado!
Desafio 2: Tradutor Pig Latin
Crie um programa que converte uma frase fornecida pelo usuário para "Pig Latin".
Regras:
- Para palavras que começam com uma vogal (a, e, i, o, u), adicione "-hay" ao final.
- Exemplo: "apple" -> "apple-hay"
- Para palavras que começam com uma consoante, mova a primeira consoante (ou cluster de consoantes) para o final da palavra e adicione "-ay".
- Exemplo: "first" -> "irst-fay"
- Exemplo: "rust" -> "ust-ray"
- Mantenha a capitalização original da primeira letra.
- Trate cada palavra separadamente.
// TODO: Implemente a função `para_pig_latin`
fn para_pig_latin(frase: &str) -> String {
let mut resultado = String::new();
let palavras = frase.split_whitespace(); // Divide a frase em palavras
for palavra in palavras {
let mut chars = palavra.chars();
if let Some(primeira_letra) = chars.next() {
let primeira_letra_lower = primeira_letra.to_ascii_lowercase();
let mut pig_latin_palavra = String::new();
let mut tem_capitalizacao = false;
if primeira_letra.is_ascii_uppercase() {
tem_capitalizacao = true;
}
// Implemente a lógica para vogais e consoantes aqui
// Dica: use `chars.collect::<String>()` para obter o resto da palavra
// Dica: use `format!` para construir a nova palavra
// Dica: Para manter a capitalização, você pode capitalizar a primeira letra
// da palavra resultante e o resto em minúsculas, ou tentar manter a original.
// Para simplicidade, podemos capitalizar a primeira letra do resultado final
// se a original era capitalizada.
// Exemplo de como obter o resto da palavra:
// let resto_palavra: String = chars.collect();
// Adicione um espaço entre as palavras, exceto para a primeira
if !resultado.is_empty() {
resultado.push(' ');
}
resultado.push_str(&pig_latin_palavra);
}
}
resultado
}
fn main() {
let frase = "Apple first Rust example";
let pig_latin_frase = para_pig_latin(frase);
println!("Frase original: {}", frase);
println!("Pig Latin: {}", pig_latin_frase);
// Saída esperada (aproximada):
// Frase original: Apple first Rust example
// Pig Latin: Apple-hay irst-fay ust-ray example-hay
}💡 Dica para o Pig Latin
Você pode usar String::from(primeira_letra).to_ascii_lowercase() para verificar se é uma vogal.
Lembre-se que chars.next() consome o caractere. chars.collect::<String>() converterá o restante do iterador de caracteres em uma nova String.
Lista de Tarefas para os Desafios:
-
Desafio 1: Análise Estatística Simples de Vetores
- Implementar o cálculo da média.
- Implementar o cálculo da mediana (ordenar o vetor e encontrar o(s) elemento(s) do meio).
- Imprimir os resultados.
-
Desafio 2: Tradutor Pig Latin
- Iterar sobre cada palavra da frase.
- Identificar se a palavra começa com vogal ou consoante.
- Aplicar a regra "-hay" para vogais.
- Aplicar a regra de mover a primeira consoante para o final e adicionar "-ay".
- Manter a capitalização original da primeira letra.
- Construir a frase resultante com as palavras traduzidas.
Resumo e Próximos Passos ✨
Nesta aula, exploramos as coleções Vec<T> e String em Rust.
Vec<T>: Ideal para listas de itens do mesmo tipo, onde o número de itens pode mudar. Aprendemos a criar, adicionar, acessar e iterar sobre seus elementos, além de entender as regras de propriedade e empréstimo e como usarenumpara armazenar tipos relacionados.String: A string owned e mutável em Rust, codificada em UTF-8. Vimos como criá-la, modificá-la e, crucialmente, como lidar com sua natureza UTF-8 ao acessar seus conteúdos através de bytes e caracteres, evitando a indexação direta.
Dominar essas coleções é um passo gigante no seu aprendizado de Rust. Elas são a base para muitas outras estruturas de dados e operações.
No próximo módulo, continuaremos a explorar as coleções comuns, focando em HashMap<K, V> (Mapas de Hash) e HashSet<T> (Conjuntos), que são essenciais para armazenar dados associativos e únicos.
Até lá, pratique bastante com os vetores e strings! Se tiver dúvidas, não hesite em perguntar. Bons estudos! 🚀