Funciones asociadas y constructores: Self y new
Funciones asociadas frente a métodos, el alias Self, la convención new y por qué Rust deliberadamente no tiene constructores especiales: construir es solo una función que devuelve Self.
Rust no tiene una palabra clave constructor ni un new mágico como C++ o Java. En su lugar, construir un valor es simplemente llamar a una función que devuelve Self. Esa aparente ausencia no es una carencia: es una decisión de diseño que te regala constructores múltiples, con nombre y falibles, y elimina toda una clase de objetos a medio inicializar.
- Distinguir funciones asociadas de métodos.
- Usar
Selfcomo alias del tipo dentro deimpl. - Aplicar la convención
newpara el constructor idiomático. - Entender por qué Rust no necesita constructores especiales.
Funciones asociadas vs métodos
Dentro de un impl, una función sin receptor (self) es una función asociada: pertenece al tipo, no a una instancia. Se llama con Tipo::funcion():
struct Circulo {
radio: f64,
}
impl Circulo {
fn unitario() -> Circulo { // función asociada (sin self)
Circulo { radio: 1.0 }
}
fn area(&self) -> f64 { // método (con self)
std::f64::consts::PI * self.radio * self.radio
}
}
let c = Circulo::unitario(); // se llama sobre el TIPO
println!("{}", c.area()); // se llama sobre la INSTANCIA
La única diferencia es el primer parámetro: con self, método; sin self, función asociada. Ambas conviven en el mismo bloque impl.
Self: el alias del tipo
Dentro de un impl, Self (con mayúscula) es un alias del tipo que estás implementando. Es idiomático usarlo en lugar de repetir el nombre:
impl Circulo {
fn unitario() -> Self { // Self es Circulo aquí
Self { radio: 1.0 }
}
}
Usar Self no es solo brevedad: si renombras el struct, el impl sigue correcto sin tocar nada, y en código genérico Self es la única forma de referirte “al tipo actual”.
La convención new
Rust no impone ningún nombre para el constructor, pero la comunidad ha convergido en new para “el constructor por defecto, sin sorpresas”:
impl Circulo {
fn new(radio: f64) -> Self {
Self { radio }
}
}
let c = Circulo::new(2.5);
Cuando la construcción puede fallar, se devuelve un Result; cuando hay varias formas legítimas de construir, se usan nombres descriptivos:
impl Circulo {
fn desde_diametro(d: f64) -> Self {
Self { radio: d / 2.0 }
}
fn validado(radio: f64) -> Result<Self, String> {
if radio > 0.0 {
Ok(Self { radio })
} else {
Err(String::from("el radio debe ser positivo"))
}
}
}
Circulo::new(...) no invoca nada oculto: es una función asociada corriente, exactamente igual que unitario o desde_diametro. Podrías llamarla crear o construir y funcionaría idéntico. Se usa new solo porque todo el ecosistema lo espera ahí.
Por qué Rust no tiene constructores especiales
En C++ o Java, un constructor es un método privilegiado con reglas propias: mismo nombre que la clase, sin tipo de retorno, invocado por new. Rust rechaza ese diseño y trata la construcción como una función normal que devuelve Self. Eso desbloquea cosas que un constructor clásico no puede:
// varios "constructores" con nombre, imposible con la firma única de C++
let a = Circulo::new(2.0);
let b = Circulo::desde_diametro(4.0);
let c = Circulo::validado(-1.0); // devuelve Err, no explota
La decisión de Rust de no tener constructores especiales resuelve varios problemas de golpe. Primero, constructores múltiples y con nombre: new, desde_diametro, con_capacidad… cada intención tiene su función legible, sin la sobrecarga ambigua de firmas de C++. Segundo, construcción falible sin excepciones: si crear el valor puede fallar, devuelves Result<Self, E> y el fallo forma parte del tipo —quien construye tiene que gestionarlo—, en vez de lanzar una excepción desde un constructor. Tercero, y más profundo: no existen objetos a medio construir. En lenguajes con constructores, un this existe antes de terminar de inicializarse, y una excepción a mitad deja un objeto zombi; en Rust, un valor no es hasta que todos sus campos tienen un valor válido, en una sola expresión. Y si haces los campos privados (nivel 5.5) y solo expones constructores que validan, conviertes esos constructores en los únicos guardianes de los invariantes del tipo: es imposible fabricar un Circulo con radio negativo si el único camino es validado. Esto es “hacer que los estados inválidos sean irrepresentables” llevado a la construcción: el tipo, no la disciplina del programador, garantiza la corrección.
- Define
struct Temperatura { celsius: f64 }con una función asociadanew(celsius: f64) -> Self. - Añade
desde_fahrenheit(f: f64) -> Selfque convierta antes de construir. - Usa
Selfen todos los retornos, en vez del nombre del tipo. - Escribe
validada(celsius) -> Result<Self, String>que rechace temperaturas por debajo del cero absoluto (-273.15). - Explica en un comentario por qué tener
newydesde_fahrenheitsería imposible con un constructor único al estilo C++.