Ir al contenido

Genéricos avanzados en Rust: const generics, const fn y PhantomData

En la página de Genéricos, un parámetro genérico siempre era un tipo: Pair<T> podía guardar i32 o &str. Esta página te enseña tres herramientas que llevan esa misma idea más lejos:

  • Los const generics (genéricos constantes) permiten que un número sea un parámetro genérico, como el 3 de [i32; 3].
  • const fn permite que el compilador ejecute tu función mientras construye el programa, así que el resultado ya está listo antes de que el programa arranque.
  • PhantomData permite que un tipo lleve una etiqueta (como “esto es el ID de un usuario” o “esta longitud está en pies”) sin guardar ningún dato extra.

Piensa en una caja de huevos. Una caja de 6 y una de 12 son productos distintos, aunque las dos sean “cajas de huevos”. El tamaño forma parte de lo que son. Los const generics te permiten decir eso en Rust. Ve con calma: cada sección se entiende por sí sola, y puedes volver a las últimas cuando las necesites.

Const generics: un número como parámetro genérico

Sección titulada «Const generics: un número como parámetro genérico»

En Rust, la longitud de un array forma parte de su tipo. [u32; 3] y [u32; 4] son tan distintos como i32 y String. Una función que recibe [u32; 3] no acepta cuatro elementos:

error[E0308]: mismatched types
--> src/main.rs:7:26
|
7 | println!("{}", total(basket));
| ----- ^^^^^^ expected an array with a size of 3, found one with a size of 4
| |
| arguments to this function are incorrect

El compilador te dice que esperaba un array de tamaño 3 y encontró uno de tamaño 4. Un const generic lo resuelve. Escribes const N: usize entre los ángulos, y la función sirve para un array de cualquier longitud:

fn summary<const N: usize>(prices: [u32; N]) -> String {
let mut sum = 0;
for price in prices {
sum += price;
}
// Dentro de la función, N es un número normal
format!("{N} artículos cuestan {sum}")
}
fn main() {
let small_basket = [4, 10, 2];
let big_basket = [3, 8, 1, 12, 6];
println!("{}", summary(small_basket));
println!("{}", summary(big_basket));
}

Salida:

3 artículos cuestan 16
5 artículos cuestan 30
▶ Pruébalo tú mismo

Lee <const N: usize> como “para cualquier número N de tipo usize”. Rust deduce N a partir del array que le pasas, igual que deduce T en los genéricos normales. Y, también como en los genéricos normales, el compilador crea una copia aparte de summary para cada longitud que uses, así que no hay coste en tiempo de ejecución.

Seamos sinceros: para esta funcioncita, un slice (porción) como prices: &[u32] funcionaría igual de bien y es más sencillo. Mira Slices. Los const generics brillan de verdad cuando la longitud tiene que ser parte de un tipo: guardada en un struct sin reservar memoria en el heap, o usada en un tipo de retorno. Eso viene ahora.

Structs con const generics: una cuadrícula de asientos

Sección titulada «Structs con const generics: una cuadrícula de asientos»

Imagina el plano de asientos de un cine: unas cuantas filas, todas con el mismo número de asientos. Aquí tienes una matriz cuyo número de filas R y de columnas C forman parte de su tipo:

struct Matrix<const R: usize, const C: usize> {
cells: [[i32; C]; R],
}
impl<const R: usize, const C: usize> Matrix<R, C> {
fn new(cells: [[i32; C]; R]) -> Self {
Matrix { cells }
}
fn size(&self) -> String {
format!("{R}x{C}")
}
// Las filas pasan a ser columnas: una matriz R x C se convierte en una C x R
fn transpose(&self) -> Matrix<C, R> {
let mut cells = [[0; R]; C];
for row in 0..R {
for col in 0..C {
cells[col][row] = self.cells[row][col];
}
}
Matrix { cells }
}
}
fn main() {
let seats = Matrix::new([[1, 2, 3], [4, 5, 6]]);
let flipped = seats.transpose();
println!("{} {:?}", seats.size(), seats.cells);
println!("{} {:?}", flipped.size(), flipped.cells);
}

Salida:

2x3 [[1, 2, 3], [4, 5, 6]]
3x2 [[1, 4], [2, 5], [3, 6]]
▶ Pruébalo tú mismo

