↑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.