- Der Zig-Compiler, der die Kompilierung von C-Code und Cross-Compilation standardmäßig bereitstellt, ist die erstaunlichste Sprache, die der Autor in 45 Jahren Erfahrung kennengelernt hat
- Mit einzigartigen Funktionen wie Compile-Time-Ausführung, Variablen mit beliebiger Bitbreite und einer Test-Block-Umgebung bietet Zig weit mehr als nur einen einfachen Ersatz für C/C++ und ermöglicht eine völlig neue Art zu programmieren
- Dank einer knappen und klaren Syntax mit Variablendeklaration per Typinferenz, anonymen Structs und Label-Breaks ist ein schneller Einstieg möglich
- Unabhängige Modultests über Test-Blöcke und die eingebaute Funktion
@breakpoint unterstützen das Debugging von optimiertem Code
- Mit Unterstützung für Bitfelder und Bitoperationen für Low-Level-Programmierung erreicht Zig zugleich Effizienz und Robustheit und integriert Vorteile von Interpretersprachen in eine kompilierte Sprache
Vorwort
- In 45 Jahren Erfahrung gab es keine Sprache, die so erstaunlich war wie Zig
- Zig ist nicht einfach nur eine neue Sprache, sondern ein Werkzeug, das die Art des Programmierens grundlegend verändert
- Zig nur als Ersatz für C oder C++ zu betrachten, wäre eine massive Unterschätzung
- Ziel dieses Artikels ist es, die einfachen, aber reizvollen Funktionen von Zig vorzustellen und Programmierern einen schnellen Einstieg zu ermöglichen
- In der Praxis gibt es noch mehr Funktionen, die die Akzeptanz von Zig in der Industrie beeinflussen
Der Zig-Compiler
- Da Zig die Kompilierung von C-Code und Cross-Compilation ohne separate Konfiguration standardmäßig bietet, hat es großen Einfluss auf die Industrie
- Die Installation erfolgt, indem man auf der Ziglang-Download-Seite den Compiler für Prozessor/OS herunterlädt, entpackt und in ein gewünschtes Verzeichnis kopiert
- Unter Windows 10 kann man die x86_64-Zip-Datei nach "Program Files" kopieren und das Stammverzeichnis in "zig-windows-x86_64" umbenennen, damit bei Versionsupdates keine Anpassung der Path-Umgebungsvariable nötig ist
- Nach dem Hinzufügen des Pfads des Stammverzeichnisses zur Path-Umgebungsvariable kann der Compiler im CLI-Modus verwendet werden
- Für den Build eines "Hello World!"-Programms wird empfohlen, den Abschnitt "Getting Started" auf der offiziellen Website zu lesen
Zentrale Konzepte und Befehle
Variablendeklaration
- Eine Variablendeklaration besteht aus einem ersten Teil mit Sichtbarkeit (
pub oder ausgelassen), var/const und Variablennamen, einem zweiten Teil mit der Typdeklaration und einem dritten Teil mit der Initialisierung
- Nur der erste und der dritte Teil sind zwingend; der Typ kann aus dem Initialisierungswert abgeleitet werden
- Beispiel:
var sum : usize = 0;
- Ohne
pub deklarierte Variablen sind nur innerhalb des Moduls zugänglich, ähnlich wie static-Variablen in C
- Von
pub-Variablendeklarationen wird abgeraten; stattdessen sollten pub-Funktionen auf ein Minimum beschränkt werden, um die Kopplung zu senken und die Kohäsion zu erhöhen
Structs, anonyme Structs und Test-Blöcke
- Anonyme Struct-Literale, die von
.{ und } umschlossen werden, dienen zur Initialisierung anderer Struct-Elemente oder zur Erzeugung neuer Structs mit initialisierten Elementen
.{ } ist ein leeres anonymes Struct-Literal
- Die Form
struct { } ist eine Struct-Deklaration
- Test-Blöcke können kompiliert und ausgeführt werden, ohne dass eine ausführbare Datei benötigt wird
Bitfelder
- Bitfelder werden in einem
packed struct als Felder mit Typen bestimmter Größe deklariert
- Pointer können auf ein bestimmtes Bitfeld zeigen
For-Schleifen
- Die Zig-Syntax ist klarer als in C, verwendet aber statt
[0..8] das offene Intervall [0..9)
- Typdeklaration, Initialisierung, Prüfung und Erhöhung der Schleifenvariablen
i werden automatisch verarbeitet
Arrays
[_] definiert ein Array unbekannter Größe, gefolgt vom Elementtyp und der Initialisierung
- Beispiel:
var grid = [_]u8{0} ** 81; initialisiert 81 u8-Elemente mit 0
- Die Arraygröße wird aus dem Wiederholungsargument der Initialisierung abgeleitet
- In der Testumgebung kann man über die Array-Elemente iterieren und ihre Summe bilden
- Variablen, die in einer
for-Schleife zwischen | deklariert werden, werden automatisch als vom gleichen Typ wie die Array-Elemente angenommen
usize ist die natürliche vorzeichenlose Ganzzahl der Plattform (u64 auf 64-Bit, u32 auf 32-Bit)
Pointer auf mehrere Elemente
- Damit ein Array-Pointer Pointer-Arithmetik verwenden kann, muss er explizit als Pointer auf mehrere Elemente deklariert werden, etwa
[*]const i32
- Auch wenn das Array
const ist, kann der Pointer selbst als var deklariert werden
Pointer-Dereferenzierung
- Ein Pointer, dem die Adresse einer einzelnen Array-Position zugewiesen wurde, kann nicht per Pointer-Arithmetik aktualisiert werden
- Für Pointer-Dereferenzierung wird
ptr.* verwendet
Label-Break
- Verschiedene Aufgaben wie die Array-Initialisierung können zur Compile-Time ausgeführt werden
- Beim Label-Break wird an den Blocknamen
: angehängt und mit break ein Wert aus dem Block zurückgegeben
0.. ist ein unendlicher Bereich, der bei 0 beginnt
- In
for-Schleifen werden Variablen automatisch initialisiert und erhöht; nach Verarbeitung der letzten Array-Position endet die Schleife
- Arrays müssen nicht explizit mit
undefined initialisiert werden
Funktionen in Zig
- Funktionen werden mit
fn deklariert und sind standardmäßig static (nur innerhalb der Datei nutzbar)
- Mit
pub fn können sie aus anderen Dateien importiert werden
- Funktionen können "inlined" werden
- Bei Funktions-Pointern steht
const voran, gefolgt vom Funktionsprototyp
Objektorientierte Programmierung in Zig
- Structs können Funktionen enthalten
- Im Stack-Beispiel lassen sich maximal 81 Elemente vom Typ
StkNode speichern
- Die Operatoren
++ und -- existieren in Zig nicht; stattdessen werden += und -= verwendet
- Der Stack-Pointer ist eine Ganzzahl, die als Index in das Array
stk dient
- Der Pointer
self wird nicht explizit als Parameter übergeben, sondern implizit als Pointer auf die Stack-Instanz angenommen, auf der die Funktion aufgerufen wird
- Bei einem Aufruf wie
stack.pop() ist self ein Pointer auf stack, ähnlich wie this in Java/C++
- Die Funktion
init() ist der Konstruktor des Stacks
- Die Funktionen
pop und push sind "inlined"
Zig-Programme bauen und ausführen
Ausführbare Datei bauen
- Um eine ausführbare Datei zu erzeugen, wird eine
main-Funktion als Programmeinstieg benötigt
- Ein einfaches Programm kann die
main-Funktion in derselben Datei enthalten
- Für unabhängiges Modul-Debugging kann man am Dateiende eine
main-Funktion einfügen und sie nach Abschluss des Debuggings wieder auskommentieren
- Kompilierbefehl:
zig build-exe -O ReleaseFast program.zig
Test-Blöcke eines Moduls ausführen
- Dies ist eine der besten Funktionen von Zig und wird für Tests und Prototyping verwendet
- Ein Test-Block beginnt mit
test "message" { und endet mit }
"message" ist die Zeichenkette, die beim Ausführen des Tests angezeigt wird
- Test-Blöcke werden unabhängig von ausführbaren Dateien ausgeführt; die fertige ausführbare Datei führt die Tests nicht aus
- Testbefehl:
zig test module.zig
- Der Test-Block in
example.zig prüft die Funktionen set und print; set nimmt eine dezimale Zeichenkette als Parameter, und print gibt nach der Überschrift "Input Grid" das Grid aus
Ausgabe in Zig
- Die Anweisung
std.debug.print ruft die Funktion print in debug.zig der Standardbibliothek std von Zig auf
- Der erste Parameter ist ein Formatstring, der zweite ein anonymes Struct mit der Liste der anzuzeigenden Variablen
- Gibt es kein Format, ist das Struct leer
- Standardmäßig erfolgt die Ausgabe auf
stderr
- Anders als
printf in C kann Zig Literal-Strings und Variablenlisten zur Compile-Time verarbeiten
Debugging ausführbarer Dateien
- Die Nutzung eines Debuggers ist außerhalb von IDEs mit integriertem Debugger (Eclipse, IntelliJ IDEA) oder integrierten Entwicklungskits (
w64devkit) nicht ganz einfach
- Die Integration von Symbolen bläht den Code auf und erfordert die Kompilierung im Debug-Modus, was deutlich weniger effizienten ausführbaren Code erzeugt
- Zig bietet eine praktische Lösung, um diese Probleme zu vermeiden
Eingebaute Funktion @breakpoint
- Wenn
@breakpoint(); in den Quellcode eingefügt wird, stoppt das Programm beim Ausführen im Debugger genau an dieser Stelle
- Dies ist eine nützliche Funktion, um optimierten Zig-Code ohne Symbole zu debuggen
- Wenn man direkt vor
@breakpoint(); std.debug.print verwendet, um zu verfolgende Variablen auszugeben, lassen sich ihre Werte in genau diesem Moment prüfen
- Im Beispiel
debug_example.zig werden innerhalb der Funktion set Code zum Ausgeben von grid und Variablen sowie @breakpoint(); eingefügt
- Build-Befehl:
zig build-exe debug_example.zig
- Danach wird
debug_example.exe mit einem Debugger wie gdb aufgerufen und das Programm mit dem Befehl r gestartet
- Mit dem Befehl
c läuft das Programm weiter, während grid und Variablen verfolgt werden
- Durch wiederholtes Drücken von Enter kann man fortfahren und prüfen, dass die Werte in
grid mit dem Test-Block in example.zig übereinstimmen
Low-Level-Programmierung in Zig
Matrixdarstellung
- Dezimale Ziffern werden als standardmäßige
u8-Ganzzahlen in einer Matrix gespeichert
- Das Eingabe-
grid liegt als Zeichenkette vor, aber ASCII-Zeichen werden intern in u8-Ganzzahlen umgewandelt
- Die Ziffern werden linear, Zeile für Zeile, im Array
grid mit 81 Positionen gespeichert: var grid = [_]u8{0} ** 81;
- Um die Korrektheit von
grid zu prüfen, muss auf Elemente über jede Zeile und jede Spalte zugegriffen werden
- Es wird ein Array mit 9 Pointern erzeugt, von denen jeder auf den Anfang einer Zeile zeigt
- Mit einem Label-Break wird ein Wert aus einem Codeblock zurückgegeben: Mit
break :fill9x9 m; wird die Matrix mit m initialisiert
- Zugriff auf Elemente:
element = matrix[i][j]
Dezimalziffern als Bits darstellen
- Das zentrale Konzept besteht darin, die dezimale Ganzzahl
i durch die Ganzzahl code zu ersetzen
i ∈ [1,9] → code = 2ⁱ⁻¹
i = 0 → code = 0
- Die Position des einzigen auf
1 gesetzten Bits in code ist i-1 (wenn i zwischen 1 und 9 liegt), andernfalls sind alle Bits 0
- Eine Tabelle zeigt die
code-Werte für jede Ziffer (1→1, 2→2, 3→4, ..., 9→256)
code in Zig berechnen
- Der Wert
code wird nur dann mit dem Links-Shift-Operator berechnet, wenn c nicht 0 ist: code = @as(u9,1) << (c-1);
- In Zig müssen Konstanten die passende Größe haben, damit der Ausdruck kompiliert und das Ergebnis einer Variablen zugewiesen werden kann
code wird als Typ u9 deklariert, da der Maximalwert 256 mindestens 9 Bit benötigt
- Zig erlaubt Variablen mit beliebiger Bitbreite
- Mit der eingebauten Funktion
@as wird die Konstante 1 auf den Typ u9 gecastet
grid mit Bitfeldern darstellen
Bitfeld-grid nach Zeilen
- Das Array
lines spiegelt das gesamte grid, indem jede Zeile als 9-Bit-Ganzzahl dargestellt wird: var lines = [_]u9{0} ** 9;
- Beim Array-Zugriff über Zeile
i wird mit einer bitweisen UND-Operation (&) geprüft, ob eine bestimmte Zahl bereits in dieser Zeile vorkommt: lines[i] & code
- Ist das Ergebnis 0, existiert die Zahl in Zeile
i noch nicht; andernfalls ist sie doppelt vorhanden
Bitfeld-grid nach Spalten
- Das Array
columns spiegelt das gesamte grid, indem jede Spalte als 9-Bit-Ganzzahl dargestellt wird: var columns = [_]u9{0} ** 9;
- Beim Array-Zugriff über Spalte
j wird mit einer bitweisen UND-Operation geprüft, ob eine bestimmte Zahl bereits in dieser Spalte vorkommt: columns[j] & code
- Ist das Ergebnis 0, existiert die Zahl in Spalte
j noch nicht; andernfalls ist sie doppelt vorhanden
Sudoku-Regeln
- Beim Einfügen einer neuen Zahl in ein leeres Sudoku-
grid darf sie noch nicht in der gesamten Zeile, Spalte oder Zelle vorkommen, die dieses neue Element enthält
- Zellen sind die neun 3x3-Teilraster, die durch dicke Linien getrennt sind
- Jedes bestimmte Element im 9x9-
grid gehört genau zu einer Zeile, einer Spalte und einer Zelle
- Im Beispiel-
grid enthält die erste Zelle 3, 5, 6, 8 und 9; 1, 2, 4 und 7 fehlen
- Die Arrays
lines und columns übernehmen die Duplikatprüfung für Zeilen und Spalten
- Für die Duplikatprüfung der Zellen wird ein neues Array benötigt
Bitfeld-grid nach Zellen
- Das Array
cells spiegelt das gesamte grid, indem jede Zelle als 9-Bit-Ganzzahl dargestellt wird: var cells = [_]u9{0} ** 9;
- Der Zugriff auf
cells ist einfacher, wenn man es als 3x3-Matrix betrachtet
- Das Array
cell wird ähnlich gefüllt wie zuvor die 9x9-Matrix
- Aus Zeile und Spalte eines Elements im ursprünglichen 9x9-
grid müssen Zeile und Spalte in der Matrix cell bestimmt werden
- Da ganzzahlige Division sehr langsam ist, liefert das Array
cindx = [_]usize{ 0,0,0, 1,1,1, 2,2,2 }; die Divisionsergebnisse
- Beim Matrixzugriff über Zeile
i und Spalte j eines Elements im 9x9-grid wird per bitweiser UND-Operation geprüft, ob eine bestimmte Zahl bereits in der zugehörigen Zelle vorkommt: cell[cindx[i]][cindx[j]] & code
- Ist das Ergebnis 0, existiert die Zahl in der Zelle noch nicht; andernfalls ist sie doppelt vorhanden
Test auf doppelte Elemente
- Nachdem alle vorherigen Elemente derselben Zeile, Spalte und Zelle mit bitweisem ODER (
|) kombiniert wurden, reicht eine bitweise UND-Operation mit code, um doppelte Elemente zu erkennen
if (((lines[i]|columns[j]|cell[cindx[i]][cindx[j]])&code) != 0) {
unreachable;
}
- Ist das Ergebnis 0, kommt das Element in Zeile, Spalte und Zelle noch nicht vor
- Ist das Ergebnis nicht 0, wird das Programm durch Ausführen des Befehls
unreachable gestoppt
- Dies ist die einfachste Möglichkeit in Zig, einen Laufzeitfehler explizit zu kennzeichnen
- Der tatsächliche Code gibt außerdem Details zur Fehlerstelle aus
- Beispiel: Wenn das
0 direkt nach der ersten 8 in der Eingabezeichenkette durch 5 ersetzt wird, tritt ein Fehler auf, weil in Zeile 3, Spalte 1 bereits eine 5 vorhanden ist
Aktualisierung der Datenstrukturen
- In der Funktion
set arbeiten verschachtelte for-Schleifen zeilenweise zusammen, um jedes neue Element aus der Eingabezeichenkette s in grid zu kopieren
- Die Variable
k hält den Index des nächsten Eingabezeichens in der Zeichenkette s
- Das Zeichen wird durch Subtraktion von
'0' in u4 umgewandelt (Variable c)
- Wenn das neue in
grid einzufügende Element nicht 0 ist (c != 0), wird der per Links-Shift berechnete code in jedes gespiegelte grid kopiert
- Dies geschieht per bitweisem ODER (
|=) mit dem jeweiligen Spiegel-grid:
lines[i] |= code;
columns[j] |= code;
cell[cindx[i]][cindx[j]] |= code;
- Es ist nicht nötig, explizit zu prüfen, ob
c im Bereich 1 bis 9 liegt — beim Ausführen der Shift-Operation würde ohnehin ein Overflow auftreten
- Beispiel: Wenn das
0 direkt nach der ersten 8 in der Eingabezeichenkette durch : ersetzt wird, tritt ein Laufzeitfehler auf
- Dasselbe gilt, wenn dasselbe
0 durch / ersetzt wird
- Das Programm funktioniert nur, wenn die Werte im Bereich 1 bis 9 liegen, also wenn das Eingabe-
grid nur Dezimalziffern enthält
- Viele Sudoku-
grids im Web verwenden . statt 0; deshalb enthält die Funktion set die Zeile if (s[k] == '.') c = 0;
- Da
c dann 0 ist, wird die Shift-Operation auf praktische Weise umgangen
Prototyping und Robustheit
- Die erzwungenen Fehler in den beiden vorigen Abschnitten demonstrieren wichtige Zig-Funktionen
- Eine davon ist die Robustheit von Zig — bei Shift-Operationen wird falsches Verhalten nicht zugelassen, sondern zur Laufzeit erkannt
- Obwohl alle Bemühungen auf Effizienz ausgerichtet scheinen, handelt es sich nicht um den typischen Fall, in dem Performance gegen Robustheit eingetauscht wird
- In C ist eine Shift-Operation mit Bitverlust das Problem des Programmierers; das führt zu besserer Performance bestimmter Assembler-Befehle
- Eine weitere Funktion ist die Möglichkeit, Test-Blöcke für Prototyping zu verwenden
- Die Anwendungsmöglichkeiten sind zahllos; die hier gezeigte Anwendung besteht lediglich darin, eine bestimmte Situation beim Auftreten eines Fehlers zu debuggen
- Schon diese Funktionen allein bieten eine erstaunliche Fähigkeit, die in Programmiersprachen sehr selten ist, besonders in kompilierten Programmiersprachen
Fazit
- Zig besteht aus drei Kernelementen: C-Kompatibilität, Cross-Compilation und einfache Installation
- Diese Eigenschaften zeigen das Potenzial, sich als neuer Standard für Systemprogrammiersprachen zu etablieren
- Viele Vorteile, die früher nur in Interpretersprachen zu finden waren, wandern nach und nach in kompilierte Sprachen, um bessere Leistung zu bieten
- Mit dem Konzept der Compile-Time-Ausführung zeigt Zig besonders auffällige Ähnlichkeiten zu Interpretersprachen
- Genau das macht Zig zugleich besonders anders und leistungsfähig, aber auch schwerer zu verstehen
1 Kommentare
Hacker-News-Meinungen
Dieser Beitrag behauptet anfangs, Zig sei nicht einfach nur eine Sprache, sondern eine völlig neue Art des Programmierens, behandelt in Wirklichkeit aber kaum wirklich Zig-spezifische Funktionen.
Typinferenz, anonyme Strukturen,
labeled breakusw. gibt es in anderen Sprachen schon seit Langem.Das wirklich Einzigartige ist comptime, doch genau das wird überhaupt nicht erwähnt.
Es ist zwar kein völlig neues Konzept wie Lisp-Makros, aber die Art, wie Zig es anstelle von Generics verwendet, ist interessant.
Insgesamt wirkt die Behauptung des Artikels jedoch stark überzogen.
Bei Rust ist beeindruckend, dass sich der Zeitpunkt der Codeausführung klar ausdrücken lässt und dass das Design an eine Query-Engine für den gesamten Code-Raum erinnert.
Siehe D-Dokumentation.
Wenn es ein
const-expressionist, wird es automatisch ausgeführt.Es sind genauso unterschiedliche Sprachen wie Java und Scala.
comptimeist weniger eine magische Erfindung als vielmehr eine moderne Version von Metaprogrammierung.Zig ist sauberer als C++-Templates, fühlt sich aber eher wie eine praktische Alternative als wie eine Revolution an.
Persönlich kann ich den übermäßigen Hype – ähnlich wie damals bei Rust – nicht ganz nachvollziehen.
Ich habe sogar die gesamte Zig-Dokumentation gelesen und war eher irritiert, dass nichts wirklich Überraschendes dabei war.
Das größte Problem von Zig ist, dass man Fehlern keine Daten anhängen kann.
Fehler werden nur über einen Nebenkanal weitergegeben, was das Debugging erschwert und am Ende dazu führt, dass Entwickler Fehlerdaten weglassen.
Siehe dieses Issue.
Mit einem simplen Code wie
AccessDeniedlässt sich die Ursache nur schwer erkennen.Selbst wenn man komplexe
Error-Objekte verwendet, braucht man in der Praxis oft einen separaten Diagnosekanal.Wegen Performance-Overhead oder Problemen mit dem Systemzustand ist es je nach Situation sicherer, so etwas per Late Binding zu behandeln.
Zig folgt dabei einer Philosophie, die Präzision und Determinismus priorisiert.
Siehe dieses Issue.
Was aber wirklich nötig ist, sind strukturiertes Logging und kontextbezogene Nachverfolgung auf Basis des Call Stacks.
std.zonwird oft als gutes Beispiel genannt, und in der Community gibt es Bestrebungen, verschiedene Patterns für Error Handling zu sammeln und in den Standard einfließen zu lassen.Es verhindert, dass träge Entwickler wahllos immer mehr Daten dranhängen.
Ich kann der Aussage zustimmen, dass die Art, wie Zig entwickelt wird, selbst eine neue Art der Sprachentwicklung ist.
Beeindruckend ist dieser langsame Evolutionsprozess, bei dem Funktionen sorgfältig geprüft und Überflüssiges entfernt werden.
Ich würde gern konkreter hören, was daran wirklich Zig-spezifisch ist.
Mir gefällt, dass sich Zig über PyPI installieren lässt.
Das ziglang-Paket kann man einfach mit
pip install ziglanginstallieren und sofort verwenden.Mit
uvxlässt sich sogar C-Code bauen.Schade ist, dass Funktionen, die es schon in Sprachen wie Ada, Object Pascal oder Modula-2 gab, hier als Zig-„Innovation“ verkauft werden.
Es ist interessant, wie Ideen von vor 40 Jahren wieder neu wirken, sobald sie in C-artiger Syntax neu verpackt werden.
Der Einstieg des Artikels war gut, danach blieb es aber bei einer bloßen Aufzählung von Zig-Funktionen.
Zigs intuitive Syntax und explizite Kontrollflusssteuerung (
deferusw.) sind durchaus attraktiv.Dank
comptimemuss man auch keine separate Makro-Syntax lernen.Alle Bestandteile greifen auf natürliche Weise ineinander, sodass es sich selbst beim ersten Einsatz wie ein Werkzeug anfühlt, das man schon lange benutzt.
Zigs Syntax
for (0..9)ist zwar intuitiv, aber als offenes Intervall oft verwirrend.Wie bei Python
range(0, 9)vergisst man leicht, ob der letzte Wert enthalten ist.0..9und0..=9in dieser Hinsicht klarer.Die Größe des Intervalls ergibt sich einfach aus der Differenz, und auch die Rückwärtsiteration wird einfacher.
0..<5(offen) und0...5(geschlossen) noch expliziter.Mir gefallen Zigs Bezeichnerregeln nicht besonders.
Die Mischung aus
snake_caseundcamelCasewirkt etwas unharmonisch.Trotzdem sind das Build-System, die Allocators, und die gesamte Compiler-Erfahrung hervorragend.
Ich benutze hauptsächlich Rust, aber meine Neugier auf Zig bleibt bestehen.
Die Präfix-Konventionen in C-Bibliotheken finde ich ebenfalls lästig.
Der Reiz von Zig liegt nicht in einem einzelnen Feature, sondern in der Summe pragmatischer Entscheidungen.
Selbst Entscheidungen, die anfangs radikal wirken, erscheinen mit tieferem Verständnis zunehmend nachvollziehbar.
Zig ist eine Sprache, die neugierige Entwickler belohnt.
Einer der Gründe, warum Zig gut ist: Die Sprache akzeptiert die Realität von Low-Level-Systemcode.
Viele Sprachen blenden solche Aspekte aus ästhetischen Gründen aus, Zig nicht.
Siehe page_allocator-Dokumentation.