Uno de los grandes descubrimientos en programación es que es posible escribir código que opera en valores de muchos tipos diferentes, incluso tipos que aún no han sido inventados. Aquí tienes dos ejemplos:
Vec<T>
es genérico: puedes crear un vector de cualquier tipo de valor, incluyendo tipos definidos en tu programa que los autores de Vec nunca anticiparon..write()
, incluyendo archivos (Files
) y flujos TCP (TcpStreams
). Tu código puede tomar un escritor o writer
por referencia, cualquier writer
, y enviarle datos. Tu código no tiene que preocuparse por qué tipo de writer
es. Más adelante, si alguien agrega un nuevo tipo de writer
, tu código ya lo admitirá.Por supuesto, esta capacidad no es nueva en Rust. Se llama polimorfismo y fue la nueva tecnología en lenguajes de programación en la década de 1970. Ahora es prácticamente universal. Rust admite el polimorfismo con dos características relacionadas: traits y genéricos. Estos conceptos serán familiares para muchos programadores, pero Rust toma un enfoque fresco inspirado en las clases de tipos de Haskell.
Los traits
son la forma en que Rust maneja las interfaces o las clases base abstractas. Al principio, se parecen a las interfaces en Java o C#. El trait
para escribir bytes se llama std::io::Write
, y su definición en la biblioteca estándar comienza así:
trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;
fn write_all(&mut self, buf: &[u8]) -> Result<()> { ... }
...
}
Este trait
ofrece varios métodos; solo hemos mostrado los primeros tres.
Los tipos estándar File
y TcpStream
implementan ambos std::io::Write
. Lo mismo ocurre con Vec<u8>
. Los tres tipos proporcionan métodos llamados .write()
, .flush()
, y así sucesivamente.
El código que utiliza un escritor o writer
sin importar su tipo se ve así:
use std::io::Write;
fn say_hello(out: &mut dyn Write) -> std::io::Result<()> {
out.write_all(b"hello world\\n")?;
out.flush()
}
El tipo de "out
" es "&mut dyn Write
", lo que significa "una referencia mutable a cualquier valor que implemente el trait Write
". Podemos pasarle a "say_hello
" una referencia mutable a cualquier valor de ese tipo:
use std::fs::File;
let mut local_file = File::create("hello.txt")?;
say_hello(&mut local_file)?; // works
let mut bytes = vec![];
say_hello(&mut bytes)?; // also works
assert_eq!(bytes, b"hello world\\n");
Este capítulo comienza mostrando cómo se utilizan los traits, cómo funcionan y cómo definir los propios. Pero hay más en los traits de lo que hemos insinuado hasta ahora. Los utilizaremos para agregar métodos de extensión a tipos existentes, incluso tipos incorporados como str y bool. Explicaremos por qué agregar un trait a un tipo no tiene costo adicional de memoria y cómo utilizar los traits sin la sobrecarga de llamadas de métodos virtuales. Veremos que los traits incorporados son el gancho en el lenguaje que Rust proporciona para la sobrecarga de operadores y otras características. Y cubriremos el tipo Self, las funciones asociadas y los tipos asociados, tres características que Rust tomó de Haskell y que resuelven elegantemente problemas que otros lenguajes abordan con soluciones alternativas y trucos.
Los genéricos son el otro tipo de polimorfismo en Rust. Al igual que una plantilla en C++, una función o tipo genérico puede ser utilizado con valores de muchos tipos diferentes:
/// Given two values, pick whichever one is less.
fn min<T: Ord>(value1: T, value2: T) -> T {
if value1 <= value2 {
value1
} else {
value2
}
}
El <T: Ord>
en esta función significa que min
puede ser utilizado con argumentos de cualquier tipo T
que implemente el trait Ord
, es decir, cualquier tipo ordenado. Un requisito como este se llama límite o restricción (bound), porque establece límites en los posibles tipos T
que podrían ser utilizados. El compilador genera código de máquina personalizado para cada tipo T
que realmente utilices.
Los genéricos y los traits están estrechamente relacionados: las funciones genéricas utilizan traits en sus restricciones (bounds) para especificar qué tipos de argumentos pueden ser aplicados. Por lo tanto, también hablaremos sobre cómo &mut dyn Write
y <T: Write>
son similares, cómo son diferentes y cómo elegir entre estas dos formas de utilizar los traits.
Un trait es una característica que cualquier tipo dado puede o no soportar. Con mayor frecuencia, un trait representa una capacidad: algo que un tipo puede hacer.
std::io::Write
puede escribir bytes.std::iter::Iterator
puede producir una secuencia de valores.std::clone::Clone
puede hacer clones de sí mismo en memoria.std::fmt::Debug
se puede imprimir usando println!()
con el especificador de formato {:?}
.
Esos cuatro traits forman parte de la biblioteca estándar de Rust y muchos tipos estándar los implementan. Por ejemplo: