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
3de[i32; 3]. const fnpermite que el compilador ejecute tu función mientras construye el programa, así que el resultado ya está listo antes de que el programa arranque.PhantomDatapermite 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 incorrectEl 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 165 artículos cuestan 30Lee <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]]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][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 16El 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, …),boolychar. Si pruebasconst NAME: &'static str, obtienes “&'static stris 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”).Na secas está bien, yN * 2también, pero dentro del cuerpo de una función, dondeNes solo un valor. La función del lenguaje que permitiríaN * 2en los tipos (generic_const_exprs) sigue estando solo en nightly. - Comprobar un valor. No puedes escribir una restricción del estilo “
Ntiene que ser mayor que 0”. Lo más parecido es un bloqueconstdentro de una función:const { assert!(N > 0, "need at least one slot") };. Falla cuando compilas el programa con unNincorrecto (puede quecargo checkno 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 casillassquare(12) en tiempo de ejecución es 144Una 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 60el 25 de diciembre es el día 359Fí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:
letylet mut, aritmética, comparaciones,if,match,loopywhile- arrays, tuplas, structs y enums, e índices como
table[month] - llamadas a otras
const fn(muchas de la biblioteca estándar son const, comou32::powoslice.len()) - referencias
&muty cálculos con números de punto flotante (estables desde Rust 1.83 y 1.82) panic!yassert!, 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 Amaracancelando el pedido #7un Id ocupa 4 bytesLos 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 incorrectEl 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”.
Unidades de medida: los metros no son pies
Sección titulada «Unidades de medida: los metros no son pies»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 mismaimpl<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 mtotal: 954.8 mEscribe 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 estadoimpl<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 abreSala de servidores: se cierraSala de servidores: se cierra con llaveSala de servidores: cerrada con llave toda la nocheSala de servidores: se quita la llaveSala de servidores: se abreSala de servidores: abierta otra vezAhora 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 Nimpl<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 2x2impl 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 -14grande: traza 15big.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 m29032.00 ft = 8848.95 m29032.00 ft = 5.50 miU::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.
Cuándo no usar estas herramientas
Sección titulada «Cuándo no usar estas herramientas»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 unVec. - 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 unBox(mira Smart pointers) o usa unVec. - Cada tamaño o tipo nuevo es una copia nueva del código. Usar un genérico con 20 valores distintos de
Ngenera 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.
Errores comunes
Sección titulada «Errores comunes»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 variantsRust 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` trait15 | show(id); | -- value moved here16 | 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 parametersEl 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> {}Práctica
Sección titulada «Práctica»1. Rellena las puntuaciones
Sección titulada «1. Rellena las puntuaciones»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]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].
2. Tiempos de espera calculados al compilar
Sección titulada «2. Tiempos de espera calculados al compilar»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 segundosesperas entre reintentos: [60, 300, 900]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.
3. Dinero que no se puede mezclar
Sección titulada «3. Dinero que no se puede mezclar»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 EURentrada: 49.99 USDPrueba 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.
Resumen
Sección titulada «Resumen»- Los const generics (
const N: usize) hacen que un número forme parte de un tipo. Los arrays de longitudes distintas son tipos distintos, eimpl<T, const N: usize>escribe métodos para todos los tamaños. - En Rust estable, los parámetros const pueden ser enteros,
boolochar. Las expresiones usadas como argumento necesitan llaves ({ PAGE * 2 }), y todavía no se permiten cálculos con unNgenérico dentro de un tipo. - Una
const fnpuede ejecutarse al compilar en contextos const (const,static, longitudes de arrays). Usawhileen lugar defor, y llama solo a otrasconst 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>eimpl<const N: usize> Matrix<N, N>dan métodos solo a algunos tamaños. Las constantes asociadas comoU::IN_METERSdan 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 que aprendiste
Sección titulada «Comprueba lo que aprendiste»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
Para leer más
Sección titulada «Para leer más»- Const generics — The Rust Reference (en inglés): las reglas exactas de los parámetros const, qué tipos se permiten y cuándo necesitas llaves.
- Const functions — The Rust Reference (en inglés): qué son los contextos const y qué puede hacer una
const fn. PhantomData— documentación de la biblioteca estándar (en inglés): la explicación oficial, con ejemplos de parámetros de tipo sin usar.- Phantom type parameters — Rust by Example (en inglés): ejemplos cortos que puedes ejecutar, incluido un caso práctico de unidades de medida.
- Const generics MVP hits beta — Rust Blog (en inglés): el anuncio que explica por qué se añadieron los const generics y qué puede hacer su primera versión.