Fíjate en el tipo de retorno de transpose: Matrix<C, R>. El compilador sabe que al voltear una matriz de 2x3 sale una de 3x2. Si te equivocaras por dentro, por ejemplo construyendo una matriz con la forma incorrecta, no compilaría. Toda la cuadrícula vive dentro del struct, sin Vec y sin heap. La línea impl<const R: usize, const C: usize> funciona como impl<T>: dice “estos métodos existen para cualquier tamaño”.

Un ejemplo práctico: un buffer circular de tamaño fijo

Sección titulada «Un ejemplo práctico: un buffer circular de tamaño fijo»

La cámara de un coche (dashcam) guarda solo los últimos minutos de vídeo. Cuando se llena, la grabación más nueva sobrescribe la más antigua. Eso es un buffer circular (ring buffer), y encaja perfecto con los const generics: la capacidad es fija, así que puedes guardar los elementos en un array.

Este es genérico sobre un tipo T y un tamaño N a la vez, por eso la línea del impl dice impl<T, const N: usize>:

struct RingBuffer<T, const N: usize> {
items: [T; N],
next: usize, // dónde irá el siguiente push
len: usize, // cuántos elementos hay guardados (nunca más de N)
}
impl<T: Copy + Default, const N: usize> RingBuffer<T, N> {
fn new() -> Self {
RingBuffer { items: [T::default(); N], next: 0, len: 0 }
}
fn push(&mut self, item: T) {
self.items[self.next] = item; // cuando está lleno, sobrescribe el más antiguo
self.next = (self.next + 1) % N;
if self.len < N {
self.len += 1;
}
}
// Los elementos guardados, del más antiguo al más nuevo
fn to_vec(&self) -> Vec<T> {
let start = (self.next + N - self.len) % N;
(0..self.len).map(|i| self.items[(start + i) % N]).collect()
}
}
fn main() {
// Guarda solo las 3 últimas lecturas de temperatura
let mut recent: RingBuffer<i32, 3> = RingBuffer::new();
for temp in [18, 19, 21, 24, 22] {
recent.push(temp);
println!("después de {temp}: {:?}", recent.to_vec());
}
}

Salida:

después de 18: [18]
después de 19: [18, 19]
después de 21: [18, 19, 21]
después de 24: [19, 21, 24]
después de 22: [21, 24, 22]
▶ Pruébalo tú mismo

[T::default(); N] llena el array con N copias de un valor por defecto (0 para i32). Repetir un valor así exige T: Copy, y por eso el impl pide Copy + Default. Un RingBuffer<i32, 3> y un RingBuffer<i32, 60> son tipos distintos, así que nunca podrás pasar un buffer de 3 huecos donde se espera uno de 60. Si prefieres la versión de la biblioteca estándar que puede crecer, mira VecDeque y BinaryHeap.

Valores por defecto, llaves y los límites actuales

Sección titulada «Valores por defecto, llaves y los límites actuales»

Un parámetro constante puede tener un valor por defecto, que se escribe const N: usize = 4. También puedes pasar una constante con nombre, o una expresión entre llaves:

struct Buffer<const N: usize = 4> {
data: [u8; N],
}
impl<const N: usize> Buffer<N> {
fn new() -> Self {
Buffer { data: [0; N] }
}
}
const PAGE: usize = 8;
fn main() {
let small: Buffer = Buffer::new(); // sin número: el valor por defecto, 4
let page = Buffer::<PAGE>::new(); // una constante con nombre
let double = Buffer::<{ PAGE * 2 }>::new(); // una expresión necesita { }
println!("{} {} {}", small.data.len(), page.data.len(), double.data.len());
}

Salida:

4 8 16
▶ Pruébalo tú mismo

El valor por defecto solo se aplica cuando escribes el tipo sin número, como en let small: Buffer. Un simple let small = Buffer::new(); falla con error[E0284]: type annotations needed for Buffer<_> (“hacen falta anotaciones de tipo”), porque Rust no adivina un valor por defecto a partir de una expresión.

