Dado um valor inteiro X que o usuário deseja sacar, imprima no terminal a quantidade de cédulas de cada valor para que o saque seja realizado. Considere todas as cédulas disponíveis no Brasil: R$ 200, R$ 100, R$ 50, R$ 20, R$ 10, R$ 5 e R$ 2.
fn main() {
let mut ent = String::new();
std::io::stdin().read_line(&mut ent);
let x: i32 = ent.trim().parse().unwrap();
println!("Você digitou {x}");
}
$ rustc caixa.rs
$ ./caixa
Por que tantos comandos foram usados para ler um inteiro do terminal?
// Rust
fn main() {
let mut ent = String::new();
std::io::stdin().read_line(&mut ent);
let x: i32 = ent.trim().parse().unwrap();
println!("Você digitou {x}");
}
// C
#include <stdio.h>
int main() {
int x;
scanf("%d", &x);
printf("Você digitou %d\n", x);
}
// C
#include <stdio.h>
int main() {
int x;
// Em caso de erro, `scanf` retorna `1` e coloca `0` no valor
// da variável
scanf("%d", &x);
printf("Você digitou %d\n", x);
}
❯ gcc scanf-test.c -o scanf-test
❯ ./scanf-test
asd
Você digitou 0
$ man scanf
SYNOPSIS
#include <stdio.h>
int scanf(const char *restrict format, ...);
RETURN VALUE
On success, these functions return the number of input items successfully matched and assigned; this can be fewer than provided for, or even zero, in the event of an early matching failure.
Neste ponto do curso você deve estar se perguntando por que que para imprimir no terminal usamos uma "função" que tem um !
no nome.
Diferentemente de printf
do C, println!
é um macro, e em Rust, macros (macro-funções, mais especificamente) são pós-fixados de !
.
Para entender o porquê, vejamos esse exemplo de código em C e sua saída.
#include <stdio.h>
#include <stdlib.h>
#define max(a, b) (a) > (b) ? (a) : (b)
int main() {
for (int i = 0; i < 10; i++)
printf("%d\n", max(rand()%10, 5));
}
let x = vec![1, 2, 3]; // Dono do dado
let y = x; // Passagem de posse
let a = &x[0]; // Erro! `x` não é mais dona do dado!
let x = vec![1, 2, 3];
let y = &x; // Empréstimo
let a = &x[0]; // OK
Você consegue dizer qual linha causará um erro?
#include <stdio.h>
#include <stdlib.h>
int main() {
int *a = (int*)malloc(sizeof(int) * 10);
int *b = a;
free(a);
printf("%d\n", a[5]);
b[9] = 10;
printf("%d\n", b[9]);
free(b);
}
Em C, Compile com
-fsanitize=address
para evitar surpresas.
Baseando-se no exerício 1, altere o código do seu caixa eletrônico e remova as cédulas de R$ 100 e R$ 10 reais.
Sempre que o programa começar, avise ao usuário quais são as cédulas disponíveis.
Use funções para listar as cédulas disponíveis e para calcular as cédulas entregues no saque.
&
são referências imutáveis (ou compartilhadas);&mut
são referências mutáveis (ou únicas);struct Cpf([u8; 11]);
struct Pessoa {
nome: String,
cpf: Cpf,
}
Cpf
é umaCpf
possui um array de 11Implementações nos permitem associar código a determinadas estruturas. Você pode pensar em implementações como paralelos a métodos em linguagens Orientadas a Objetos; a diferença é que estrutura e código são definidos em blocos diferentes.
struct Pessoa {
nome: String,
sobrenome: String,
}
impl Pessoa {
fn nome_completo(
&self
) -> String {
format!("{} {}",
self.nome,
self.sobrenome
)
}
}
Crie uma struct que represente um usuário com nome e e-mail e implemente os seguintes métodos:
Em Rust, o enum
é o que chamamos de união discriminada (tagged union). Com ele, é possível definir não somente um nome para um valor constante, mas também incluir valores nas variantes do enumerador.
enum FormaGeometrica {
Circulo { raio: f32 },
Quadrado { lado: f32 },
Retangulo { altura: f32, largura: f32 },
}
fn main() {
let mut entrada = String::new();
let stdin = std::io::stdin();
stdin.read_line(&mut entrada);
let x = entrada.trim().parse::<i32>();
match x {
Ok(x) => println!("x é {x}"),
Err(e) => println!("Erro: {e}"),
}
}
Antes de acessarmos o
valor de um enum, é
necessário discriminar
a variante.
Podemos fazer isso de
várias maneiras, sendo
a mais comum com o
comando match
.
Escreva uma implementação para o enum
FormaGeometrica que imprima a área da forma no terminal.
#include <stdio.h>
typedef struct {
enum { RETANGULO, QUADRADO, CIRCULO } tipo;
union {
struct { float altura, largura; } retangulo;
struct { float lado; } quadrado;
struct { float raio; } circulo;
};
} FiguraGeometrica;
int main() {
FiguraGeometrica fig = {
.tipo = QUADRADO,
.quadrado = { .lado = 2.0 }
};
printf("%.2f\n", fig.quadrado.lado);
}
#include <iostream>
#include <variant>
struct Retangulo { int largura, altura; };
struct Quadrado { int lado; };
struct Circulo { int raio; };
using FiguraGeometrica = std::variant<
Retangulo, Quadrado, Circulo
>;
int main() {
auto fig = FiguraGeometrica {
Retangulo { .largura = 10, .altura = 20 }
};
std::cout
<< "largura: "
<< std::get<Retangulo>(fig).largura
<< std::endl;
}
Como vimos anteriormente, Rust não é uma linguagem Orientada a Objetos. Contudo, ela oferece um recurso familiar aos programadores OO para a reutilização de código (dentro de inúmeras outras funções): os traços.
Traços descrevem uma série de métodos que devem ser implementados por uma struct ou enum.
trait Animal {
fn ameacar(&self);
}
struct Cachorro;
struct Gato;
impl Animal for Cachorro {
fn ameacar(&self) {
println!("Grrr");
}
}
impl Animal for Gato {
fn ameacar(&self) {
println!("Hiss");
}
}
fn main() {
let c = Cachorro;
let g = Gato;
c.ameacar();
g.ameacar();
}
Traços também podem provir implementações padrão para os métodos especificados, de tal forma que não seja necessário re-implementá-los para todas as estruturas que quiserem implementá-los.
trait Animal {
fn ameacar(&self);
fn ameacar_e_atacar(&self) {
self.ameacar();
println!("Slash!");
}
}
Neste exemplo, tanto as estruturas Cachorro
e Gato
terão o método .ameacar_e_atacar
auto-definido.
derive
Vimos previamente que funções pós-fixadas com !
são funções-macro. Em Rust, 3 tipos de macro existem, no total, sendo um dos mais importantes o derive
.
fn main() {
let c = Carro {
modelo: "Fusca",
numero_portas: 2
};
dbg!(c); // Imprime `Carro { ... }`
}
#derive[Debug]
struct Carro {
modelo: String,
numero_portas: i32,
}
Estes macros comumente são utilizados para prover funcionalidades trivialmente implementáveis. Exemplos:
Debug
: Possibilita impressão dos dados da estrutura;Eq
: Possibilita comparação de igualidade entre estruturas;Hash
: Possibilita que a estrutura seja usada como chave de HashMap
;Display
e Debug
Faça uma estrutura que represente um aluno, com pelo menos 3 campos de tipos diferentes. Utilize o derive
para implementar Debug
na estrutura e realizar a impressão de depuração.
Após isso, implemente Display
para definir como um aluno deve ser apresentado no SIGAA. Siga este template para a impressão:
Olá, {aluno.nome}. Sua matrícula é {aluno.matricula}.
Como vimos anteriormente, uma maneira fácil de escrever código reutilizável é agrupar "tipos" que aceitam operações em comum num enumerador, como no caso de FiguraGeometrica.
Contudo, existem certas ocasiões nas quais é preferível a utilização de estruturas e traços para agrupar operações em comum.
Neste capítulo, veremos como podemos escrever funções, estruturas e traços que aceitem múltiplos tipos diferentes baseados no comportamento dos tipos aceitáveis.
Rust oferece tipos genéricos de forma similar a C++ ou Java. Com estes tipos, é possível escrever funções que atuam em múltiplos tipos de dados diferentes, contanto que estes tipos de dados possuam alguma funcionalidade em comum.
use std::fmt::Display;
fn imprime_array<T: Display>(arr: &[T]) {
for (i, t) in arr.iter().enumerate() {
println!("{i}: {t}")
}
}
fn main() {
let x = [10, 20, 30, 40];
imprime_array(&x);
let y = ["Olá", "Mundo"];
imprime_array(&y);
}
struct Cliente<M: MeioDeContato> {
nome: String,
contato: M
}
trait MeioDeContato {
fn envia_mensagem(
&self, mensagem: String
);
}
struct Email(String);
struct Celular {
ddd: [2; u8],
numero: [9; u8]
}
impl MeioDeContato for Email {
fn envia_mensagem(
&self, mensagem: String
) {
envia_email(&self.0, mensagem);
}
}
impl MeioDeContato for Celular {
fn envia_mensagem(
&self, mensagem: String
) {
envia_sms(
&self.ddd,
&self.numero,
&mensagem[..=256]
);
}
}
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
Tipos Genéricos podem ser utilizados para implementar traços automaticamente.
use std::{fmt::Display, iter::IntoIterator};
fn main() {
[10, 20, 30].imprime();
["Olá", "Mundo!"].imprime();
}
trait Imprime {
fn imprime(self);
}
impl<T: Display, It: IntoIterator<Item = T>> Imprime for It {
fn imprime(self) {
for i in self.into_iter() {
println!("{i}");
}
}
}
É possível apagar as informações de um tipo por meio de ponteiros (Box
) ou referências. Quando usados, é possível omitir o tamanho que um tipo ocupa na pilha (stack) e, portanto, não permitem que acessemos os campos internos quando utilizados.
Por este motivo, é necessário definir operações que podem ser utilizadas por meio de traços
fn main() {
imprime(&4);
imprime(&"Olá");
}
fn imprime(obj: &dyn std::fmt::Display) {
println!("{}", obj);
}