Menu

Zero Generics: Typparameter auf Funktionen und Shapes

Wie Generics in Zero funktionieren: Typparameter an Funktionen und Shapes deklarieren, generische Funktionen aufrufen und das Typ-Alias-Muster, das aus langen Parametrisierungen saubere Namen macht.

Diese Seite enthält ausführbare Editoren — bearbeiten, ausführen und Ausgabe sofort sehen.

Warum Generics

Du stößt schnell auf Typen, die für mehr als einen Element-Typ funktionieren sollten. Ein „Paar" aus zwei Werten interessiert nicht, ob die Werte Integer, Strings oder eine selbst definierte Shape sind. Mit Generics schreibst du den Typ einmal und instanziierst ihn mit den Element-Typen, die der Aufrufer braucht.

Die Alternative – IntPair, StringPair, BytePair und so weiter zu schreiben – wird schnell öde und lässt sich nicht gut komponieren. Generics in Zero sind das Standard-Werkzeug dafür.

Generische Funktionen

Deklariere Typparameter in spitzen Klammern zwischen Funktionsname und Parameterliste:

fun makePair<T, U>(left: T, right: U) -> Pair<T, U> {
    return Pair { left: left, right: right }
}

T und U sind Platzhalter-Typen – der Aufrufer entscheidet, was sie sind.

Aufrufe müssen sie normalerweise nicht ausschreiben; der Compiler inferiert aus den Argumenttypen:

let pair = makePair(40, 2_u8)

Hier wird T als i32 inferiert (der Default für ein Integer-Literal ohne Suffix) und U als u8 (durch das _u8-Suffix). Das resultierende Binding pair hat den Typ Pair<i32, u8>.

Würde die Inferenz die falschen Typen wählen – etwa weil die Literale mehrdeutig sind – kannst du die Parameter an der Aufrufstelle pinnen:

let pair = makePair<u8, u8>(1, 2)

(Ob die Aufruf-Syntax mit spitzen Klammern exakt so ist, kann sich zwischen Zero-Versionen unterscheiden; schau in die aktuelle Doku für die genaue Schreibweise. Stabil ist das Inferenz-zuerst-Verhalten.)

Generische Shapes

Shapes nehmen Typparameter genauso entgegen:

shape Pair<T, U> {
    left: T,
    right: U,
}

Der Typ jedes Feldes darf die Parameter erwähnen. Instanzen pinnen sie fest:

let intBytes: Pair<i32, u8> = Pair { left: 40, right: 2_u8 }
let words:    Pair<String, String> = Pair { left: "hi", right: "there" }

Ein durchgespieltes Beispiel, das eine generische Shape und eine generische Funktion kombiniert – klick Run, um die Inferenz am Werk zu sehen:

Eine generische Shape-Deklaration, eine generische Funktion, eine stark typisierte Aufrufstelle. Kein IntBytePair-Boilerplate.

Typ-Aliase

Wenn derselbe parametrisierte Typ immer wieder auftaucht, gib ihm mit type einen Namen:

type BytePair = Pair<u8, u8>

Jetzt ist BytePair überall, wo du einen Typ schreiben kannst, gleichwertig mit Pair<u8, u8>:

Ein Alias ist nur ein Namens-Feature – er schafft keinen eigenständigen Typ. Eine Funktion, die ein BytePair nimmt, akzeptiert problemlos einen Wert vom Typ Pair<u8, u8> (und umgekehrt).

Generics in der Standardbibliothek

Derselbe Mechanismus treibt einen großen Teil der Standardbibliothek an. Ein paar, die du in echtem Zero-Code sehen wirst:

  • Maybe<T> – ein optionaler Wert, der entweder ein T oder nichts hält.
  • Span<T> – ein geliehener Slice über T-Werte. Span<u8> ist die kanonische Sicht auf einen Byte-Puffer.
  • ref<T> und mutref<T> – explizite Referenztypen für Fälle, in denen du Daten ohne Kopie teilen musst.

Du musst die nicht alle auf einmal lernen. Der Sinn von Generics ist, dass dieselbe Shape für jeden Element-Typ funktioniert, den du gerade hast.

