`Unpin`: el permiso para moverse, y por qué casi todo lo tiene
`Unpin` es un auto-trait que marca los tipos que pueden moverse libremente aunque estén pinchados: la inmensa mayoría. Su nombre engaña: no significa no se puede pinchar, sino el pinchado no le afecta. Para un `T: Unpin`, un `Pin<&mut T>` no restringe nada y devuelve un `&mut T` gratis. Solo los tipos genuinamente auto-referenciales —los futuros generados, o quien use `PhantomPinned`— son `!Unpin` y quedan de verdad atados por `Pin`.
Si Pin fuera una restricción que cae sobre todos los valores, sería insoportable: no podrías mover un String, ni reasignar un i32, sin pelear con envoltorios. Pero tu experiencia diaria con Rust dice lo contrario: el noventa y nueve por ciento del código nunca se topa con Pin. La pieza que reconcilia ambas cosas es Unpin, un auto-trait con el nombre más contraintuitivo de la biblioteca estándar. Unpin no significa este tipo no se puede pinchar. Significa lo opuesto: a este tipo el pinchado no le afecta; muévelo cuando quieras, aunque esté dentro de un Pin. Y como casi todos los tipos lo cumplen automáticamente, Pin se vuelve transparente para casi todo, y solo muerde donde de verdad hace falta: los poquísimos valores que guardan un puntero a sí mismos.
- Definir
Unpincomo el auto-trait de los tipos a los que mover no perjudica. - Deshacer la trampa del nombre:
Unpines permiso para moverse, no incapacidad de pinchar. - Ver que sobre un
T: Unpin, unPin<&mut T>no restringe nada y cede un&mut Tseguro. - Identificar quién es
!Unpin: los futuros auto-referenciales y los tipos conPhantomPinned.
Un auto-trait que reparte permisos
Unpin es un auto-trait, de la misma familia que Send y Sync: el compilador lo implementa solo, automáticamente, para todo tipo cuyas partes sean todas Unpin. No lo escribes; lo deduce la estructura. Y como los bloques básicos —i32, bool, char, String, Vec<T>, Box<T>, &T— son todos Unpin, cualquier struct o enum que compongas con ellos hereda Unpin gratis. En la práctica, casi todos los tipos de Rust son Unpin.
// Unpin se deriva por composicion, igual que Send y Sync: si todos los campos
// son Unpin, el tipo entero es Unpin, sin que escribas absolutamente nada.
struct Punto { x: i32, y: String } // i32 y String son Unpin => Punto es Unpin
La semántica es la clave, y hay que decirla con cuidado. T: Unpin significa: pinchar un T no aporta ninguna garantía útil, porque mover un T nunca lo corrompe. Es la afirmación de que T no tiene punteros internos que un move fuera a invalidar. Un String puede moverse un millón de veces sin daño; sus tres palabras del stack significan lo mismo en cualquier dirección. Por eso String es Unpin: la promesa de inmovilidad que ofrece Pin no le sirve de nada, porque no la necesita.
Léelo despacio una vez y no volverás a equivocarte. Unpin no es no pinchable. Unpin es inmune al pinchado, libre para salir de un Pin, seguro de mover aunque esté pinchado. La confusión viene de leer el prefijo un- como negación de pin, cuando en realidad describe la capacidad de des-pincharse sin consecuencias. Un tipo Unpin puede entrar en un Pin y volver a salir mediante Pin::get_mut, tan campante. El tipo interesante es el contrario, !Unpin: ese es el que, una vez pinchado, ya no puede salir con seguridad.
Pin sobre un Unpin no restringe nada
Aquí se cierra el círculo con la lección anterior. Dijimos que Pin<&mut T> se niega a entregar un &mut T. Matiz: se niega salvo que T sea Unpin. Cuando el tipo es Unpin, esa negación desaparece, porque mover el valor no hace daño y no hay nada que proteger:
use std::pin::Pin;
let mut n = 42_i32; // i32 es Unpin
let mut p: Pin<&mut i32> = Pin::new(&mut n); // Pin::new: seguro, sin unsafe, porque es Unpin
let r: &mut i32 = p.as_mut().get_mut(); // get_mut: seguro, porque i32 es Unpin
*r += 1; // puedes mutarlo y moverlo a placer
Dos consecuencias visibles. Primera: Pin::new es un constructor seguro para tipos Unpin —no necesitas unsafe ni prometer nada, porque no hay invariante que romper—. Segunda: Pin::get_mut te devuelve un &mut T seguro si T: Unpin. Es decir, sobre un tipo Unpin, Pin es un envoltorio transparente que no te quita ninguna capacidad. Por eso puedes escribir toneladas de código sin enterarte de que Pin existe: todo lo que tocas es Unpin, y Pin sobre Unpin es un no-operar.
El requisito no es una convención: está en la propia firma de la biblioteca estándar, como una acotación Unpin que restringe cuándo esos métodos seguros existen.
// Esquema de las firmas reales (simplificado):
impl<P> Pin<P> where P: Deref, P::Target: Unpin {
pub fn new(pointer: P) -> Pin<P> { /* seguro: el destino es Unpin */ }
}
impl<'a, T: ?Sized> Pin<&'a mut T> {
pub fn get_mut(self) -> &'a mut T where T: Unpin { /* seguro: mover no daña */ }
}
Quién es !Unpin, y cómo optar por serlo
Solo un puñado de tipos no son Unpin, y son exactamente los que guardan punteros a sí mismos. El caso estrella: los Future que el compilador genera a partir de un async fn cuando alguna referencia cruza un await (lección 2). El compilador los marca !Unpin porque sí se corromperían al moverse. Para esos, Pin recupera todos sus dientes: Pin::new deja de estar disponible (hay que usar unsafe o los ayudantes de la lección 5), y get_mut seguro desaparece.
También puedes volver !Unpin un tipo propio, aunque rara vez lo harás a mano. El marcador es PhantomPinned, un campo de tamaño cero que le dice al compilador no me des Unpin automático:
use std::marker::PhantomPinned;
struct Inmovible {
dato: String,
interno: *const u8, // pretende apuntar dentro de self.dato
_fija: PhantomPinned, // este campo hace que Inmovible sea !Unpin
}
Sin PhantomPinned, ese struct sería Unpin por composición —pese a tener un puntero crudo— y Pin no lo protegería, porque el compilador no sabe que el puntero es interno. PhantomPinned es la forma de decir este tipo tiene una razón real para no moverse; hazme cumplir la garantía de Pin.
El contraste se ve al intentar las mismas operaciones sobre un tipo de cada mundo:
use std::pin::Pin;
use std::marker::PhantomPinned;
// Mundo Unpin: todo permitido, sin unsafe
let mut n = 10_i32;
let p = Pin::new(&mut n); // compila: i32 es Unpin
let r: &mut i32 = p.get_mut(); // compila: recuperas el &mut gratis
*r += 1;
// Mundo !Unpin: las mismas llamadas dejan de estar disponibles
struct Nudo { _fija: PhantomPinned }
let mut nudo = Nudo { _fija: PhantomPinned };
// let p = Pin::new(&mut nudo); // ERROR: Nudo no es Unpin, Pin::new no aplica
// let r = p.get_mut(); // ERROR: get_mut seguro exige Unpin
// Para pinchar un Nudo hay que usar Box::pin, pin!, o new_unchecked (unsafe).
La misma línea que compila para el i32 se apaga para el Nudo. No hay dos APIs distintas: es una API cuyos métodos seguros exigen Unpin, de modo que el compilador te deja pasar cuando mover es inocuo y te frena cuando no. Unpin es, literalmente, el interruptor que decide si Pin estorba o desaparece.
flowchart TD
A[Un tipo cualquiera] --> B{Guarda un puntero a si mismo}
B -->|no, el caso comun| C[Es Unpin de forma automatica]
B -->|si, futuros o PhantomPinned| D[Es no Unpin]
C --> E[Pin es transparente con get mut y new seguros]
D --> F[Pin ata de verdad sin get mut seguro ni Pin new]
E --> G[El codigo normal nunca nota Pin]
F --> H[Solo estos pocos tipos pagan la restriccion]
style C fill:#a6e3a1,color:#11111b
style D fill:#f38ba8,color:#11111b
style G fill:#89b4fa,color:#11111b
style H fill:#f9e2af,color:#11111bPiensa en el problema de diseño que Unpin resuelve, porque es más sutil de lo que parece. Pin existe por un caso rarísimo —los valores auto-referenciales— pero tenía que colarse en la firma de Future::poll, que es una de las abstracciones más centrales y ubicuas de todo el lenguaje moderno: cada async fn, cada ejecutor, cada .await pasa por ahí. Si Pin impusiera su restricción a todos los tipos, habría contaminado la experiencia de programar con una fricción constante para pagar el coste de un caso que casi nadie escribe a mano. La solución es invertir el modelo de coste por defecto. Unpin hace que la restricción sea opt-out y no opt-in: el defecto es puedes moverte libremente y solo los tipos que tienen una razón concreta para no moverse renuncian a ese permiso. Y como Unpin es un auto-trait derivado estructuralmente, esa partición del universo de tipos ocurre sola, sin que nadie anote nada: la biblioteca estándar entera, tus structs, tus enums, todo cae en el lado permisivo automáticamente, y solo las máquinas de estados generadas por async —que el compilador sabe auto-referenciales— caen en el lado restringido. El resultado es que la característica pensada para un caso difícil y minoritario impone cero impuesto sobre el caso común, porque el sistema de tipos separa los dos mundos gratis. Esta es la misma jugada que hace tolerables a Send y Sync: una propiedad que casi todos cumplen, deducida por composición, de modo que la abstracción cara solo se activa donde importa. Unpin no es un detalle secundario de Pin; es la pieza que hace posible poner Pin en el corazón del async sin que el resto del lenguaje se entere. Sin Unpin, Pin sería un peaje universal; con Unpin, es un peaje que solo cruzan los futuros.
Unpin es un auto-trait, como Send y Sync, que el compilador deduce por composición y que cumplen casi todos los tipos. Significa mover este tipo no lo corrompe, no no se puede pinchar; es permiso para salir de un Pin. Sobre un T: Unpin, Pin es transparente: Pin::new es seguro y Pin::get_mut devuelve un &mut T seguro, así que el código normal jamás nota Pin. Solo son !Unpin los tipos con punteros a sí mismos: los futuros auto-referenciales que genera el compilador y los tipos marcados con PhantomPinned. Como el defecto es permisivo y opt-out, Pin solo restringe a esos pocos, y por eso cabe en la firma de Future::poll sin gravar al resto.
- Explica, sin mirar, qué significa exactamente
Unpiny por qué el nombre invita a entenderlo al revés. - Enumera cinco tipos de la biblioteca estándar que sean
Unpiny justifica en una frase por qué mover cada uno es inocuo. - Escribe
let mut n = 0_i32;y anima unPin<&mut i32>conPin::new; recupera un&mut i32conget_muty explica por qué aquí no hace faltaunsafe. - Añade
PhantomPinneda un struct y razona qué cambia respecto aPin::newyget_mutsobre ese tipo. - Argumenta por qué que
Unpinsea un auto-trait con defecto permisivo es lo que permite meterPin<&mut Self>enFuture::pollsin molestar al código que no toca async.