Podemos usar genéricos para criar definições para itens como assinaturas de função ou estruturas, que podemos então usar com muitos tipos de dados concretos diferentes. Vejamos primeiro como definir funções, estruturas, enums e métodos usando genéricos. Em seguida, discutiremos como os genéricos afetam o desempenho do código.
Em Definições de Função
Ao definir uma função que usa genéricos, colocamos os genéricos na assinatura da função onde normalmente especificaríamos os tipos de dados dos parâmetros e o valor de retorno. Isso torna nosso código mais flexível e fornece mais funcionalidade aos chamadores de nossa função, evitando a duplicação de código.
Continuando com nossa função maior
, a Listagem 10-4 mostra duas funções que encontram o maior valor em uma fatia.
Nome do arquivo: src/main.rs
A função maior_i32
é aquela que extraímos na Listagem 10-3 que encontra a maior i32
em uma fatia. A função maior_char
encontra o maior char
em uma fatia. Os corpos das funções têm o mesmo código, portanto, vamos eliminar a duplicação introduzindo um parâmetro de tipo genérico em uma única função.
Para parametrizar os tipos na nova função que definiremos, precisamos nomear o parâmetro de tipo, assim como fazemos para os parâmetros de valor de uma função. Você pode usar qualquer identificador como um nome de parâmetro de tipo. Mas usaremos T
porque, por convenção, os nomes dos parâmetros no Rust são curtos, geralmente apenas uma letra, e a convenção de nomenclatura de tipo do Rust é CamelCase. Abreviação de “tipo”, T
é a escolha padrão da maioria dos programadores do Rust.
Quando usamos um parâmetro no corpo da função, temos que declarar o nome do parâmetro na assinatura para que o compilador saiba o que esse nome significa. Da mesma forma, quando usamos um nome de parâmetro de tipo em uma assinatura de função, temos que declarar o nome do parâmetro de tipo antes de usá-lo. Para definir a função genérica maior
, coloque as declarações de nome de tipo entre colchetes angulares, <>
, entre o nome da função e a lista de parâmetros, assim:
fn maior<T>(list: &[T]) -> &T {
Lemos esta definição como: a função maior
é genérica sobre algum tipo T
. Essa função tem um parâmetro denominado list
, que é uma fatia de valores do tipo T
. A função maior
retornará uma referência a um valor do mesmo tipo T
.
A Listagem 10-5 mostra a definição da função maior
combinada usando o tipo de dados genérico em sua assinatura. A lista também mostra como podemos chamar a função com uma fatia de valores i32
ou valores char
. Observe que este código não será compilado ainda, mas vamos consertar isso mais tarde neste capítulo.
Nome do arquivo: src/main.rs
Esse código não compila.
fn maior<T>(list: &[T]) -> &T {
let mut maior = &list[0];
for item in list {
if item > maior {
maior = item;
}
}
maior
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = maior(&number_list);
println!("O maior numero é {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = maior(&char_list);
println!("O maior char é {}", result);
}
Se compilarmos este código agora, obteremos este erro:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `&T`
--> src/main.rs:5:17
|
5 | if item > maior {
| ---- ^ ------- &T
| |
| &T
|
help: consider restricting type parameter `T`
|
1 | fn maior<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
| ^^^^^^^^^^^^^^^^^^^^^^
error: aborting due to previous error
For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10`
To learn more, run the command again with --verbose.
A nota menciona std::cmp::PartialOrd
, que é um característica. Falaremos sobre as características na próxima seção. Por enquanto, esse erro afirma que o corpo de maior
não funcionará para todos os tipos possíveis que T
poderiam ser. Como queremos comparar valores de tipo T
no corpo, só podemos usar tipos cujos valores podem ser solicitados. Para permitir comparações, a biblioteca padrão tem a característica std::cmp::PartialOrd
que você pode implementar em tipos (consulte o Apêndice C para mais informações sobre esta característica). Você aprenderá como especificar que um tipo genérico tem uma característica particular na seção “Características como parâmetros”, mas vamos primeiro explorar outras maneiras de usar parâmetros de tipo genérico.
Em Definições Struct
Também podemos definir estruturas para usar um parâmetro de tipo genérico em um ou mais campos usando a sintaxe <>
. A Listagem 10-6 mostra como definir uma estrutura Point<T>
para conter os valores das coordenadas x
e y
de qualquer tipo.
Nome do arquivo: src/main.rs
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}
A sintaxe para usar genéricos em definições de estrutura é semelhante à usada em definições de função. Primeiro, declaramos o nome do parâmetro de tipo entre colchetes angulares logo após o nome da estrutura. Então, podemos usar o tipo genérico na definição de estrutura, onde, de outra forma, especificaríamos tipos de dados concretos.
Observe que, como nós usamos apenas um tipo genérico para definir Point<T>
, esta definição diz que a estrutura Point<T>
é genérica sobre algum tipo T
, e os campos x
e y
são ambos do mesmo tipo, qualquer que seja o tipo. Se criarmos uma instância de Point<T>
que possui valores de tipos diferentes, como na Listagem 10-7, nosso código não será compilado.
Nome do arquivo: src/main.rs
Esse código não compila.
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}
Neste exemplo, quando atribuímos o valor inteiro 5 a x
, informamos ao compilador que o tipo genérico T
será um inteiro para esta instância de Point<T>
. Então, quando especificarmos 4.0 para y
, que definimos como sendo do mesmo tipo x
, obteremos um erro de incompatibilidade de tipo como este:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0308]: mismatched types
--> src/main.rs:7:38
|
7 | let wont_work = Point { x: 5, y: 4.0 };
| ^^^ expected integer, found floating-point number
error: aborting due to previous error
For more information about this error, try `rustc --explain E0308`.
error: could not compile `chapter10`
To learn more, run the command again with --verbose.
Para definir uma estrutura Point
onde x
e y
são genéricos, mas podem ter tipos diferentes, podemos usar vários parâmetros de tipo genérico. Por exemplo, na Listagem 10-8, podemos alterar a definição de Point
para ser genérico sobre os tipos T
e U
onde x
é do tipo T
e y
é do tipo U
.
Nome do arquivo: src/main.rs
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let both_integer = Point { x: 5, y: 10 };
let both_float = Point { x: 1.0, y: 4.0 };
let integer_and_float = Point { x: 5, y: 4.0 };
}
Agora todas as instâncias de Point
mostrado são permitidas! Você pode usar quantos parâmetros de tipo genérico quiser em uma definição, mas usar mais do que alguns torna seu código difícil de ler. Quando você precisa de muitos tipos genéricos em seu código, isso pode indicar que seu código precisa ser reestruturado em partes menores.
Em Definições Enum
Como fizemos com structs, podemos definir enums para conter tipos de dados genéricos em suas variantes. Vamos dar uma outra olhada no enum Option<T>
que a biblioteca padrão fornece, que usamos no Capítulo 6:
Esta definição agora deve fazer mais sentido para você. Como você pode ver, Option<T>
é um enum que é genérico sobre o tipo T
e tem duas variantes:, Some
que contém um valor de tipo T
e uma variante None
que não contém nenhum valor. Usando o enum Option<T>
, podemos expressar o conceito abstrato de ter um valor opcional e, por Option<T>
ser genérico, podemos usar essa abstração independentemente do tipo do valor opcional.
Enums também podem usar vários tipos genéricos. A definição do enum Result
que usamos no Capítulo 9 é um exemplo:
O enum Result
é genérico em dois tipos, T
e E
, e tem duas variantes: Ok
que contém um valor de tipo T
e Err
, que contém um valor de tipo E
. Essa definição torna conveniente usar o enum Result
em qualquer lugar em que tenhamos uma operação que pode ser bem-sucedida (retornar um valor de algum tipo T
) ou se falhar (retornar um erro de algum tipo E
). Na verdade, é isso que usamos para abrir um arquivo na Listagem 9-3, onde T
foi preenchido com o tipo std::fs::File
quando o arquivo foi aberto com sucesso e E
foi preenchido com o tipo std::io::Error
quando houve problemas ao abrir o arquivo.
Ao reconhecer situações em seu código com várias definições de struct ou enum que diferem apenas nos tipos dos valores que contêm, você pode evitar a duplicação usando tipos genéricos.
Em Definições de Método
Podemos implementar métodos em structs e enums (como fizemos no Capítulo 5) e usar tipos genéricos em suas definições também. A Listagem 10-9 mostra a estrutura Point<T>
que definimos na Listagem 10-6 com um método denominado x
implementado nela.
Nome do arquivo: src/main.rs
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}
Aqui, definimos um método denominado x
em Point<T>
que retorna uma referência aos dados no campo x
.
Observe que temos que declarar T
logo depois de impl
para que possamos usá-lo para especificar que estamos implementando métodos no tipo Point<T>
. Ao declarar T
como um tipo genérico depois de impl
, Rust pode identificar que o tipo entre os colchetes angulares em Point
é um tipo genérico em vez de um tipo concreto.
Poderíamos, por exemplo, implementar métodos apenas nas instâncias de Point<f32>
, em vez das instâncias Point<T>
com qualquer tipo genérico. Na Listagem 10-10, usamos o tipo concreto f32
, o que significa que não declaramos nenhum tipo depois de impl
.
Nome do arquivo: src/main.rs
Este código significa que o tipo Point<f32>
terá um método denominado distancia_da_origen
e outras instâncias de Point<T>
onde T
não é do tipo f32
não terão esse método definido. O método mede o quão longe nosso ponto está do ponto nas coordenadas (0,0, 0,0) e usa operações matemáticas que estão disponíveis apenas para tipos de ponto flutuante.
Os parâmetros de tipo genérico em uma definição de estrutura nem sempre são os mesmos que você usa nas assinaturas de método dessa estrutura. Por exemplo, a Listagem 10-11 define o método mixup
na estrutura Point<T, U>
da Listagem 10-8. O método recebe outro Point
como parâmetro, que pode ter tipos diferentes do self
Point
que estamos chamando mixup
. O método cria uma nova instância de Point
com o valor x
de self
Point
(do tipo T
) e o valor y
do passado Point
(do tipo W
).
Nome do arquivo: src/main.rs
struct Point<T, U> {
x: T,
y: U,
}
impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y,
}
}
}
fn main() {
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c' };
let p3 = p1.mixup(p2);
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}
Em main
, definimos um Point
que possui um i32
para x
(com valor 5
) e um f64
para y
(com valor 10.4
). A variável p2
é uma estrutura Point
que possui uma fatia de string para x
(com valor "Hello"
) e uma char
para y
(com valor c
). Chamando mixup
em p1
com o argumento p2
nos dá p3
, que terá um i32
para x
, porque x
veio de p1
. A variável p3
terá um char
para y
, porque y
veio de p2
. A chamada da macro println!
será impressa p3.x = 5, p3.y = c
.
O objetivo deste exemplo é demonstrar uma situação em que alguns parâmetros genéricos são declarados com impl
e alguns são declarados com a definição do método. Aqui, os parâmetros genéricos T
e U
são declarados depois de impl
, porque vão com a definição da estrutura. Os parâmetros genéricos V
e W
são declarados depois de fn mixup
, porque são relevantes apenas para o método.
Desempenho do código usando genéricos
Você pode estar se perguntando se há um custo de tempo de execução ao usar parâmetros de tipo genérico. A boa notícia é que o Rust implementa genéricos de forma que seu código não execute mais devagar usando tipos genéricos do que faria com tipos concretos.
Rust faz isso realizando a monomorfização do código que está usando genéricos em tempo de compilação. Monomorfização é o processo de transformar código genérico em código específico, preenchendo os tipos concretos que são usados quando compilados.
Nesse processo, o compilador faz o oposto das etapas que usamos para criar a função genérica na Listagem 10-5: o compilador examina todos os lugares onde o código genérico é chamado e gera o código para os tipos concretos com os quais o código genérico é chamado.
Vejamos como isso funciona com um exemplo que usa o enum Option<T>
da biblioteca padrão:
Quando o Rust compila esse código, ele realiza a monomorfização. Durante esse processo, o compilador lê os valores que foram usados nas instâncias de Option<T>
e identifica dois tipos de Option<T>
: um é i32
e o outro é f64
. Como tal, ele expande a definição genérica de Option<T>
em Option_i32
e Option_f64
, assim, substituindo a definição genérica por outras específicas.
A versão monomorfizada do código se parece com o seguinte. O genérico Option<T>
é substituído pelas definições específicas criadas pelo compilador:
Nome do arquivo: src/main.rs
enum Option_i32 {
Some(i32),
None,
}
enum Option_f64 {
Some(f64),
None,
}
fn main() {
let integer = Option_i32::Some(5);
let float = Option_f64::Some(5.0);
}
Como o Rust compila o código genérico em um código que especifica o tipo em cada instância, não pagamos nenhum custo de tempo de execução pelo uso de genéricos. Quando o código é executado, ele funciona exatamente como se tivéssemos duplicado cada definição manualmente. O processo de monomorfização torna os genéricos de Rust extremamente eficientes em tempo de execução.
Traduzido por Acervo Lima. O original pode ser acessado aqui.
0 comentários:
Postar um comentário