Los const generics de Rust estable son útiles, pero están limitados a propósito. Conviene conocer los bordes antes de chocar con ellos:

  • Tipos permitidos: solo enteros (usize, u8, i32, …), bool y char. Si pruebas const NAME: &'static str, obtienes “&'static str is forbidden as the type of a const generic parameter” (ese tipo está prohibido para un parámetro const genérico).
  • Nada de cálculos con un parámetro genérico dentro de un tipo. [f32; N * 2] dentro de un struct o una firma genérica es un error (“generic parameters may not be used in const operations”). N a secas está bien, y N * 2 también, pero dentro del cuerpo de una función, donde N es solo un valor. La función del lenguaje que permitiría N * 2 en los tipos (generic_const_exprs) sigue estando solo en nightly.
  • Comprobar un valor. No puedes escribir una restricción del estilo “N tiene que ser mayor que 0”. Lo más parecido es un bloque const dentro de una función: const { assert!(N > 0, "need at least one slot") };. Falla cuando compilas el programa con un N incorrecto (puede que cargo check no lo detecte).

const fn: deja que el compilador haga las cuentas

Sección titulada «const fn: deja que el compilador haga las cuentas»

Algunas tiendas imprimen su lista de precios una sola vez, en lugar de calcular cada precio en la caja. Una const fn es una función que el compilador puede ejecutar mientras construye tu programa, así que el resultado queda “impreso” en el programa de antemano.

Ya conociste los elementos const en la página de Variables. Su valor tiene que conocerse en tiempo de compilación, así que normalmente solo puedes usar literales y cálculos sencillos. Con const fn también puedes llamar ahí a tus propias funciones:

const fn square(n: usize) -> usize {
n * n
}
const BOARD_SIDE: usize = 4;
const CELLS: usize = square(BOARD_SIDE); // lo calcula el compilador
fn main() {
// La longitud de un array tiene que conocerse al compilar, y CELLS se conoce
let board = [0u8; CELLS];
println!("un tablero de {BOARD_SIDE}x{BOARD_SIDE} tiene {} casillas", board.len());
println!("square(12) en tiempo de ejecución es {}", square(12));
}

Salida:

un tablero de 4x4 tiene 16 casillas
square(12) en tiempo de ejecución es 144
▶ Pruébalo tú mismo

Una const fn sigue siendo una función normal. square(12) dentro de main es una llamada corriente en tiempo de ejecución. Solo tiene que ejecutarse al compilar cuando se usa en un contexto const: un elemento const o static, la longitud de un array, un argumento const genérico o un bloque const { ... }.

Un uso clásico es una tabla de consulta (lookup table): calculas una lista de valores una vez, al compilar, y en tiempo de ejecución solo la lees. Aquí el compilador construye la tabla “día del año en que empieza cada mes” para un año no bisiesto:

const DAYS_IN_MONTH: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const fn days_before_each_month() -> [u32; 12] {
let mut table = [0; 12];
let mut month = 1;
while month < 12 {
table[month] = table[month - 1] + DAYS_IN_MONTH[month - 1];
month += 1;
}
table
}
// La construye el compilador una sola vez. Al ejecutar son solo 12 números.
const DAYS_BEFORE: [u32; 12] = days_before_each_month();
fn day_of_year(month: usize, day: u32) -> u32 {
DAYS_BEFORE[month - 1] + day
}
fn main() {
println!("{:?}", DAYS_BEFORE);
println!("el 1 de marzo es el día {}", day_of_year(3, 1));
println!("el 25 de diciembre es el día {}", day_of_year(12, 25));
}

Salida:

