wandres.dev
STRUCTS Y MÉTODOS · modelar datos

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.

⏱ 16 min

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.

🎯 Al terminar esta lección sabrás
  • Distinguir funciones asociadas de métodos.
  • Usar Self como alias del tipo dentro de impl.
  • Aplicar la convención new para 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"))
        }
    }
}
ℹ️
new no es especial

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
Sin constructor privilegiado: construir es solo devolver Self

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.

⚔️ Constructores con nombre e invariantes
  1. Define struct Temperatura { celsius: f64 } con una función asociada new(celsius: f64) -> Self.
  2. Añade desde_fahrenheit(f: f64) -> Self que convierta antes de construir.
  3. Usa Self en todos los retornos, en vez del nombre del tipo.
  4. Escribe validada(celsius) -> Result<Self, String> que rechace temperaturas por debajo del cero absoluto (-273.15).
  5. Explica en un comentario por qué tener new y desde_fahrenheit sería imposible con un constructor único al estilo C++.