Eine Associated Function ist eine Funktion in einem impl-Block, die keinen self-Receiver hat. Sie gehört zum Typ, nicht zu einer Instanz — du rufst sie mit ::-Syntax statt mit .-Notation auf: String::new(), Vec::with_capacity(100), Konto::neu(). Klassische Anwendung: Konstruktor-Funktionen, weil Rust keine speziellen Konstruktor-Syntax wie in Java oder C++ hat — jeder fn neu(...) -> Self ist einfach eine Associated Function. Dieser Artikel zeigt die Syntax, die Konventionen für Namen (new, from, with_, default) und die typischen Patterns.
Definition
struct Konto {
saldo_cent: i64,
}
impl Konto {
// Associated function — kein self
fn neu() -> Self {
Konto { saldo_cent: 0 }
}
// Method — &self
fn saldo(&self) -> i64 {
self.saldo_cent
}
}
fn main() {
let k = Konto::neu(); // ::-Syntax, weil kein self
let s = k.saldo(); // .-Syntax, weil self vorhanden
assert_eq!(s, 0);
}Der Unterschied zwischen Konto::neu() und k.saldo() ist nicht semantisch tief, sondern folgt rein aus der Signatur. Eine Funktion mit self-Parameter (in irgendeiner Form: self, &self, &mut self) ist eine Method und wird per .-Notation an einer Instanz aufgerufen. Eine Funktion ohne self-Parameter ist eine Associated Function und wird per ::-Notation am Typ aufgerufen.
Diese Aufteilung ist syntaktisch sauber: Konto::neu() macht klar, dass hier keine Instanz nötig ist (logisch — sie soll ja erst erzeugt werden). k.saldo() macht klar, dass k die Instanz ist, an der die Methode arbeitet. In anderen Sprachen (Java, C++) sind Konstruktoren syntaktisch speziell (new Konto(), Konto()); Rust spart sich diese Sonderbehandlung und nutzt einfach den allgemeinen Mechanismus der Associated Function.
Konstruktor-Pattern: new
Rust hat kein Schlüsselwort für Konstruktoren. Stattdessen ist die Konvention: eine Associated Function namens new, die eine neue Instanz erzeugt:
struct Vec3 { x: f64, y: f64, z: f64 }
impl Vec3 {
pub fn new(x: f64, y: f64, z: f64) -> Self {
Vec3 { x, y, z }
}
}
fn main() {
let v = Vec3::new(1.0, 2.0, 3.0);
}new ist eine reine Konvention, kein reserviertes Schlüsselwort. Du könntest die Funktion auch erzeuge, make oder create nennen — der Compiler interessiert sich nicht für den Namen. Aber: Konsumenten deiner Library erwarten new, weil es überall in der Stdlib und in idiomatischem Rust-Code so heißt. Vec::new(), String::new(), HashMap::new(), BTreeMap::new() — wer das Pattern kennt, sucht bei deinem Struct als erstes nach MeinStruct::new().
In dieser Doku verwenden wir teilweise neu als deutsche Alternative für Lehrbeispiele. Im produktiven Code solltest du der englischsprachigen Konvention folgen — das macht deine API für die internationale Rust-Community sofort lesbar.
Mehrere Konstruktoren
Da Rust kein Overloading hat, brauchst du mehrere Funktionen mit verschiedenen Namen:
# struct Vec3 { x: f64, y: f64, z: f64 }
impl Vec3 {
pub fn new(x: f64, y: f64, z: f64) -> Self {
Vec3 { x, y, z }
}
pub fn null() -> Self {
Vec3 { x: 0.0, y: 0.0, z: 0.0 }
}
pub fn von_array(a: [f64; 3]) -> Self {
Vec3 { x: a[0], y: a[1], z: a[2] }
}
}Rust hat kein Function-Overloading — du kannst nicht zwei Funktionen new(x, y, z) und new(arr) parallel definieren, die sich nur in der Signatur unterscheiden. Stattdessen bekommen die verschiedenen Konstruktoren eindeutige, beschreibende Namen: new für die Standard-Form, null für den Null-Vektor, von_array für die Konvertierung aus einem Array.
Diese Variante ist verbose, hat aber Vorteile: aus dem Aufrufer-Code geht direkt hervor, welche Konstruktor-Variante gewählt wurde. Vec3::null() ist klarer als Vec3::new(0.0, 0.0, 0.0) und auch klarer als ein hypothetisches überladenes Vec3::new(). Konventionelle Namen wie new, default, with_capacity, from, try_from, with_X helfen, dass deine API für Rust-Entwickler intuitiv ist.
Naming-Konventionen
| Name | Bedeutung | Beispiel |
|---|---|---|
new | Standard-Konstruktor | Vec::new() |
default | Default-Wert (oft via Default-Trait) | Vec::default() |
with_capacity(n) | Konstruktor mit Vor-Allokation | Vec::with_capacity(100) |
with_X | Konstruktor mit spezieller Konfiguration | String::with_capacity(n) |
from(value) | Konvertierung von einem anderen Typ | String::from("Hi") |
try_from(value) | Fallible Konvertierung | i32::try_from(100u64) |
parse | aus String konstruieren | i32::from_str("42") |
empty, null, zero | „leere" oder „null"-Variante | Vec3::null() |
Diese Namens-Konventionen sind nicht nur Stil-Empfehlungen — sie sind die Grundlage für idiomatic Rust. Wer eine API entwirft und sich daran hält, schreibt Code, der sich für Rust-Programmierer sofort vertraut anfühlt. Wer eigene Namen wählt (z. B. MyStruct::create() statt new(), oder convert() statt from()), zwingt Konsumenten zu permanenter Dokumentations-Lektüre.
Besonders wichtig ist die from/try_from-Konvention: sie verbindet sich mit den Stdlib-Traits From und TryFrom, und das into()-Methode ergibt sich automatisch über die Blanket-Impls. Wenn du From<X> for Y implementierst, bekommen alle Konsumenten gratis x.into() und können in generischen impl Into<Y>-Parametern arbeiten.
Associated Functions ohne Self-Return
Nicht jede Associated Function muss eine Instanz erzeugen. Sie kann auch Hilfs-Funktionen sein, die zum Typ thematisch gehören:
struct Geld { eur: u32, cent: u8 }
impl Geld {
pub fn neu(eur: u32, cent: u8) -> Self {
Geld { eur, cent: cent.min(99) }
}
// Type-Level-Utility — keine Instanz, keine Rückgabe von Self
pub fn parse_cents(s: &str) -> Result<u64, String> {
let teile: Vec<&str> = s.split(',').collect();
match teile.as_slice() {
[eur, cent] => {
let e: u64 = eur.parse().map_err(|_| String::from("kein Euro"))?;
let c: u64 = cent.parse().map_err(|_| String::from("kein Cent"))?;
Ok(e * 100 + c)
}
_ => Err(String::from("Format: EUR,CC")),
}
}
}
fn main() {
let cents = Geld::parse_cents("19,90").unwrap();
assert_eq!(cents, 1990);
}parse_cents ist eine Type-Level-Hilfsfunktion: sie gehört thematisch zum Geld-Typ (sie versteht das Geld-Format), baut aber keine Geld-Instanz, sondern liefert nur den Cents-Wert. Solche Funktionen sind ideale Associated Functions — der Aufrufer ruft Geld::parse_cents("..."), und die Namensgebung macht klar, in welchem semantischen Kontext die Funktion arbeitet.
Die Alternative wäre eine freie Funktion irgendwo im Modul (fn parse_geld_cents(...)). Die Associated-Function-Variante ist organisatorisch besser: alle Geld-bezogenen Funktionen liegen am Typ und sind über die Geld::-Namensraum auffindbar. Bei IDE-Auto-Completion (etwa Geld:: + Tab) bekommst du alle relevanten Funktionen direkt vorgeschlagen.
Associated Constants
Associated Constants sind verwandt: Werte, die zum Typ gehören:
struct Kreis;
impl Kreis {
pub const PI: f64 = 3.141592653589793;
pub const KONST_EULER: f64 = 0.5772156649;
}
fn main() {
let umfang = 2.0 * Kreis::PI * 5.0;
println!("{umfang}");
}Associated Constants sind das ruhende Geschwister der Associated Functions: Werte (statt Funktionen), die zum Typ gehören. Aufruf ebenfalls per ::-Syntax — Kreis::PI. Sie sind sehr nützlich für Domain-Konstanten (wie mathematische Konstanten), Default-Werte (Vec3::NULL), oder magische Zahlen, die zum Typ logisch dazugehören.
Im Vergleich zu globalen const-Items in einem Modul haben Associated Constants den Vorteil der Namensraum-Sortierung: alles Kreis-Bezogene findet sich unter Kreis::*. Aus IDE-Perspektive ist das auch besser auffindbar.
Trait-basierter Konstruktor: Default
Der Default-Trait standardisiert „Default-Konstruktor":
#[derive(Default)]
struct Konto {
saldo_cent: i64,
// Alle Felder müssen Default haben — i64::default() = 0
}
fn main() {
let k = Konto::default(); // saldo_cent = 0
assert_eq!(k.saldo_cent, 0);
}#[derive(Default)] generiert Konto::default(). Voraussetzung: alle Felder implementieren Default. Für eigene Default-Werte: manuelle Implementierung:
# struct Konto { saldo_cent: i64 }
impl Default for Konto {
fn default() -> Self {
Konto { saldo_cent: 0 }
}
}#[derive(Default)] ist der schnellste Weg zu einem Default-Konstruktor — der Compiler generiert die Default-Implementation automatisch, sofern alle Felder selbst Default implementieren. Bei Standard-Typen wie i64, String, Vec<T>, HashMap ist das automatisch erfüllt.
Wenn du nicht-triviale Defaults brauchst (z. B. eine Konfiguration mit Default-URL), implementierst du Default manuell. Die Methode wird dann genauso aufgerufen — Konfig::default() —, aber sie kann beliebige Initialisierungs-Logik enthalten. Der Default::default()-Aufruf taucht überall auf: als Initial-Wert in Builder-Patterns, in generischem Code (T::default()), bei Option::unwrap_or_default() und Result::unwrap_or_default().
From / TryFrom — Konvertierungs-Konstruktoren
From<T> und TryFrom<T> sind Traits für Konvertierungen. Wer sie implementiert, bekommt automatisch:
String::from("Hi")— Konvertierung von&strzuString."Hi".into()— generische Konvertierung überInto<String>.
struct UserId(u64);
impl From<u64> for UserId {
fn from(value: u64) -> Self {
UserId(value)
}
}
fn main() {
let id1 = UserId::from(42);
let id2: UserId = 42.into();
// Beide gleichwertig.
}Die From/Into-Mechanik ist ein zentrales Idiom in Rust. Du implementierst From<X> for Y, und automatisch hast du Into<Y> for X — über eine Blanket-Impl in der Stdlib. Dadurch kann jeder Aufrufer wählen, ob er Y::from(x) (explizit am Ziel-Typ) oder x.into() (im Kontext) verwendet.
Die into()-Variante ist besonders mächtig in generischen Funktions-Parametern: fn foo(x: impl Into<UserId>) akzeptiert sowohl u64-Werte (Konvertierung über From<u64>) als auch direkte UserId-Instanzen. Damit baust du flexible APIs, ohne separate Overloads schreiben zu müssen. Ein einzelner From-Impl gibt dir dieses ganze Flexibilitäts-Spektrum kostenlos.
Aufruf via Type-Alias
Associated Functions funktionieren auch mit Type-Aliasen:
type UserMap = std::collections::HashMap<u64, String>;
fn main() {
let map: UserMap = UserMap::new(); // via Alias
}Sehr nützlich, wenn der Original-Typ generisch ist und der Alias spezialisiert.
Praxis: Associated Functions im echten Code
Mehrere Konstruktor-Varianten
pub struct Server {
host: String,
port: u16,
tls: bool,
}
impl Server {
pub fn new(host: impl Into<String>, port: u16) -> Self {
Server { host: host.into(), port, tls: false }
}
pub fn neu_mit_tls(host: impl Into<String>, port: u16) -> Self {
Server { host: host.into(), port, tls: true }
}
pub fn localhost(port: u16) -> Self {
Server::new("localhost", port)
}
}
fn main() {
let a = Server::new("example.com", 80);
let b = Server::neu_mit_tls("example.com", 443);
let c = Server::localhost(8080);
let _ = (a.host, b.host, c.host);
}Wenn ein Typ verschiedene Konstruktions-Modi anbietet, sollten sie klar benannt sein. Server::new(host, port) für die generische Form, Server::neu_mit_tls(host, port) mit explizitem TLS-Hinweis, Server::localhost(port) als Convenience für die häufige Test-/Dev-Konfiguration. Jeder Konstruktor sagt im Namen, was er macht.
Diese Variante ist ergonomischer als ein generisches new(host, port, options) mit Options-Struct: der Aufrufer muss keine Default-Werte für irrelevante Parameter setzen, und die API ist selbsterklärend. Bei sehr vielen Konstruktoren mit komplexen Konfigurationen kippt das Verhältnis irgendwann zugunsten eines Builder-Patterns — das eigene Kapitel dazu folgt.
Konstanten + Konstruktor zusammen
#[derive(Clone, Copy)]
pub struct Vec2 { pub x: f64, pub y: f64 }
impl Vec2 {
pub const NULL: Vec2 = Vec2 { x: 0.0, y: 0.0 };
pub const EINHEIT_X: Vec2 = Vec2 { x: 1.0, y: 0.0 };
pub const EINHEIT_Y: Vec2 = Vec2 { x: 0.0, y: 1.0 };
pub fn new(x: f64, y: f64) -> Self { Vec2 { x, y } }
pub fn rotation(winkel: f64) -> Self {
Vec2 { x: winkel.cos(), y: winkel.sin() }
}
}
fn main() {
let o = Vec2::NULL;
let x = Vec2::EINHEIT_X;
let r = Vec2::rotation(std::f64::consts::PI / 4.0);
let _ = (o.x, x.x, r.x);
}Konstanten und Konstruktoren passen wunderbar zusammen in einem impl-Block. Die häufig genutzten Standardwerte (NULL, EINHEIT_X, EINHEIT_Y) sind als const direkt am Typ verfügbar — keine Allokation, kein Funktionsaufruf, nur ein Typ-Member-Access. Für berechnete Werte gibt es die Konstruktor-Variante (new, rotation).
Der Aufrufer kann je nach Situation wählen: Vec2::NULL ist klarer und schneller als Vec2::new(0.0, 0.0). Vec2::rotation(winkel) hingegen kann nicht als Konstante existieren, weil der Wert vom Argument abhängt.
Cache-Builder
use std::collections::HashMap;
pub struct LruCache {
map: HashMap<String, String>,
kapazitaet: usize,
}
impl LruCache {
pub fn new() -> Self {
LruCache::with_capacity(100)
}
pub fn with_capacity(kapazitaet: usize) -> Self {
LruCache {
map: HashMap::with_capacity(kapazitaet),
kapazitaet,
}
}
}Das new()/with_capacity()-Doppelpattern ist eines der häufigsten in der Stdlib — Vec, HashMap, String, VecDeque, HashSet alle bieten es. new() ist der minimale Default, with_capacity(n) ist die optimierte Variante für bekannte Größen.
Im Beispiel wird new() als Convenience-Wrapper über with_capacity(100) implementiert — eine schöne Form, Code-Duplikation zu vermeiden. Wenn du an dem 100-Default später etwas ändern willst, musst du es nur einmal anpassen. Diese Komposition kleiner Konstruktoren ist typisches gutes API-Design.
Conversion-Konstruktoren
pub struct Tag(String);
impl Tag {
pub fn new(name: impl Into<String>) -> Self {
Tag(name.into())
}
}
impl From<&str> for Tag {
fn from(s: &str) -> Self {
Tag(s.to_string())
}
}
impl From<String> for Tag {
fn from(s: String) -> Self {
Tag(s)
}
}
fn main() {
let t1 = Tag::from("rust");
let t2 = Tag::from(String::from("tutorial"));
let t3: Tag = "ownership".into();
let _ = (t1.0, t2.0, t3.0);
}Wenn dein Typ aus mehreren Quell-Typen konstruierbar sein soll, implementierst du From<X> für jeden davon. Hier wird From<&str> und From<String> für Tag implementiert — beide allokieren intern, aber der Aufrufer kann je nach Datenquelle die passende Form nutzen, ohne Konvertierung im Voraus.
Die .into()-Variante macht das in generischem Code besonders bequem: eine Funktion mit tag: impl Into<Tag>-Parameter akzeptiert &str, String, Tag direkt — alle drei werden automatisch konvertiert. Das macht Library-APIs sehr nutzerfreundlich, ohne dass du selbst Overloads schreiben musst.
TryFrom für validierende Konvertierung
pub struct Port(u16);
impl TryFrom<u16> for Port {
type Error = String;
fn try_from(value: u16) -> Result<Self, Self::Error> {
if value < 1024 {
Err(String::from("Port < 1024 ist privilegiert"))
} else {
Ok(Port(value))
}
}
}
fn main() {
let p1 = Port::try_from(8080).unwrap();
let p2 = Port::try_from(80);
assert!(p2.is_err());
let _ = p1.0;
}Wenn die Konvertierung scheitern kann (etwa weil die Eingabe Validierungs-Bedingungen verletzt), nutzt du TryFrom statt From. Die Methode try_from gibt Result<Self, Error> zurück — Erfolg oder ein Fehler-Wert.
Im Beispiel validiert Port::try_from den Wertebereich: Ports unter 1024 sind auf Unix-Systemen privilegiert und brauchen Root-Rechte, also lehnen wir sie ab. Damit hast du eine Typ-Garantie: jede Port-Instanz, die existiert, ist garantiert ein nicht-privilegierter Port. Andere Funktionen können sich darauf verlassen, ohne selbst zu prüfen — das ist wieder das „Make Invalid States Unrepresentable"-Prinzip.
Static-Singleton-Pattern
use std::sync::OnceLock;
pub struct Logger {
level: String,
}
impl Logger {
pub fn instanz() -> &'static Logger {
static LOGGER: OnceLock<Logger> = OnceLock::new();
LOGGER.get_or_init(|| Logger { level: "INFO".into() })
}
pub fn log(&self, msg: &str) {
println!("[{}] {msg}", self.level);
}
}
fn main() {
Logger::instanz().log("Start");
Logger::instanz().log("Verarbeite");
}instanz() als Associated Function plus OnceLock für lazy-initialisierten Singleton. Der Rückgabetyp &'static Logger ist eine Referenz, die für die gesamte Programm-Laufzeit gültig bleibt — die Markierung 'static ist eine Lifetime und wird in Kapitel 16 ausführlich behandelt.
Builder-Konstruktor
pub struct Anfrage {
url: String,
timeout_ms: u32,
}
impl Anfrage {
pub fn an(url: impl Into<String>) -> Self {
Anfrage { url: url.into(), timeout_ms: 5000 }
}
pub fn timeout(mut self, ms: u32) -> Self {
self.timeout_ms = ms;
self
}
}
fn main() {
let req = Anfrage::an("https://example.com").timeout(10_000);
let _ = req.url;
}an() als sprechender Konstruktor — semantisch klarer als new(...).
Helper-Funktion am Typ
pub struct Slug;
impl Slug {
pub fn aus(text: &str) -> String {
text.chars()
.filter(|c| c.is_alphanumeric() || c.is_whitespace())
.map(|c| if c.is_whitespace() { '-' } else { c.to_ascii_lowercase() })
.collect()
}
}
fn main() {
assert_eq!(Slug::aus("Hello World!"), "hello-world");
}Slug als Unit-Struct dient als „Namespace" für die aus-Funktion. Klassisches Pattern für Helper-Funktionen, die thematisch zu einem Konzept gehören.
Konstante mit Konstruktor-Helfer
pub struct Theme {
pub hintergrund: String,
pub text: String,
}
impl Theme {
pub const DARK: fn() -> Theme = || Theme {
hintergrund: "#1a1a1a".into(),
text: "#e0e0e0".into(),
};
pub fn hell() -> Self {
Theme {
hintergrund: "#ffffff".into(),
text: "#202020".into(),
}
}
}
fn main() {
let h = Theme::hell();
let d = (Theme::DARK)();
let _ = (h.text, d.text);
}FAQ
Was unterscheidet Method von Associated Function?
Method hat self-Receiver (&self, &mut self, self) — Aufruf mit .-Syntax. Associated Function hat keinen self — Aufruf mit ::-Syntax. Beide leben im gleichen impl-Block.
Gibt es Konstruktoren wie in Java?
Nein, kein Schlüsselwort. Stattdessen ist die Konvention: eine Associated Function namens new, die Self zurückgibt. Du darfst sie auch anders nennen — der Compiler erzwingt nichts.
Warum new statt eines speziellen Schlüsselworts?
Weil Konstruktoren in Rust gewöhnliche Funktionen sind — sie können fehlschlagen (Rückgabe Result), mehrere Varianten haben, generisch sein, andere Konstruktoren aufrufen. Spezielle Konstruktor-Syntax in OOP-Sprachen schränkt diese Flexibilität ein.
Self oder konkreten Typ?
Innerhalb impl T { ... } sind Self und T austauschbar. Self ist idiomatischer — sieht in generischen Implementierungen besser aus, hält den Code bei Typ-Umbenennungen stabil. fn new() -> Self ist Standard.
Wann new, wann default?
new() für Konstruktion mit Parameter-Argumenten oder spezifischer Logik. default() für „leeren Standard"-Wert, der per Default-Trait standardisiert ist. Viele Typen haben beide: Vec::new() (Default-Konstruktor) und Vec::default() (über Default-Trait, gleichbedeutend).
From impliziert Into.
Wenn du impl From<u64> for UserId schreibst, bekommst du Into<UserId> für u64 kostenlos durch eine Blanket-Impl in der Stdlib. Damit ist 42.into() (mit let x: UserId = ...) gleichwertig zu UserId::from(42).
Associated Constants sind const-Items im impl.
impl T { pub const VAL: i32 = 42; } definiert eine Konstante am Typ. Aufruf: T::VAL. Sehr nützlich für Domain-Konstanten wie Vec3::NULL, Color::RED. Können auch via Trait T: SomeTrait aus dem Trait kommen.
Aufruf mit Fully-Qualified-Syntax.
T::method(...) ruft eine Associated Function. Bei Mehrdeutigkeit (z. B. mehrere Traits mit gleichem Namen): <T as Trait>::method(...). Diese „Fully Qualified Syntax" ist selten nötig, hilft aber bei Ambiguitäten.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Associated Functions
- Rust Reference – Associated Items
- std::default::Default
- std::convert::From
- Rust API Guidelines – C-CTOR