[0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
el 1 de marzo es el día 60
el 25 de diciembre es el día 359
▶ Pruébalo tú mismo

Fíjate en el bucle while en lugar de un for. No es una cuestión de estilo. Esto es lo que sí puedes usar hoy dentro de una const fn:

  • let y let mut, aritmética, comparaciones, if, match, loop y while
  • arrays, tuplas, structs y enums, e índices como table[month]
  • llamadas a otras const fn (muchas de la biblioteca estándar son const, como u32::pow o slice.len())
  • referencias &mut y cálculos con números de punto flotante (estables desde Rust 1.83 y 1.82)
  • panic! y assert!, que se convierten en errores de compilación si se disparan al compilar

Y lo que todavía no puedes usar: bucles for (necesitan el trait Iterator, y los métodos de traits no son const en estable), llamadas a funciones normales como String::from o println!, y cualquier cosa que reserve memoria en el heap, como hacer push a un Vec.

¿const o static? Los dos son valores globales que nunca cambian, pero se guardan de forma distinta:

const static
Dónde vive se copia en cada sitio donde lo usas en un único lugar fijo de la memoria
Dirección puede cambiar cada vez siempre la misma; vive durante todo el programa ('static)
¿Puede ser mutable? nunca existe static mut, pero necesita unsafe
Úsalo para números, tablas pequeñas, ajustes una tabla grande que quieres guardar una sola vez, o un global del que necesitas una referencia

Si dudas, usa const. Si necesitas un global que puedas cambiar, usa un static con un atómico o un Mutex dentro, en lugar de static mut. Mira Estado compartido.

PhantomData: una etiqueta que no se puede confundir

Sección titulada «PhantomData: una etiqueta que no se puede confundir»

Este es un bug que pasa constantemente en sistemas reales. Los IDs de usuario y los IDs de pedido son solo números, así que nada te impide pasar un ID de usuario a cancel_order. Te gustaría que el tipo llevara una etiqueta: Id<User> frente a Id<Order>. Tu primer intento podría ser este:

struct Id<T> {
value: u32,
}
error[E0392]: type parameter `T` is never used
--> src/main.rs:1:11
|
1 | struct Id<T> {
| ^ unused type parameter
|
= help: consider removing `T`, referring to it in a field, or using a marker such as `PhantomData`

Rust rechaza un parámetro de tipo que ningún campo usa (“type parameter T is never used”). La solución es la que sugiere el propio compilador. std::marker::PhantomData<T> es un campo que dice “este struct está ligado a T”, pero no guarda nada. Ocupa cero bytes:

use std::marker::PhantomData;
struct User;
struct Order;
struct Id<T> {
value: u32,
_marker: PhantomData<T>, // cero bytes, solo una etiqueta
}
impl<T> Id<T> {
fn new(value: u32) -> Self {
Id { value, _marker: PhantomData }
}
}
fn find_user(id: &Id<User>) -> &'static str {
if id.value == 7 { "Amara" } else { "desconocido" }
}
fn cancel_order(id: &Id<Order>) {
println!("cancelando el pedido #{}", id.value);
}
fn main() {
let user_id: Id<User> = Id::new(7);
let order_id: Id<Order> = Id::new(7);
println!("el usuario #{} es {}", user_id.value, find_user(&user_id));
cancel_order(&order_id);
println!("un Id ocupa {} bytes", std::mem::size_of::<Id<User>>());
}

Salida:

el usuario #7 es Amara
cancelando el pedido #7
un Id ocupa 4 bytes
▶ Pruébalo tú mismo

Los dos IDs guardan el número 7, y los dos ocupan 4 bytes, exactamente como un u32 normal. Pero ahora prueba cancel_order(&user_id);:

error[E0308]: mismatched types
--> src/main.rs:32:18
|
32 | cancel_order(&user_id);
| ------------ ^^^^^^^^ expected `&Id<Order>`, found `&Id<User>`
| |
| arguments to this function are incorrect

El compilador esperaba un &Id<Order> y encontró un &Id<User>: la confusión se detecta antes de que el programa llegue a ejecutarse. PhantomData es un valor además de un tipo, y por eso escribes _marker: PhantomData al construir el struct. El _ del principio es solo una costumbre habitual para decir “este campo nunca se lee”.

En 1999, la NASA perdió la sonda Mars Climate Orbiter porque un programa daba números en unidades imperiales y otro esperaba unidades métricas. Ambos eran “solo números”. Con PhantomData, la unidad pasa a formar parte del tipo, y un único Length<U> genérico sirve para todas las unidades:

use std::marker::PhantomData;
use std::ops::Add;
struct Meters;
struct Feet;
struct Length<U> {
value: f64,
_unit: PhantomData<U>,
}
impl<U> Length<U> {
fn new(value: f64) -> Self {
Length { value, _unit: PhantomData }
}
}
// Un solo impl para todas las unidades, pero los dos lados deben usar la misma
impl<U> Add for Length<U> {
type Output = Length<U>;
fn add(self, other: Length<U>) -> Length<U> {
Length::new(self.value + other.value)
}
}
impl Length<Feet> {
fn to_meters(self) -> Length<Meters> {
Length::new(self.value * 0.3048)
}
}
fn main() {
let run: Length<Meters> = Length::new(400.0) + Length::new(250.0);
let climb: Length<Feet> = Length::new(1000.0);
let climb_m = climb.to_meters();
println!("subida: {:.1} m", climb_m.value);
let total = run + climb_m;
println!("total: {:.1} m", total.value);
}

Salida:

subida: 304.8 m
total: 954.8 m
▶ Pruébalo tú mismo

Escribe run + climb (olvidando to_meters()) y Rust se niega:

error[E0308]: mismatched types
--> src/main.rs:39:23
|
39 | let total = run + climb;
| ^^^^^ expected `Length<Meters>`, found `Length<Feet>`

Esperaba metros y encontró pies. En la página de Traits avanzados, Meters y Centimeters eran dos structs separados, cada uno con su propio impl de Add. Aquí, un solo impl<U> Add for Length<U> sirve para cualquier unidad que vayas a sumar.

Typestate: una puerta que no se puede cerrar con llave si está abierta

Sección titulada «Typestate: una puerta que no se puede cerrar con llave si está abierta»

El mismo truco sirve para seguir el estado de algo. Este patrón se llama typestate (estado en el tipo). Una puerta puede estar abierta, cerrada o cerrada con llave. Puedes echar la llave a una puerta cerrada, pero echarle la llave a una puerta abierta no tiene sentido. Con una etiqueta de estado en el tipo, los movimientos no válidos sencillamente no existen:

use std::marker::PhantomData;
struct Open;
struct Closed;
struct Locked;
struct Door<State> {
room: String,
_state: PhantomData<State>,
}
// Disponible en cualquier estado
impl<State> Door<State> {
fn room(&self) -> &str {
&self.room
}
// La misma puerta, con una nueva etiqueta de estado
fn into_state<Next>(self, action: &str) -> Door<Next> {
println!("{}: {action}", self.room);
Door { room: self.room, _state: PhantomData }
}
}
impl Door<Closed> {
fn new(room: &str) -> Self {
Door { room: room.to_string(), _state: PhantomData }
}
fn open(self) -> Door<Open> {
self.into_state("se abre")
}
fn lock(self) -> Door<Locked> {
self.into_state("se cierra con llave")
}
}
impl Door<Open> {
fn close(self) -> Door<Closed> {
self.into_state("se cierra")
}
}
impl Door<Locked> {
fn unlock(self) -> Door<Closed> {
self.into_state("se quita la llave")
}
}
fn main() {
let door = Door::new("Sala de servidores").open().close().lock();
println!("{}: cerrada con llave toda la noche", door.room());
let door = door.unlock().open();
println!("{}: abierta otra vez", door.room());
}

Salida:

Sala de servidores: se abre
Sala de servidores: se cierra
Sala de servidores: se cierra con llave
Sala de servidores: cerrada con llave toda la noche
Sala de servidores: se quita la llave
Sala de servidores: se abre
Sala de servidores: abierta otra vez
▶ Pruébalo tú mismo

Ahora intenta echarle la llave a una puerta abierta, Door::new("Sala de servidores").open().lock():

error[E0599]: no method named `lock` found for struct `Door<Open>` in the current scope
--> src/main.rs:50:55
|
7 | struct Door<State> {
| ------------------ method `lock` not found for this struct
...
50 | let door = Door::new("Sala de servidores").open().lock();
| ^^^^ method not found in `Door<Open>`
|
= note: the method was found for `Door<Closed>`

El mensaje dice que no existe un método lock para Door<Open>, y la nota añade que ese método sí existe para Door<Closed>. Cada método recibe self por valor, así que la puerta antigua se consume y no puedes seguir usando una puerta “cerrada” después de abrirla. La página de Patrones de diseño muestra la misma idea de typestate, pero con estados que además guardan sus propios datos. PhantomData es la versión ligera para cuando los estados son solo etiquetas.

Métodos para un solo tamaño y constantes asociadas

Sección titulada «Métodos para un solo tamaño y constantes asociadas»

En la página de Genéricos viste impl Pair<f64>: métodos solo para un tipo concreto. Con los const generics funciona igual. Algunos métodos solo tienen sentido para ciertos tamaños:

struct Matrix<const R: usize, const C: usize> {
cells: [[i32; C]; R],
}
// Solo matrices cuadradas: filas y columnas son el mismo N
impl<const N: usize> Matrix<N, N> {
fn trace(&self) -> i32 {
let mut sum = 0;
for i in 0..N {
sum += self.cells[i][i];
}
sum
}
}
// Solo matrices de 2x2
impl Matrix<2, 2> {
fn determinant(&self) -> i32 {
let [[a, b], [c, d]] = self.cells;
a * d - b * c
}
}
fn main() {
let small = Matrix { cells: [[3, 8], [4, 6]] };
let big = Matrix { cells: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] };
println!("pequeña: traza {}, determinante {}", small.trace(), small.determinant());
println!("grande: traza {}", big.trace());
}

Salida:

pequeña: traza 9, determinante -14
grande: traza 15
▶ Pruébalo tú mismo

big.determinant() no compila, y tampoco trace() sobre una matriz de 2x3: obtienes errores como error[E0599]: no method named trace found for struct Matrix<2, 3>, con la nota the method was found for Matrix<N, N> (el método existe, pero solo para matrices cuadradas). La regla “solo las matrices cuadradas tienen traza” queda escrita en los tipos.

Una constante asociada es una constante que pertenece a un tipo o a un trait, igual que un método. En un trait, cada implementación elige su propio valor, y el código genérico puede leerlo como U::NAME. Démosles a las unidades de antes un símbolo y un tamaño:

use std::fmt;
use std::marker::PhantomData;
trait Unit {
const SYMBOL: &'static str;
const IN_METERS: f64; // cuántos metros mide una de estas unidades
}
struct Meters;
struct Feet;
struct Miles;
impl Unit for Meters {
const SYMBOL: &'static str = "m";
const IN_METERS: f64 = 1.0;
}
impl Unit for Feet {
const SYMBOL: &'static str = "ft";
const IN_METERS: f64 = 0.3048;
}
impl Unit for Miles {
const SYMBOL: &'static str = "mi";
const IN_METERS: f64 = 1609.344;
}
struct Length<U> {
value: f64,
_unit: PhantomData<U>,
}
impl<U: Unit> Length<U> {
fn new(value: f64) -> Self {
Length { value, _unit: PhantomData }
}
// Funciona entre dos unidades cualesquiera, usando sus constantes
fn convert<To: Unit>(&self) -> Length<To> {
Length::new(self.value * U::IN_METERS / To::IN_METERS)
}
}
impl<U: Unit> fmt::Display for Length<U> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:.2} {}", self.value, U::SYMBOL)
}
}
fn main() {
let marathon: Length<Miles> = Length::new(26.2);
let everest: Length<Feet> = Length::new(29032.0);
println!("{} = {}", marathon, marathon.convert::<Meters>());
println!("{} = {}", everest, everest.convert::<Meters>());
println!("{} = {}", everest, everest.convert::<Miles>());
}

