↑Programmieren in Rust
Größere Programme werden durch Strukturierung leichter verständlich. Auf der untersten Ebene benutzt man dazu Kontrollstrukturen anstelle von Sprunganweisungen. Dann kommen Funktionen, die Vorgänge abstrahieren und über ihre Argumente und Rückgabewerte Schnittstellen liefern. Auf der nächsten Ebene bündelt man zusammengehörige Funktionen und Datentypen in Module. Schnittstellen entstehen hierbei, indem man bestimmte Funktionen und Datentypen als öffentlich kennzeichnet.
Ein Modul wird mit dem Schlüsselwort mod
eingeleitet.
Der Inhalt des Moduls befindet sich entweder in geschweiften Klammern,
oder bei längeren Modulen in einer extra Datei. Funktionen, Datentypen,
Konstanten und Variablen sind nur dann öffentlich, wenn sie mit
dem Schlüsselwort pub
markiert wurden.
Die Grundstruktur von Modulen sei hier an der Bestimmung der Parität verdeutlicht:
mod parity { pub fn even(x: i32) -> bool {x%2 == 0} } use parity::even; fn main() { println!("{}", even(0)); }
Die use
-Anweisung macht lediglich deutlich,
welchem Modul die Funktion even
entstammen soll.
Es könnte ja mehrere Module mit einer so benannten Funktion geben.
Zudem gibt die use
-Anweisung darüber Aufschluss,
welche Abhängigkeiten ein Modul besitzt.
Möchte man sich unmissverständlich ausdrücken, kann zudem
klargestellt werden, aus welchem Crate das Modul entstammen soll.
Mit dem Schlüsselwort crate
bezieht man sich hierbei
auf das gegenwärtige Crate. So kann
use parity::even;
als Kurzform von
use crate::parity::even;
gelesen werden.
Alternativ zur use
-Anweisung lässt sich der Aufruf als
parity::even(0)
oder in aller Ausführlichkeit als
crate::parity::even(0)
qualifizieren.
Das Crate std
bezeichnet die Standardbibliothek.
Mit std::collections::Vec
ist also der Typ
Vec
aus dem in der Standardbibliothek befindlichen
Modul collections
gemeint. Ich möchte anmerken, dass
die Standardbibliothek nicht fest mit Rust verdrahtet ist, dass man
sie also gegen eine alternative Standardbibliothek austauschen könnte,
was für eine Systemprogrammiersprache unter manchen Umständen
vorteilhaft sein kann. Des Weiteren ist die Standardbibliothek kein
monolitischer Block, sondern baut selbst auf den Crates core
und alloc
auf.
Ein Modul kann seinerseits Untermodule enthalten. Mittels
pub use
lässt sich Funktionalität aus einem Untermodul
als öffentliche Schnittstelle weitergeben. Das heißt, die
Funktionalität erscheint von Außen betrachtet so, als wäre sie im Modul
selbst definiert.
mod parity { mod even { pub fn even(x: i32) -> bool {x%2 == 0} } mod odd { pub fn odd(x: i32) -> bool {x%2 == 1} } pub use {even::even, odd::odd}; }
Über die Existenz der Untermodule ist von Außen betrachtet quasi
nichts bekannt. Wollte man sie zugänglich machen, müsste man
pub mod
schreiben.
Woher die reexportierte Funktionalität entstammt, ist beliebig. Beispielsweise hindert einen nichts daran, die verschachtelte Struktur aufzulösen:
mod even { pub fn even(x: i32) -> bool {x%2 == 0} } mod odd { pub fn odd(x: i32) -> bool {x%2 == 1} } mod parity { pub use crate::{even::even, odd::odd}; }
Auf der nächsthöheren Ebene kann man auch wieder zusammengehörige Module bündeln. Man spricht dann von einer Bibliothek, die in Rust kurz als Crate bezeichnet wird. Eine Bibliothek muss aber nicht unbedingt in Module aufgeteilt sein.
Die Datei lib.rs
bildet die Wurzel einer
Bibliothek. Alle ihre öffentlichen Schnittstellen werden wie bei einem
Modul mit pub
gekennzeichnet. Das gilt gleichermaßen
für ihre öffentlichen Module, die also als pub mod
erscheinen müssen.
Der Befehl
cargo new --lib lib0
erzeugt die neue Bibliothek lib0
mit der
Verzeichnisstruktur:
lib0/ └─ src/ └─ lib.rs
Eigentlich umfasst der Begriff Crate auch ausführbare
Programme. Ein ausführbares Programm unterscheidet sich von einer
Bibliothek lediglich dadurch, dass die Wurzel durch die Datei
main.rs
anstelle von lib.rs
gebildet wird.
Innerhalb einer Bibliothek darf es zyklische Definitionen geben. Zwischen Bibliotheken sind solchen Verwebungen nicht erlaubt.
Das folgende Programm arbeitet zwar korrekt, ist jedoch in schlechtem Stil geschrieben.
fn is_ascii_alphabetic(c: char) -> bool { let x = u32::from(c); 65 <= x && x <= 90 || 97 <= x && x <= 122 } fn main() { let a: Vec<&str> = "Ein Schiff taucht im Nebel auf." .split(|x| !is_ascii_alphabetic(x)).collect(); println!("{:?}", a); }
Im Programm tauchen »magische« Konstanten mit irgendeiner bestimmten Bedeutung auf, diese ist jedoch nicht klar erkennbar. Der Leser muss sich die Bedeutung der Konstanten erst erschließen. Hier mag das wohl einfach sein. Bei komplizierten Programmen steht man da aber schnell im Regen. Ganz kritisch ist die Situation wo dieselbe Konstante mehrmals im Programm vorkommt. Würde jemand die Konstante an einer Stelle zur Anpassung verändern, blieben die anderen Stellen wahrscheinlich übersehen, es kommt zu einem logischen Fehler.
Für besseres Verständnis, Abstraktion und Faktorisierung lassen
sich globale oder auch lokale Konstanten mit dem Schlüsselwort
const
definieren. Konstanten in einem Modul sind privat.
Öffentlich sind sie nur dann, wenn sie mit pub
markiert
wurden. Das verbesserte Programm ist hoffentlich angenehmer zu lesen:
mod charsets { const A: u32 = 'a' as u32; const Z: u32 = 'z' as u32; const CAPA: u32 = 'A' as u32; const CAPZ: u32 = 'Z' as u32; pub fn is_ascii_alphabetic(c: char) -> bool { let x = u32::from(c); CAPA <= x && x <= CAPZ || A <= x && x <= Z } } use charsets::is_ascii_alphabetic; fn main() { let a: Vec<&str> = "Ein Schiff taucht im Nebel auf." .split(|x| !is_ascii_alphabetic(x)).collect(); println!("{:?}", a); }
Unter normalen Umständen werden Konstanten in großen Buchstaben geschrieben, das wird auch vom Compiler überprüft. Empfindet man das in einer bestimmten Situation zu unschön, lässt es sich auch ausschalten. Man muss dafür die Zeile
#[allow(non_upper_case_globals)]
vor die Definition der Konstante schreiben, oder wenn es dutzende solche Konstanten gibt,
#![allow(non_upper_case_globals)]
am Anfang des Moduls.
Konstante Zeichenketten werden normalerweise mit dem Schlüsselwort
static
definiert, nicht mit const
:
static TEXT: &str = "Ein Schiff taucht im Nebel auf.";
Manchmal möchte man Binärdaten nicht zur Laufzeit aus einer Datei laden, sonern bereits zur Kompilierzeit in die ausführbare Datei einbetten. Handelt es sich lediglich um ein paar wenige Bytes, kann man die Daten wie in
static DATA: &[u8] = &[0x45, 0x75, 0x6c, 0x65];
direkt als Literal angeben. Für große Datenmengen steht
stattdessen das Makro include_bytes
zur Verfügung,
das die Daten zur Kompilierzeit aus der angegebenen Datei lädt:
static DATA: &[u8] = include_bytes!("data.bin");
Weiterhin existiert auch ein Makro inlude_str
zur Einbettung von UTF-8-kodierten Dateien als Zeichenketten.
Man kann sich damit unter anderem ein Quine ermogeln. Unter einem Quine
wird ein Programm verstanden, das seinen eigenen Quelltext repliziert.
Mit include_str
geht das ganz einfach:
static SELF: &str = include_str!("main.rs"); fn main() { println!("{}", SELF); }
Eine sinnvollere Idee ist meiner Ansicht nach die nummerierte Ausgabe des eigenen Quelltextes, die man mit
for (i, line) in SELF.lines().enumerate() { println!("{:02} {}", i + 1, line); }
bewerkstelligen kann.
Das Makro include
bietet die Möglichkeit, einen
Quelltext-Abschnitt eines Moduls in eine andere Datei auszulagern.
Wer dazu geneigt ist, ein übermäßig langes Modul zu schreiben, kann
dieses damit trotzdem übersichtlich zu Dateien auftrennen. Es sei
aber gesagt, dass dieser Weg eigentlich nicht beschritten werden muss,
da das Modulsystem ebenfalls die notwendigen Fähigkeiten dafür besitzt.
Es ist ja mit dem qualifizierten Import ausgestattet und enthält mit dem
öffentlichen Import einen Reexport.
In gewisser Hinsicht darf man
mod m;
als Kurzschreibweise für
mod m {include!("m.rs");}
betrachten. Allerdings besteht zwischen beidem in der näheren
Betrachtung ein Unterschied. Das Modul m
enthalte
beispielsweise ein Untermodul m0
und das Verzeichnis
haben die folgende Struktur:
src/ ├─ main.rs ├─ m/ │ └─ m0.rs └─ m.rs
Einbindung vermittels include
macht dann
die explizite Pfadangabe erforderlich, weil die Umstellung
auf das Unterverzeichnis nicht mehr automatisch stattfindet.
/* Datei m.rs */ #[path = "m/m0.rs"] pub mod m0;
Definitionen dürfen sich wechselseitig aufeinander beziehen. Darüber hinaus darf man solche Definitionen in Module aufteilen. So zieht sich die wechselseitig rekursive Definition
mod even { use super::odd::odd; pub fn even(x: u32) -> bool { if x == 0 {true} else {odd(x - 1)} } } mod odd { use super::even::even; pub fn odd(x: u32) -> bool { if x == 0 {false} else {even(x - 1)} } }
der beiden Funktionen even
und odd
durch zwei Module.