↑Programmieren in Rust
Fortführung von ›Grundbegriffe: Kontrollfluss‹.
Manchmal gibt man nur für eine bestimmte Enum-Variante eine
Ausführung an. Das ist vor allem dann der Fall, wenn es wie bei
Option
und Result
nur zwei Varianten
gibt.
match expression { E::A(x) => {/* Ausführung von Zweig A */}, _ => {} }
Hierfür steht eine Kurzschreibweise zur Verfügung:
if let E::A(x) = expression { /* Ausführung von Zweig A */ }
Für das Konstrukt
match expression { E::A(x) => {/* Ausführung von Zweig A */}, _ => {/* Ausführung sonst */} }
gibt es entsprechend:
if let E::A(x) = expression { /* Ausführung von Zweig A */ } else { /* Ausführung sonst */ }
Erlaubt ist auch else if let
. Mehr noch, man
darf je Zweig frei zwischen if
bzw. else if
und if let
bzw. else if let
wählen.
Bekommt eine Schleife eine Marke 'label
, führt
break 'label
zum Ausbruch aus dieser Schleife. Dies
erlaubt den Ausbruch aus mehreren verschachtelten Schleifen.
Dieses Mittel sei an der folgenden Funktion demonstriert, die eine
Sequenz von Werten in ein Array von vollständigen Blöcken der
Größe n
zerlegt.
fn complete_chunks<T: Clone>(a: &[T], n: u32) -> Vec<Vec<T>> { let mut acc = vec![]; let mut i = a.into_iter(); 'outer: loop { let mut t = vec![]; for _ in 0..n { match i.next() { Some(x) => t.push(x.clone()), None => break 'outer } } acc.push(t); } acc } fn main() { let a: Vec<u32> = (0..10).collect(); println!("{:?}", complete_chunks(&a,4)); }
Außerdem ist es möglich, mit einem Wert aus der Schleife auszubrechen. Man könnte das Beispiel also auch so schreiben:
fn complete_chunks<T: Clone>(a: &[T], n: u32) -> Vec<Vec<T>> { let mut acc = vec![]; let mut i = a.into_iter(); 'outer: loop { let mut t = vec![]; for _ in 0..n { match i.next() { Some(x) => t.push(x.clone()), None => break 'outer acc } } acc.push(t); } }
Natürlich hätte man das in diesem Fall auch anders formulieren können, z. B. durch Einführung einer Hilfsvariable oder einfach
None => return acc
Sollen mehrere Muster zum gleichen Ausführungszweig führen, lassen sich diese mit einem senkrechten Strich Oder-verknüpfen. Betrachten wir dazu die Fibonacci-Folge
a0 := 0; a1 := 1; an := an−1 + an−2.
Die Funktion
fn fib(n: u32) -> u32 { if n == 0 || n == 1 {1} else {fib(n-1) + fib(n-2)} }
ist äquivalent zu:
fn fib(n: u32) -> u32 { match n { 0 | 1 => 1, n => fib(n-1) + fib(n-2) } }
Als Muster sind auch Bereiche erlaubt. So lässt sich
in der obigen Funktion fib
das Muster
0 | 1
gegen 0..=1
ersetzen. Ein
besseres Beispiel ist die folgende Funktion char_class
,
die ASCII-Zeichen auf eine Zeichenklasse projiziert.
enum CharClass {None, Digit, Lower, Upper} fn char_class(c: char) -> CharClass { match c { '0'..='9' => CharClass::Digit, 'a'..='b' => CharClass::Lower, 'A'..='B' => CharClass::Upper, _ => CharClass::None } }
Auch Bereiche lassen sich mit dem senkrechten Strich Oder-verknüpfen. Dies sei an einer Variation der letzten Funktion verdeutlicht.
enum CharClass {None, Digit, Alpha} fn char_class(c: char) -> CharClass { match c { '0'..='9' => CharClass::Digit, 'a'..='b' | 'A'..='B' => CharClass::Alpha, _ => CharClass::None } }
Entpacken eines Tupels t = (1,2)
als
let x = t.0; let y = t.1;
geht auch kürzer mittels:
let (x, y) = t;
Zudem ist auch teilweises Entpacken möglich. Z. B.
sind für ein Tupel t = (1,2,3)
die folgenden
drei Zeilen gleichbedeutend:
let x = t.0; let (x, _, _) = t; let (x, ..) = t;
Man bekommt sogar
let x = t.0; let z = t.2;
mittels:
let (x, .., z) = t;
Des Weiteren darf das Entpacken von Tupeln auch in Verbindung mit dem Pattern-Matching vorkommen. Wollen wir bspw. die Potenzfunktion
fn pow(x: u32, n: u32) > u32 { if n == 0 {1} else {x*pow(x, n-1)} }
als einstellige Funktion formulieren, führt das zu:
fn pow(t: (u32, u32)) -> u32 { match t { (_, 0) => 1, (x, n) => x*pow((x, n-1)) } }
Obendrein darf das Entpacken sogar in der Angabe der Argumente vorkommen, womit sich die alternative Formulierung
fn pow((x, n): (u32,u32)) -> u32 { if n == 0 {1} else {x*pow((x, n-1))} }
erlaubt.
Matching von Enumerationen ist schon öfters vorgekommen.
Der Vollständigkeit halber hier nochmals ein Beispiel. Die
Funktion checked_mul
hat Rückgabewerte vom Typ
enum Option<T> {None, Some(T)}.
Hiermit gestattet sich die Formulierung der Potenzfunktion mit Überlauf-Prüfung als:
fn pow(x: u32, n: u32) -> Option<u32> { if n == 0 {Some(1)} else { x.checked_mul(match pow(x, n-1) { Some(value) => value, None => return None }) } }
Dieser spezielle Fall gewährt zudem die Abkürzung mit dem Fragezeichen-Operator:
fn pow(x: u32, n: u32) -> Option<u32> { if n == 0 {Some(1)} else {x.checked_mul(pow(x, n-1)?)} }
Ein Match-Arm kann eine zusätzliche logische Bedingung
bekommen, die Wächter, engl. match guard, genannt
wird. Die Bedingung wird durch ein dem Muster nachgestelltes
if
eingeleitet.
Das folgende Programm zeigt den klassischen euklidischen Algorithmus, wobei ein Wächter in der Fallunterscheidung zum Einsatz kommt.
fn gcd(a: u32, b: u32) -> u32 { match (a, b) { (a, 0) => a, (0, b) => b, (a, b) if a > b => gcd(a-b, b), (a, b) => gcd(a, b-a) } } fn lcd(a: u32, b: u32) -> u32 { a*b/gcd(a,b) } fn main() { println!("{}", (1..=10).fold(1, lcd)); }
Die gezeigte Formulierung ist äquivalent zu:
fn gcd(a: u32, b: u32) -> u32 { if b == 0 {a} else if a == 0 {b} else if a > b {gcd(a-b, b)} else {gcd(a, b-a)} }
Pattern-Matching mit Wächtern ist so allgemein, dass sich beliebige Verzweigungen in diese Form umformulieren lassen. Beispielsweise ist die Potenzfunktion auch als
fn pow(x: u32, n: u32) -> u32 { match n { n if n == 0 => 1, n => x*pow(x, n-1) } }
oder
fn pow(x: u32, n: u32) -> u32 { match () { () if n == 0 => 1, () => x*pow(x, n-1) } }
formulierbar. Allgemein kann man jede Verzweigung
if cond {block1} else {block2}
in die Form
match () { () if cond => {block1}, () => {block2} }
bringen.
Erwähnen möchte ich noch eine recht entbehrliche Funktionalität die nur in sehr speziellen Situationen von Nutzen ist. Betrachten wir die folgende Funktion, die zu einem Feld von Feldern von Zahlen die Stelle des ersten Vorkommens einer gegebenen Zahl findet.
fn position(a: &[Vec<i32>], x: i32) -> Option<(usize, usize)> { for (i, b) in a.iter().enumerate() { for (j, &y) in b.iter().enumerate() { if x == y {return Some((i, j));} } } None } fn main() { let a = vec![vec![1, 2], vec![3, 4, 5]]; assert_eq!(Some((1, 0)), position(&a, 3)); }
Möchte man das Programm nun funktional schreiben oder in
Funktionen zerlegen, gelangt man zur Komplikation, dass der Ausbruch
mittels return
oder break
nur bei
gewöhnlichen Schleifen möglich ist. Um das dennoch zu erreichen,
steht als eine Art Verallgemeinerung der tiefe Ausbruch mit dem Typ
ControlFlow<B, C>
zur Verfügung, dabei
handelt es sich wie bei Result
um eine Enumeration
von zwei Varianten. Sie ist als
enum ControlFlow<B, C = ()> { Continue(C), Break(B) }
definiert. Damit lässt sich das Programm in die folgende Gestalt bringen.
use std::ops::ControlFlow; use ControlFlow::{Break, Continue}; fn position_at(i: usize, b: &[i32], x: i32) -> ControlFlow<(usize, usize)> { b.iter().enumerate().try_for_each(|(j, &y)| { if x == y {Break((i, j))} else {Continue(())} }) } fn position(a: &[Vec<i32>], x: i32) -> Option<(usize, usize)> { a.iter().enumerate() .try_for_each(|(i, b)| position_at(i, b, x)) .break_value() }
Des Weiteren unterstützt ControlFlow
den
Fragezeichen-Operator, wobei der Aufstieg erwartungsgemäß zur Variante
Break
gehört.
Bemerkenswert ist, dass der tiefe Ausbruch gänzlich als Funktionalität einer Bibliothek formuliert werden kann ohne die Sprache erweitern zu müssen.