Salida:

26.20 mi = 42164.81 m
29032.00 ft = 8848.95 m
29032.00 ft = 5.50 mi
▶ Pruébalo tú mismo

U::IN_METERS se resuelve al compilar, así que en tiempo de ejecución no hay ninguna tabla que buscar. Añadir una unidad nueva, por ejemplo Kilometers, es un bloque impl Unit pequeñito, y convert y Display funcionan con ella al instante. Un impl propio del tipo (inherente) también puede tener constantes asociadas, por ejemplo impl<T, const N: usize> RingBuffer<T, N> { const CAPACITY: usize = N; }, que se lee como RingBuffer::<i32, 3>::CAPACITY.

Si quieres una implementación de un trait para todos los tipos que cumplan una condición, eso es un blanket impl, y lo verás en la página de Traits avanzados.

Estas funciones del lenguaje son potentes, y por eso tientan. Unos consejos sinceros:

  • Muchas veces basta con un slice o un Vec. Si una función solo lee elementos, &[T] acepta cualquier longitud y se lee más fácil. Si la longitud solo se conoce al ejecutar (un archivo, lo que escribe el usuario), de todos modos necesitas un Vec.
  • Los arrays grandes viven en la pila (stack). Un RingBuffer<u8, 10_000_000> son diez megabytes dentro del struct, y eso puede desbordar la pila. Mételo en un Box (mira Smart pointers) o usa un Vec.
  • Cada tamaño o tipo nuevo es una copia nueva del código. Usar un genérico con 20 valores distintos de N genera 20 versiones. Eso cuesta tiempo de compilación y tamaño del binario. Mira Rendimiento.
  • El typestate añade tipos y mensajes de error más largos. Es genial para una API que va a usar mucha gente (un builder, una conexión, un flujo de pago). Para un estado que cambia según datos que solo se conocen al ejecutar, como un estado cargado de una base de datos, usa un enum y match.

