18. Mär 2024
Lesedauer 12 Min.
Kontrollstrukturen
Rust-Kurs, Teil 2
Von Funktionen und Kontrollstrukturen bis zum Ownership-Konzept.

Der erste Artikel dieser Serie zur Programmiersprache Rust [1] hat die grundlegende Struktur von Rust-Programmen vorgestellt. Erläutert wurden die Kernkonzepte, Ziele und Vorteile von Rust, und das erste Rust-Programm implementiert. Darüber hinaus wurden die Datentypen von Rust besprochen, und auf die Unterschiede zu anderen Programmiersprachen hingewiesen. Diese zweite Folge des Kurses beschäftigt sich mit Funktionen, Statements, Expressions und den Kontrollstrukturen von Rust. Thematisiert wird zudem das Ownership-Konzept, mit dessen Hilfe Race Conditions in Multithreading-Anwendungen vermieden werden.
Funktionen
Im ersten Teil der Serie haben Sie bereits Bekanntschaft mit Rust-Funktionen gemacht. Sie haben die Funktion main() kennengelernt, die beim Starten des Programms aufgerufen wird, und auch eigene Funktionen mit dem Schlüsselwort fn definiert.Rust-Funktionen liefern in ihrer Standardausführung keinen Rückgabewert, C-Programmierer (C/C++/C#) kennen das vom Schlüsselwort void. Hier eine Funktionsdefinition und deren Aufruf:
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>()
{
say_hello_world();
}
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">say_hello_world</span></span>()
{
<span class="hljs-built_in">println!</span>(<span class="hljs-string">"Hello World!!"</span>);
}
Auch eine Parameterliste kann eine Rust-Funktion aufweisen, sofern man für jeden Parameter den entsprechenden Datentyp definiert. Hier ein Beispiel:
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>()
{
calculate_something(<span class="hljs-number">42</span>, <span class="hljs-number">85</span>);
}
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">calculate_something</span></span>(a: <span class="hljs-keyword">i32</span>, b: <span class="hljs-keyword">i32</span>)
{
<span class="hljs-built_in">println!</span>(<span class="hljs-string">"a * a = {}, b * b = {}"</span>, a * a, b * b);
}
Beim Aufruf der Funktion sind dabei passende Funktions-
parameter zu übergeben. Ist das nicht der Fall, wirft der Rust-Compiler eine Fehlermeldung. In Bild 1 sehen Sie die Meldung, wenn Float-Werte (f32) anstelle von Integer-Werten (i32) übergeben wurden. Die Abbildung zeigt, dass der Compiler sehr aussagekräftige Fehlermeldungen liefert, die genau anzeigen, an welcher Stelle welches Problem aufgetreten ist.
parameter zu übergeben. Ist das nicht der Fall, wirft der Rust-Compiler eine Fehlermeldung. In Bild 1 sehen Sie die Meldung, wenn Float-Werte (f32) anstelle von Integer-Werten (i32) übergeben wurden. Die Abbildung zeigt, dass der Compiler sehr aussagekräftige Fehlermeldungen liefert, die genau anzeigen, an welcher Stelle welches Problem aufgetreten ist.

Fehlermeldung des Compilers: Es wurden Fließkommawerte anstatt Integer-Werten übergeben (Bild 1)
Autor
Obwohl Rust erst einmal keinen Rückgabewert für Funktionen vorsieht, kann man dennoch einen vorsehen: Er wird mit einem Pfeil (->) und der Angabe des gewünschten Datentyps definiert. Der folgende Code-Schnipsel zeigt eine einfache Funktion, die einen übergebenen Integer-Wert potenziert und das Ergebnis als i32-Wert zurückgibt.
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>()
{
<span class="hljs-keyword">let</span> result = power_of_n(<span class="hljs-number">2</span>, <span class="hljs-number">3</span>);
<span class="hljs-built_in">println!</span>(<span class="hljs-string">"2 ^ 3 = {}"</span>, result);
}
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">power_of_n</span></span>(x: <span class="hljs-keyword">i32</span>, n: <span class="hljs-keyword">u32</span>) -> <span class="hljs-keyword">i32</span>
{
<span class="hljs-keyword">let</span> result = i32::pow(x, n);
result
}
Sehen Sie sich die Implementierung der Funktion näher an, werden Sie feststellen, dass es nirgendwo ein return-Statement gibt. In Rust genügt es, am Ende einer Funktion einen Ausdruck (Expression) zu definieren, welcher das Funktionsergebnis festlegt. In der Funktion power_of_n ist diese Expression die Variable result. Das Ergebnis der Funktion ist der aktuelle Inhalt dieser Variablen.Im Code der Funktion sehen Sie zudem, dass hinter result das sonst übliche Semikolon fehlt. Dies ist eine weitere Besonderheit von Rust. Der Compiler toleriert das Fehlen des Strichpunkts, aber auch wenn man ihn setzt, führt das nicht zu einem Fehler:
<span class="hljs-built_in">return</span> <span class="hljs-literal">result</span>;
Zudem besteht die Möglichkeit, eine Funktion vorzeitig durch ein return-Statement zu verlassen. Hier ein Beispiel:
fn main<span class="hljs-function"><span class="hljs-params">()</span></span>
<span class="hljs-function">{</span>
<span class="hljs-function"> <span class="hljs-title">println</span>!<span class="hljs-params">(<span class="hljs-string">"10 / 2 = {}"</span>, </span></span>
<span class="hljs-function"><span class="hljs-params"> early_function_termination(<span class="hljs-number">10</span>, <span class="hljs-number">2</span>))</span>;</span>
<span class="hljs-function"> <span class="hljs-title">println</span>!<span class="hljs-params">(<span class="hljs-string">"10 / 0 = {}"</span>, </span></span>
<span class="hljs-function"><span class="hljs-params"> early_function_termination(<span class="hljs-number">10</span>, <span class="hljs-number">0</span>))</span>;</span>
<span class="hljs-function">}</span>
<span class="hljs-function"><span class="hljs-title">fn</span> <span class="hljs-title">early_function_termination</span><span class="hljs-params">(</span></span>
<span class="hljs-function"><span class="hljs-params"> x: i32, y: i32)</span> -></span> i32
{
<span class="hljs-keyword">if</span> y == <span class="hljs-number">0</span>
{
<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>
}
<span class="hljs-keyword">else</span>
{
x / y
}
}
Wie Sie sehen, wird mit einem if Statement überprüft, ob eine Division durch 0 durchgeführt werden soll. Ist das der Fall, wird die Funktion vorzeitig durch das return-Statement beendet.Rust-Funktionen können erwartungsgemäß nicht nur skalare Werte zurückgeben, sondern auch komplexe Datentypen, beispielsweise vom Datentyp Tupel, den Sie bereits in der ersten Folge [1] kennengelernt haben. Listing 1 zeigt ein Beispiel. Darin wird für die Funktion tuple_return_values() ein Tupel als Rückgabewert definiert, der zwei Integer-Werte beinhaltet. Beim Aufruf dieser Funktion wird das Ergebnis der Variablen result zugewiesen, deren Datentyp vom Rust-Compiler zugewiesen wird.
Listing 1: Tupel als Rückgabewert
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>()<br/>{<br/> <span class="hljs-keyword">let</span> result = tuple_return_values(<span class="hljs-number">2</span>, <span class="hljs-number">4</span>);<br/> <span class="hljs-built_in">println!</span>(<span class="hljs-string">"2 ^ 2 = {}, 4 ^ 4 = {}"</span>,<br/> result.<span class="hljs-number">0</span>,<br/> result.<span class="hljs-number">1</span>);<br/>}<br/><span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">tuple_return_values</span></span>(x: <span class="hljs-keyword">i32</span>, y: <span class="hljs-keyword">i32</span>) -&gt; (<span class="hljs-keyword">i32</span>, <span class="hljs-keyword">i32</span>)<br/>{<br/> <span class="hljs-keyword">let</span> result = (x * x, y * y);<br/> result<br/>}
In diesem Fall ist der zurückgelieferte Datentyp wiederum ein Tupel vom Typ (i32, i32). Auf die einzelnen Bestandteile des Tupels können Sie über result.0 (erster Integer-Wert) und result.1 (zweiter Integer-Wert) zugreifen.
Kontrollstrukturen
Wie Sie mit Rust-Funktionen arbeiten können, soll dieser Abschnitt über die Kontrollstrukturen von Rust verdeutlichen. Es geht um Bedingungen, Schleifen und Ähnliches. Darauf aufbauend folgen weiter unten die Expressions, die ebenfalls ein leistungsfähiges Konzept in Rust darstellen.Das if-else-Statement haben Sie bereits im Code-Schnipsel kennengelernt, in dem ein Variablenwert überprüft wurde, um eine Division durch 0 zu vermeiden. Sehen Sie sich den Code noch einmal an, werden Sie feststellen, dass die Bedingung nicht in Klammern eingefasst ist:
<span class="hljs-keyword">if</span> <span class="hljs-attr">y</span> == <span class="hljs-number">0</span>
Setzt man die Bedingung dennoch in Klammern, schreibt also if (y == 0), lässt sich der Code zwar nach wie vor kompilieren, der Compiler präsentiert jedoch eine Warnung, die darauf hinweist, dass die Klammern entfernt werden sollten (Bild 2).

Ein Beispiel für eine Option-Enumeration (Bild 4)
Autor
Sollen mehrere Bedingungen zu einem logischen Ausdruck zusammengefasst werden, sind runde Klammern dafür selbstverständlich erlaubt, wie folgendes Beispiel im zweiten If-Statement zeigt:
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">if_statements</span></span>(x: <span class="hljs-keyword">i32</span>, y: <span class="hljs-keyword">i32</span>, z: <span class="hljs-keyword">i32</span>)
{
<span class="hljs-keyword">if</span> x < <span class="hljs-number">10</span> && y > <span class="hljs-number">20</span>
{
<span class="hljs-built_in">println!</span>(
<span class="hljs-string">"The 1st if condition is met!"</span>);
}
<span class="hljs-keyword">if</span> x < <span class="hljs-number">10</span> || (y < <span class="hljs-number">10</span> && z < <span class="hljs-number">10</span>)
{
<span class="hljs-built_in">println!</span>(<span class="hljs-string">"The 2nd if condition is met!"</span>);
}
}
Eine weitere Besonderheit von Rust ist es, dass jeder Code-Block in geschwungene Klammern eingefasst werden muss – selbst dann, wenn es sich nur um eine einzige Zeile Code handelt! Die folgende Funktion lässt sich daher nicht kompilieren, da sich die Code-Blöcke des if-else-Statements nicht in geschwungenen Klammern befinden. Die zugehörige Fehlermeldung zeigt Bild 3.

Klammerfehler: Alle Code-Blöcke müssen innerhalb geschweifter Klammern stehen (Bild 3)
Autor
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">if_else_statement_not_working</span></span>(x: <span class="hljs-keyword">i32</span>)
{
<span class="hljs-keyword">if</span> x < <span class="hljs-number">10</span>
<span class="hljs-built_in">println!</span>(<span class="hljs-string">"x is smaller than 10"</span>);
<span class="hljs-keyword">else</span>
<span class="hljs-built_in">println!</span>(<span class="hljs-string">"x is larger than 10"</span>);
}
Damit der Compiler den Code akzeptiert, sind beide Aufrufe des Makros println!() in je einen eigenen Code-Block aufzunehmen, der durch geschwungene Klammern definiert wird.Listing 2 zeigt die richtige Implementierung. Wer bislang mit C, C++ oder C# gearbeitet hat, sollte diesen syntaktischen Feinheiten bei der Arbeit mit Rust besondere Aufmerksamkeit widmen.
Listing 2: Code-Blöcke setzen
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">if_else_statement_working</span></span>(x: <span class="hljs-keyword">i32</span>)<br/>{<br/> <span class="hljs-keyword">if</span> x &lt; <span class="hljs-number">10</span><br/> {<br/> <span class="hljs-built_in">println!</span>(<span class="hljs-string">"x is smaller than 10"</span>);<br/> }<br/> <span class="hljs-keyword">else</span> {<br/> <span class="hljs-built_in">println!</span>(<span class="hljs-string">"x is larger than 10"</span>);<br/> }<br/>}
Für die Implementierung von Schleifen stellt Rust mehrere Statements zur Verfügung. Das einfachste davon ist das loop-Statement, das eine Endlosschleife erzeugt, sofern keine Abbruchbedingung implementiert wird:
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">loop_statement</span></span>(i: <span class="hljs-keyword">i32</span>)
{
<span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> loop_counter = <span class="hljs-number">0</span>;
<span class="hljs-keyword">loop</span>
{
<span class="hljs-built_in">println!</span>(<span class="hljs-string">"Current loop counter = {}"</span>,
loop_counter);
<span class="hljs-keyword">if</span> loop_counter > i
{
<span class="hljs-keyword">return</span>;
}
loop_counter += <span class="hljs-number">1</span>;
}
}
Auch Rust bietet das aus sehr vielen Sprachen bekannte for-Statement, das wie folgt genutzt wird:
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">for_statement</span></span>(i: <span class="hljs-keyword">i32</span>)
{
<span class="hljs-keyword">for</span> n <span class="hljs-keyword">in</span> <span class="hljs-number">0</span>..i
{
<span class="hljs-built_in">println!</span>(<span class="hljs-string">"Current loop counter = {}"</span>, n);
}
}
In diesem Code wird die Schleife von 0 bis zum übergebenen Wert von i durchlaufen, wobei der Wert i exkludiert ist. Rufen Sie also die Funktion mit dem Wert 10 auf, wird die Schleife mit den Werten 0 bis 9 durchlaufen.Das for-Statement erwartet hierbei einen sogenannten Iterator als Input, dessen Ergebnis definiert, wie oft die for-Schleife durchlaufen werden soll. Dadurch können Sie die for-Schleife auf jede Variable anwenden, die einen Iterator implementiert.In einem späteren Artikel werden Sie sehen, wie Sie eigene Iteratoren für benutzerdefinierte Datentypen implementieren können. Listing 3 zeigt einige Beispiele für for-Schleifen.
Listing 3: for-Schleifen
fn various_for_statements()<br/>{<br/> // <span class="hljs-string">#0</span>, <span class="hljs-string">#1</span>, <span class="hljs-string">#2</span>, <span class="hljs-string">#3</span>, <span class="hljs-string">#4</span><br/> println!(<span class="hljs-string">""</span>);<br/> <span class="hljs-keyword">for</span> n <span class="hljs-keyword">in</span> <span class="hljs-number">0</span>..<span class="hljs-number">5</span><br/> {<br/> println!(<span class="hljs-string">"#{}"</span>, n);<br/> }<br/> // <span class="hljs-string">#0</span>, <span class="hljs-string">#1</span>, <span class="hljs-string">#2</span>, <span class="hljs-string">#3</span>, <span class="hljs-string">#4</span>, <span class="hljs-string">#5</span><br/> println!(<span class="hljs-string">""</span>);<br/> <span class="hljs-keyword">for</span> n <span class="hljs-keyword">in</span> <span class="hljs-number">0</span>..=<span class="hljs-number">5</span><br/> {<br/> println!(<span class="hljs-string">"#{}"</span>, n);<br/> }<br/> // <span class="hljs-string">#0</span>, <span class="hljs-string">#1</span>, <span class="hljs-string">#2</span>, <span class="hljs-string">#3</span>, <span class="hljs-string">#4</span><br/> println!(<span class="hljs-string">""</span>);<br/> let array = [<span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>, <span class="hljs-number">4</span>];<br/> <span class="hljs-keyword">for</span> n <span class="hljs-keyword">in</span> array<br/> {<br/> println!(<span class="hljs-string">"#{}"</span>, n);<br/> }<br/> // <span class="hljs-string">"SQL Server"</span>, <span class="hljs-string">"Oracle"</span>, <span class="hljs-string">"MySQL"</span><br/> println!(<span class="hljs-string">""</span>);<br/> let array = [<span class="hljs-string">"SQL Server"</span>, <span class="hljs-string">"Oracle"</span>, <span class="hljs-string">"MySQL"</span>];<br/> <span class="hljs-keyword">for</span> n <span class="hljs-keyword">in</span> array<br/> {<br/> println!(<span class="hljs-string">"{}"</span>, n);<br/> }<br/> // <span class="hljs-string">#1</span>, <span class="hljs-string">#2</span>, <span class="hljs-string">#3</span><br/> println!(<span class="hljs-string">""</span>);<br/> let vector = vec![<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>];<br/> <span class="hljs-keyword">for</span> n <span class="hljs-keyword">in</span> vector<br/> {<br/> println!(<span class="hljs-string">"#{}"</span>, n);<br/> }<br/>}
Das while-Statement bietet eine weitere Möglichkeit zur Implementierung von Schleifen in Rust. Hier wird die Abbruchbedingung am Beginn der Schleife erwartet. Abbruchbedingungen am Ende der Schleife (do ... while) werden von Rust nicht unterstützt. Listing 4 zeigt ein Beispiel einer while-Schleife.
Listing 4: while-Schleifen
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">while_statement</span></span>(i: <span class="hljs-keyword">i32</span>)<br/>{<br/> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> loop_counter = <span class="hljs-number">0</span>;<br/> <span class="hljs-keyword">while</span> loop_counter &lt; i<br/> {<br/> <span class="hljs-built_in">println!</span>(<span class="hljs-string">"#{}"</span>, loop_counter);<br/> loop_counter += <span class="hljs-number">1</span>;<br/> }<br/>}
Das while-Statement erlaubt die Befehle break und continue. Mit break lässt sich die komplette Schleife sofort abbrechen und der Programmlauf mit dem nächsten Befehl außerhalb der Schleife fortsetzen. Mit continue wird der aktuelle Schleifendurchlauf abgebrochen und der Programmlauf mit dem nächsten Schleifendurchlauf fortgeführt (Listing 5).
Listing 5: while mit break und continue
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">while_break_continue_statement</span></span>(i: <span class="hljs-keyword">i32</span>)<br/>{<br/> <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> loop_counter = <span class="hljs-number">0</span>;<br/> <span class="hljs-keyword">while</span> loop_counter &lt; i<br/> {<br/> <span class="hljs-keyword">if</span> loop_counter % <span class="hljs-number">2</span> == <span class="hljs-number">0</span><br/> {<br/> loop_counter += <span class="hljs-number">1</span>;<br/> <span class="hljs-keyword">continue</span>;<br/> }<br/> <span class="hljs-built_in">println!</span>(<span class="hljs-string">"#{}"</span>, loop_counter);<br/> loop_counter += <span class="hljs-number">1</span>;<br/> <span class="hljs-keyword">if</span> loop_counter &gt; <span class="hljs-number">10</span><br/> {<br/> <span class="hljs-keyword">break</span>;<br/> }<br/> }<br/>}
Expressions
Auf den ersten Blick sieht Rust-Code dem in C/C++ und C# geschriebenen Code zum Verwechseln ähnlich. Bis auf einige Feinheiten in der Syntax können Sie alle bis jetzt gezeigten Beispiele ohne Probleme in einer der C-Programmiersprachen implementieren. Bei Expressions werden die Unterschiede jedoch größer. C-Sprachen trennen strikt zwischen Statements und Expressions. Ein Statement kann zum Beispiel eine einfache for-Schleife sein:
<span class="hljs-keyword">for</span> (int <span class="hljs-built_in">i</span> = <span class="hljs-number">0</span>; <span class="hljs-built_in">i</span>< <span class="hljs-number">5</span>; <span class="hljs-built_in">i</span>++) { ... }
Bei einer Expression handelt es sich um einen Ausdruck, der immer einen entsprechenden Wert produziert, etwa so:
<span class="hljs-attribute">float miles</span> = kilometers / 1.6;
Bei Rust handelt es sich im Gegensatz zu den C-Sprachen um eine sogenannte Expression Language, das heißt, dass auch Statements entsprechende Werte produzieren können. In Rust können zum Beispiel das if- und das match-Statement Werte produzieren. Mehr über das match-Statement erfahren Sie weiter unten.Sehen Sie sich zunächst das Beispiel in Listing 6 an. Darin wird die Variable fast_runner auf Basis eines if-Statements initialisiert. Ist die übergebene Geschwindigkeit größer als zwölf Kilometer pro Stunde, wird die Variable mit true initialisiert, ansonsten mit false.
Listing 6: if-Statement mit Expression
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">if_expression</span></span>(speed_in_kmh: <span class="hljs-keyword">f32</span>)<br/>{<br/> <span class="hljs-keyword">let</span> fast_runner = <br/> <span class="hljs-keyword">if</span> speed_in_kmh &gt; <span class="hljs-number">12.0</span><br/> {<br/> <span class="hljs-literal">true</span><br/> }<br/> <span class="hljs-keyword">else</span><br/> {<br/> <span class="hljs-literal">false</span><br/> };<br/> <span class="hljs-built_in">println!</span>(<span class="hljs-string">"Are you a fast runner with a speed of </span><br/><span class="hljs-string"> {} km/h? {}"</span>, speed_in_kmh, fast_runner);<br/>}
Wie Sie auch erkennen können, werden am Ende der Werte true beziehungsweise false keine Strichpunkte gesetzt, sondern erst am Ende der letzten geschwungenen Klammer, die zum if-Statement gehört, steht ein Semikolon.Mithilfe dieser Syntax sind Sie in der Lage, Variablen auf Basis eines einzelnen Statements zu initialisieren. In anderen Programmiersprachen erfordert dies zwei Schritte: Zunächst müssen Sie die Variable deklarieren und diese danach auf Basis einer Bedingung initialisieren. In Rust können Sie beide Vorgänge kombinieren.In Rust lassen sich zudem mehrere Statements mit geschwungenen Klammern in einen Code-Block zusammenfassen, wobei das letzte Statement im Block einen Wert produzieren und einer Variablen zugewiesen werden kann (ohne Terminierung durch einen Strichpunkt). Listing 7 zeigt ein einfaches Beispiel dazu. Darin wird die Variable triathlete auf Basis eines Code-Blocks initialisiert. Der Inhalt des Code-Blocks wird in geschwungenen Klammern definiert, der Funktionsaufrufe beinhalten kann.
Listing 7: Code-Blöcke und Expressions
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">code_blocks_expressions</span></span>()<br/>{<br/> <span class="hljs-keyword">let</span> triathlete = <br/> {<br/> do_swimming();<br/> do_biking();<br/> do_running();<br/> do_sleeping();<br/> <span class="hljs-literal">true</span><br/> };<br/> <span class="hljs-built_in">println!</span>(<span class="hljs-string">"Are you now a triathlete? {}"</span>, <br/> triathlete);<br/>}
Das letzte Statement beziehungsweise die letzte Expression innerhalb des Code-Blocks ist der Rückgabewert, der die Variable initialisiert. Diese/s Statement/Expression darf wiederum nicht durch einen Strichpunkt abgeschlossen werden, da es/sie den Rückgabewert des Code-Blocks darstellt.Eine weitere Besonderheit in Rust ist das match-Statement, das eine Vereinfachung eines geschachtelten if-else-Statements darstellt:
<span class="hljs-keyword">if</span> condition1 { <span class="hljs-params">...</span> }
<span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> condition2 { <span class="hljs-params">...</span> }
<span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> condition3 { <span class="hljs-params">...</span> }
<span class="hljs-keyword">else</span> { <span class="hljs-params">...</span> }
Ein solches Statement können Sie über das match-Statement flexibler und eleganter schreiben. Listing 8 zeigt ein Beispiel. Wie Sie im Listing sehen, entspricht das match-Statement dem switch-Statement in anderen Programmiersprachen. Eine Besonderheit von Rust ist hier aber, dass Sie den Default-Zweig immer angeben müssen. Dieser wird in Rust durch den Unterstrich (_), ein sogenanntes Wildcard Pattern, dargestellt.
Listing 8: match Statement
fn match_statement(speed_in_kmh: i32)<br/>{<br/> <span class="hljs-keyword">let</span> message = <br/> {<br/> <span class="hljs-keyword">match</span> speed_in_kmh<br/> {<br/> <span class="hljs-number">0.</span>.=<span class="hljs-number">9</span> =&gt; <span class="hljs-string">"slow"</span>,<br/> <span class="hljs-number">10.</span>.=<span class="hljs-number">12</span> =&gt; <span class="hljs-string">"average"</span>,<br/> <span class="hljs-number">13.</span>.=<span class="hljs-number">15</span> =&gt; <span class="hljs-string">"fast"</span>,<br/> <span class="hljs-number">16.</span>. =&gt; <span class="hljs-string">"ultra fast"</span>,<br/> <span class="hljs-keyword">_</span> =&gt; <span class="hljs-string">""</span><br/> }<br/> };<br/> println!(<span class="hljs-string">"You are a {} runner"</span>, message);<br/>}
Bei der Prüfung der Integer-Intervalle im match-Statement aus Listing 8 sind noch einige Besonderheiten zu erwähnen:Das Gleichheitszeichen (=) vor der Ziffer (zum Beispiel bei „=9“) bedeutet, dass es sich um ein Inklusiv-Intervall handelt, das heißt, dass der angegebene Wert im Intervall ebenfalls enthalten ist. Werden im match-Statement Überlappungen festgestellt, generiert der Rust-Compiler eine entsprechende Warnung.Eine weitere Besonderheit von Rust ist der Datentyp Option<T>, der eine Enumeration mit einem generischen Parameter darstellt (Bild 4). Diese Enumeration kann zwei Ausprägungen aufweisen:

Ein Beispiel für eine Option-Enumeration (Bild 4)
Autor
- None wird verwendet, um anzugeben, dass ein Fehler aufgetreten ist.
- Some(T): wird verwendet, um das Ergebnis vom Datentyp <T> zurückzuliefern, wenn kein Fehler aufgetreten ist.
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">div_with_option_datatype</span></span>(
x: <span class="hljs-keyword">i32</span>, y: <span class="hljs-keyword">i32</span>) -> <span class="hljs-built_in">Option</span><<span class="hljs-keyword">i32</span>>
{
<span class="hljs-keyword">if</span> y == <span class="hljs-number">0</span> {
<span class="hljs-literal">None</span>
}
<span class="hljs-keyword">else</span> {
<span class="hljs-literal">Some</span>(x / y)
}
}
Wird als Divisor der Wert 0 übergeben, liefert die Funktion Option<i32>.None als Ergebnis zurück, ansonsten einen Wert vom Typ Option<i32>.Some. Rufen Sie diese Funktion auf, müssen Sie freilich prüfen, welcher der beiden möglichen Enum-Werte zurückgegeben wurde.Dafür lässt sich entweder ein if-else-Statement oder das match-Statement einsetzen – oder aber eine besondere Abkürzung nehmen, die Rust bereit hält: das Statement if let. Hier ein Beispiel für den Aufruf obiger Funktion mit if let:
<span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-literal">Some</span>(x) =
div_with_option_datatype(<span class="hljs-number">10</span>, <span class="hljs-number">2</span>)
{
<span class="hljs-built_in">println!</span>(<span class="hljs-string">"10 / 2 = {}"</span>, x);
}
<span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-literal">None</span> = div_with_option_datatype(<span class="hljs-number">10</span>, <span class="hljs-number">0</span>)
{
<span class="hljs-built_in">println!</span>(<span class="hljs-string">"Division by zero!"</span>);
}
Wie Sie sehen, lässt sich der Code mit if let kürzer und eleganter als ein vollständiges match-Statement verwenden. Dieses leistungsfähige Konstrukt werden Sie in Rust sehr oft antreffen.
Das Ownership-Konzept
Nachdem Sie nun bereits einen guten Überblick über die Syntax von Rust haben, möchte ich noch auf das sogenannte Ownership-Konzept zu sprechen kommen. Dabei handelt es sich um eine Möglichkeit des Memory-Managements, die in anderen Programmiersprachen nicht zur Anwendung kommt. Hinsichtlich des Memory-Managements gibt in Programmiersprachen zwei extreme Ausprägungen:- In C/C++ Code ist der Entwickler selbst für das Memory-Management verantwortlich. Er muss Speicher selbst allokieren und wieder freigeben (über malloc() und free() in C beziehungsweise über die beiden Operatoren new und delete in C++).
- In Managed-Code-Umgebungen wie zum Beispiel dem .NET Framework oder der Java VM wird das Memory-Management von einem Garbage Collector durchgeführt, der nicht verwendeten Speicher periodisch ermittelt und automatisch freigibt.
- Jeder Wert in Rust gehört zu einer Variablen. Diese wird als Owner (Besitzer) bezeichnet.
- Es darf zu jedem Zeitpunkt immer nur einen Owner für eine Variable geben.
Listing 9: Gültigkeitsbereiche
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">variable_scopes</span></span>()<br/>{<br/> {<br/> <span class="hljs-keyword">let</span> message = <span class="hljs-built_in">String</span>::from(<span class="hljs-string">"Hello World!"</span>);<br/> <span class="hljs-built_in">println!</span>(<span class="hljs-string">"{}"</span>, message);<br/> } <br/> <span class="hljs-comment">// The allocated heap memory is released here!</span><br/><span class="hljs-comment"> // We can't reference the variable</span><br/><span class="hljs-comment"> // outside of its scope</span><br/><span class="hljs-comment"> // println!("{}", message);</span><br/><span class="hljs-comment">}</span>
Daraus folgt, dass der Speicherbereich für die Variable message automatisch auf dem Heap freigegeben wird, wenn die Variable ihren Gültigkeitsbereich verlässt. Dadurch kann zu einem späteren Zeitpunkt nicht mehr auf den Inhalt der Variablen zugegriffen werden.In C/C++ Code müssten Sie sich um die Freigabe des Speichers selbst kümmern, da Sie ansonsten ein Memory Leak im Code hätten. In .NET würde der Speicherbereich zu einem späteren Zeitpunkt durch den Garbage Collector freigegeben, dessen Aufruf sich jedoch negativ auf die Performance der Anwendung auswirkt.Um explizit Speicher auf dem Heap allokieren zu können, bietet Rust den Datentyp Box<T> an, der einen Zeiger auf einen Wert vom Typ T darstellt, welcher auf dem Heap allokiert wurde. Sobald die Variable des Datentyps Box<T> ihren Gültigkeitsbereich verlässt, wird der Speicher auf dem Heap automatisch freigegeben. Listing 10 zeigt ein Beispiel dafür.
Listing 10: Box<T>
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">box_t_datatype</span></span>()<br/>{<br/> <span class="hljs-comment">// Allocate some memory on the heap</span><br/><span class="hljs-comment"> let integer_value = Box::new(42);</span><br/><span class="hljs-comment"> let point_tuple = Box::new((5, 4));</span><br/><span class="hljs-comment"> println!("The integer value {} was allocated on </span><br/><span class="hljs-comment"> the heap.", integer_value);</span><br/><span class="hljs-comment"> println!("The tuple with the values {} and {} </span><br/><span class="hljs-comment"> was also allocated on the heap.", </span><br/><span class="hljs-comment"> point_tuple.0,</span><br/><span class="hljs-comment"> point_tuple.1);</span><br/><span class="hljs-comment">} </span><br/><span class="hljs-comment">// The heap memory is released here!</span>
Bei den aufgelisteten Regeln wurde erwähnt, dass jeder Wert genau zu einer Variablen gehört. Aus dieser Regel ergeben sich nun interessante Seiteneffekte in Rust. Stellen Sie sich zum Beispiel vor, dass Sie eine Variable auf dem Heap allokieren und diese Variable anschließend einer weiteren Variablen zuweisen:
let s = String::from("Hello World!");
let t = s;
In C-Sprachen würde den Variablen s und t einfach die Hauptspeicheradresse zugewiesen werden, auf welcher der String allokiert wurde. Daraus folgt, dass die beiden Variablen s und t jeweils einen Zeiger auf die Hauptspeicheradresse besitzen.Würden Sie in weiterer Folge den allokierten Hauptspeicher der Variable s auf dem Heap freigeben, hätte die Variable t einen ungültigen Zeiger, der bei einem etwaigen späteren Zugriff auf t zwangsläufig zu einem Absturz des Programms führen würde.Um dieses Szenario zu vermeiden, wählt Rust einen komplett anderen Ansatz. Weisen Sie der Variablen t die String-Referenz der Variablen s zu, wird diese Referenz von der Variablen s zur Variablen t verschoben. Die Variable s beinhaltet keinen Wert/keine Referenz mehr, und daher kann auf diese Variable auch nicht mehr zugegriffen werden. Dieses Konzept wird bei Rust als Move bezeichnet. Hier ein Beispiel für Moves:
fn moves()
{
// Allocate a string on the heap+
let s = String::from("Hello World");
// “Move” the reference into the variable "t"
// The variable "s" has afterwards no
// reference anymore, and can’t be used
// anymore
let t = s;
println!("{}", t);
// The following statement generates
// a compiler error
// println!("{}", s);
}
Entfernen Sie die Kommentarzeichen vor der letzten println-Zeile im Code, generiert der Rust-Compiler nach der Move-Operation einen Fehler, wenn Sie auf die „leere“ Variable zugreifen möchten. Rust stellt dadurch sicher, dass jede Variable zu jedem Zeitpunkt nur einen Owner (Besitzer) hat.Solche Move-Operationen treten in Rust aber nicht nur bei der Initialisierung von Variablen auf, sondern in einer Vielzahl unterschiedlicher Szenarien. Allokieren Sie zum Beispiel in einer Funktion einen Wert auf dem Heap und liefern diesen als Funktionsergebnis zurück, wird die Ownership dieses Wertes entsprechend übertragen.Übergeben Sie Werte, die auf dem Heap allokiert wurden, einer Funktion als Parameter, wird die Ownership dieser Werte an die Funktionsparameter übertragen. Listing 11 zeigt ein einfaches Beispiel. Darin wird ein neuer Vektor über den Funktionsaufruf Vec::new() auf dem Heap allokiert; anschließend werden dem Vektor ein paar Werte hinzugefügt. Danach wird der Inhalt des Vektors durch den Aufruf der Funktion print_vector() ausgegeben.
Listing 11: Moves mit Funktionsaufrufen
fn ownership_moves_function_calls()<br/>{<br/> let mut databases = Vec::new();<br/> databases.push("SQL Server");<br/> databases.push("Oracle");<br/> databases.push("MySQL");<br/> // The ownership of the variable "databases" <br/> // is passed into the function.<br/> // It has no value after the function call <br/> // anymore<br/> print_vector(databases);<br/> // The following statement generates a <br/> // compiler error<br/> // println!("Length of databases: {}", <br/> // databases.len());<br/>}<br/>fn print_vector(string_vector: Vec&lt;&amp;str&gt;)<br/>{<br/> for string in string_vector<br/> {<br/> println!("{}", string);<br/> }<br/>}
Da hier jedoch keine Referenzen verwendet werden, wird einfach die Ownership der Variablen databases an die Variable string_vector der Funktion print_vector() übertragen. Daraus resultiert, dass die Variable databases nach dem Funktionsaufruf keinen Wert mehr besitzt und jeder weitere Zugriff darauf zu einer Fehlermeldung führt.Dadurch, dass eine Variable in Rust zu jedem Zeitpunkt immer nur einen einzigen Owner haben kann, werden auch Race Conditions in Multithreading-Anwendungen automatisch vermieden, indem sich der gefährdete Code erst gar nicht erfolgreich kompilieren lässt.Dieses Verhalten ist in Rust fest eingebaut und für die Systementwicklung extrem wichtig.
Fazit
In dieser zweiten Folge des Rust-Kurses haben Sie einen Überblick über Funktionen, Kontrollstrukturen und Expressions erhalten. Außerdem wurde ein allererster Blick auf das Ownership-Konzept von Rust geworfen.In der nächsten Folge des Kurses lernen Sie weitere Feinheiten des Ownership-Konzeptes kennen, indem Sie das sogenannte Borrowing-Konzept sowie Referenzen einsetzen. Außerdem werden die sogenannten Lifetime Annotations von Rust vorgestellt.Fussnoten
- Klaus Aschenbrenner, Die ersten Schritte, Rust-Kurs, Teil 1, dotnetpro 3/2024, Seite 106 ff., http://www.dotnetpro.de/A2403Rust