Wann sich Generics lohnen (und wann nicht)

Greif zu Generics, wenn du dich dabei erwischst, dieselbe Funktion oder Shape mit unterschiedlichen Element-Typen zweimal zu schreiben. Greif zu einem konkreten Typ, wenn:

  • Die Logik der Funktion nur für einen bestimmten Typ Sinn ergibt (etwa ein Parser für Strings).
  • Du willst, dass der Typ in Fehlermeldungen auftaucht, damit Debugging einfacher wird.
  • Die Performance-Charakteristik von einer bestimmten Speichergröße abhängt.

Die Kosten von Generics sind real – größere Binaries (jede Instanziierung erzeugt neuen Code) und etwas längere Compile-Zeiten. Für die meisten Anwendungscodes sind diese Kosten vernachlässigbar, aber es lohnt sich, sie zu kennen, wenn du knappen, embedded-artigen Code baust, wo die Binär-Größe zählt.

Eine Anmerkung zu Constraints

Manche Generics-Systeme erlauben, einen Typparameter einzuschränken („T muss == unterstützen", „T muss Iterator implementieren"). Zeros Constraint-Geschichte in pre-1.0 ist noch im Fluss – die Beispiele im offiziellen Repo benutzen Generics in ihrer schlichten Form, ohne aufwendige Bounds. Wenn sich die Sprache setzt, ist mit einer Constraint-Syntax in einer kleinen, regulären Form zu rechnen, die zum Rest der Sprache passt. Schreib bis dahin Generics, die für jedes T funktionieren, das du ihnen tatsächlich übergibst, und lass dir vom Compiler sagen, wenn eine Operation nicht unterstützt wird.

Als Nächstes: Enums

Mit Generics parametrisierst du über Typen. Der nächste Baustein liegt am anderen Ende des Spektrums – Enums, Zeros schlichter Aufzählungstyp für Fälle, in denen die Varianten keine extra Daten tragen.

Häufig gestellte Fragen

Wie funktionieren Generics in Zero?

Deklariere Typparameter in spitzen Klammern nach dem Funktions- oder Shape-Namen: fun makePair<T, U>(left: T, right: U) -> Pair<T, U> oder shape Pair<T, U> { left: T, right: U }. Aufrufer pinnen die Parameter entweder explizit fest (Pair<i32, u8>) oder überlassen es dem Compiler, sie aus den Argumenten des Aufrufs zu inferieren.

Können Shapes in Zero generisch sein?

Ja. Eine Shape kann Typparameter mit derselben spitzen-Klammern-Syntax aufnehmen wie Funktionen: shape Pair<T, U> { left: T, right: U }. Jedes Feld darf die Parameter in seinem Typ verwenden. Instanzen entstehen, indem man den parametrisierten Typ ausschreibt – etwa Pair<i32, u8>.

Muss man beim Aufruf einer generischen Funktion die Typparameter angeben?

Meistens nicht. Der Compiler inferiert sie aus den Argumenttypen. makePair(40, 2_u8) reicht – T wird zu i32, U zu u8. Du kannst die Parameter explizit pinnen, wenn die Inferenz den falschen Typ wählen würde oder wenn du sie an der Aufrufstelle dokumentieren willst.

Was ist ein Typ-Alias in Zero?

Ein Typ-Alias ist eine Kurzschreibweise für einen längeren Typausdruck. type BytePair = Pair<u8, u8> lässt dich BytePair überall dort schreiben, wo du sonst Pair<u8, u8> schreiben würdest. Der Alias ist ein reines Namens-Feature – er führt keinen neuen Typ ein, sondern nur eine kürzere Bezeichnung für einen existierenden.

Wo tauchen Generics in Zeros Standardbibliothek auf?

Überall – immer dort, wo ein Typ einen beliebigen Element-Typ halten oder bearbeiten muss. Maybe<T> für optionale Werte, Span<u8> für Byte-Slices, Container-Typen parametrisiert über ihren Element-Typ. Derselbe Generics-Mechanismus deckt sowohl benutzerdefinierte als auch Standardbibliothekstypen ab.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S