Una buena regla: empieza por lo sencillo, y recurre a estas herramientas cuando aparezca un bug real o un coste real que te ayuden a evitar.

Hacer cálculos con un parámetro const dentro de un tipo. Parece natural, pero Rust estable todavía no lo permite:

struct Pairs<const N: usize> {
values: [f32; N * 2],
}
error: generic parameters may not be used in const operations
--> src/main.rs:2:19
|
2 | values: [f32; N * 2],
| ^ cannot perform const operation using `N`
|
= help: const parameters may only be used as standalone arguments here, i.e. `N`

El mensaje dice que los parámetros genéricos no se pueden usar en operaciones const, y que aquí N solo puede ir solo. Soluciones: usa [[f32; 2]; N] (un array de pares), añade un segundo parámetro o usa un Vec si el tamaño no tiene por qué estar en el tipo.

Olvidar las llaves alrededor de una expresión. Buffer::<2 + 2>::new() da error: expressions must be enclosed in braces to be used as const generic arguments (las expresiones deben ir entre llaves), y la línea de ayuda muestra la solución: Buffer::<{ 2 + 2 }>::new(). Un número a secas o el nombre de una const no necesitan llaves.

Llamar a algo que no es const en un contexto const. Una const no puede llamar a una función normal, y una const fn no puede usar un bucle for:

const GREETING: String = String::from("hello");
error[E0015]: cannot call non-const associated function `<String as From<&str>>::from` in constants
--> src/main.rs:1:26
|
1 | const GREETING: String = String::from("hello");
| ^^^^^^^^^^^^^^^^^^^^^
|
= note: calls in constants are limited to constant functions, tuple structs and tuple variants

Rust te avisa de que String::from no es una función const, y de que en las constantes solo se puede llamar a funciones const. Para texto, usa const GREETING: &str = "hello";. Dentro de una const fn, cambia el for por un bucle while con un índice, como en la tabla de consulta de más arriba.

#[derive(Clone, Copy)] en un struct con PhantomData. Esperarías que Id<User> fuera Copy, ya que solo guarda un u32. Pero derive añade la restricción T: Copy, y User no es Copy, así que el ID se mueve en lugar de copiarse:

error[E0382]: use of moved value: `id`
--> src/main.rs:16:10
|
14 | let id: Id<User> = Id { value: 1, _marker: PhantomData };
| -- move occurs because `id` has type `Id<User>`, which does not implement the `Copy` trait
15 | show(id);
| -- value moved here
16 | show(id);
| ^^ value used here after move
|
note: if all bounds were met, you could clone the value
--> src/main.rs:4:1
|
3 | #[derive(Clone, Copy)]
| ----- derived `Clone` adds implicit bounds on type parameters

El error dice que id se usó después de haberse movido, y la nota señala que el Clone derivado añade restricciones ocultas a los parámetros de tipo. La solución más sencilla es derivar Clone, Copy también en los tipos marcador (#[derive(Clone, Copy)] struct User;). O escribir los impls a mano, que no necesitan ninguna restricción sobre T:

impl<T> Clone for Id<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for Id<T> {}

Escribe fn pad_scores<const N: usize>(scores: &[u32]) -> [u32; N], que copie las puntuaciones en un array de exactamente N elementos. Rellena los huecos que falten con 0 y descarta las puntuaciones que sobren. Llámala como pad_scores::<5>(&[90, 85]) y como let top3: [u32; 3] = pad_scores(&[70, 95, 88, 60]);. Pista: empieza con [0; N], luego recorre 0..N y usa scores.get(i).

Ver solución
fn pad_scores<const N: usize>(scores: &[u32]) -> [u32; N] {
let mut out = [0; N];
for i in 0..N {
if let Some(&score) = scores.get(i) {
out[i] = score;
}
}
out
}
fn main() {
let five = pad_scores::<5>(&[90, 85]);
let top3: [u32; 3] = pad_scores(&[70, 95, 88, 60]);
println!("{:?}", five);
println!("{:?}", top3);
}

Salida:

[90, 85, 0, 0, 0]
[70, 95, 88]
▶ Pruébalo tú mismo

Aquí N no puede salir del argumento (un slice no lleva su longitud en el tipo), así que sale del turbofish ::<5> o del tipo que pides, [u32; 3].

Escribe dos const fn, minutes(n: u64) -> u64 y hours(n: u64) -> u64, que devuelvan un número de segundos. Úsalas para definir const SESSION_TIMEOUT: u64 = hours(1) + minutes(30); y un array const RETRY_DELAYS: [u64; 3] con 1, 5 y 15 minutos. Imprime los dos. Pista: hours puede llamar a minutes.

Ver solución
const fn minutes(n: u64) -> u64 {
n * 60
}
const fn hours(n: u64) -> u64 {
minutes(n * 60)
}
const SESSION_TIMEOUT: u64 = hours(1) + minutes(30);
const RETRY_DELAYS: [u64; 3] = [minutes(1), minutes(5), minutes(15)];
fn main() {
println!("tiempo de espera de la sesión: {SESSION_TIMEOUT} segundos");
println!("esperas entre reintentos: {:?}", RETRY_DELAYS);
}

Salida:

tiempo de espera de la sesión: 5400 segundos
esperas entre reintentos: [60, 300, 900]
▶ Pruébalo tú mismo

El compilador calcula el 5400 y la tabla de esperas mientras construye el programa, y hours(1) + minutes(30) se lee mucho mejor que un 5400 suelto. Si le quitas const a minutes, los elementos const dejan de compilar con E0015.

Crea struct Euro; y struct Dollar;, y un Money<C> con un campo cents: i64 y un campo PhantomData<C>. Dale new(cents) y plus(self, other: Money<C>) -> Money<C>. Suma una comida y un café en euros e imprime el total. Pista: como other tiene el mismo C, no se pueden sumar un Money<Euro> y un Money<Dollar>.

Ver solución
use std::marker::PhantomData;
struct Euro;
struct Dollar;
struct Money<C> {
cents: i64,
_currency: PhantomData<C>,
}
impl<C> Money<C> {
fn new(cents: i64) -> Self {
Money { cents, _currency: PhantomData }
}
fn plus(self, other: Money<C>) -> Money<C> {
Money::new(self.cents + other.cents)
}
}
fn main() {
let lunch: Money<Euro> = Money::new(1250);
let coffee: Money<Euro> = Money::new(320);
let ticket: Money<Dollar> = Money::new(4999);
let total = lunch.plus(coffee);
println!("total: {}.{:02} EUR", total.cents / 100, total.cents % 100);
println!("entrada: {}.{:02} USD", ticket.cents / 100, ticket.cents % 100);
}

Salida:

total: 15.70 EUR
entrada: 49.99 USD
▶ Pruébalo tú mismo

Prueba lunch.plus(ticket): falla con expected Money<Euro>, found Money<Dollar> (esperaba euros y encontró dólares). Guardar los céntimos como un entero en lugar de f64 también evita errores de redondeo con el dinero.

  • Los const generics (const N: usize) hacen que un número forme parte de un tipo. Los arrays de longitudes distintas son tipos distintos, e impl<T, const N: usize> escribe métodos para todos los tamaños.
  • En Rust estable, los parámetros const pueden ser enteros, bool o char. Las expresiones usadas como argumento necesitan llaves ({ PAGE * 2 }), y todavía no se permiten cálculos con un N genérico dentro de un tipo.
  • Una const fn puede ejecutarse al compilar en contextos const (const, static, longitudes de arrays). Usa while en lugar de for, y llama solo a otras const fn.
  • PhantomData<T> es un campo de tamaño cero que liga un struct a un tipo que no guarda. Úsalo para IDs con tipo, unidades de medida y typestate.
  • impl Matrix<2, 2> e impl<const N: usize> Matrix<N, N> dan métodos solo a algunos tamaños. Las constantes asociadas como U::IN_METERS dan a cada tipo su propio valor calculado al compilar.
  • Estas herramientas cuestan tiempo de compilación y legibilidad. Úsalas cuando eviten un bug real.

Comprueba lo aprendido

1. ¿Por qué no puedes pasar un [u32; 4] a una función que recibe [u32; 3]?

2. ¿Qué añade PhantomData<T> a un struct en tiempo de ejecución?

3. ¿Cuál de estas cosas NO puedes usar dentro de una const fn en Rust estable?

4. ¿Qué significa impl<const N: usize> Matrix<N, N>?

Puntuación: 0 / 4