hacktricks/todo/rust-basics.md
2024-02-10 15:36:32 +00:00

20 KiB

Rust Grundlagen

Generische Typen

Erstellen Sie eine Struktur, bei der einer ihrer Werte jeden beliebigen Typ haben kann.

struct Wrapper<T> {
value: T,
}

impl<T> Wrapper<T> {
pub fn new(value: T) -> Self {
Wrapper { value }
}
}

Wrapper::new(42).value
Wrapper::new("Foo").value, "Foo"

Option, Some & None

Der Option-Typ bedeutet, dass der Wert entweder vom Typ Some (es gibt etwas) oder None sein kann:

pub enum Option<T> {
None,
Some(T),
}

Du kannst Funktionen wie is_some() oder is_none() verwenden, um den Wert der Option zu überprüfen.

Makros

Makros sind mächtiger als Funktionen, da sie sich erweitern, um mehr Code zu erzeugen als der Code, den du manuell geschrieben hast. Zum Beispiel muss eine Funktionssignatur die Anzahl und den Typ der Parameter angeben, die die Funktion hat. Makros hingegen können eine variable Anzahl von Parametern entgegennehmen: Wir können println!("hello") mit einem Argument aufrufen oder println!("hello {}", name) mit zwei Argumenten. Außerdem werden Makros vor der Interpretation des Codes durch den Compiler erweitert, sodass ein Makro beispielsweise ein Trait für einen bestimmten Typ implementieren kann. Eine Funktion kann das nicht, da sie zur Laufzeit aufgerufen wird und ein Trait zur Kompilierungszeit implementiert werden muss.

macro_rules! my_macro {
() => {
println!("Check out my macro!");
};
($val:expr) => {
println!("Look at this other macro: {}", $val);
}
}
fn main() {
my_macro!();
my_macro!(7777);
}

// Export a macro from a module
mod macros {
#[macro_export]
macro_rules! my_macro {
() => {
println!("Check out my macro!");
};
}
}

Iterieren

In Rust gibt es verschiedene Möglichkeiten, um über eine Sammlung von Elementen zu iterieren. Das for-Schleifenkonstrukt ist eine der häufigsten Methoden, um dies zu tun. Es kann verwendet werden, um über Arrays, Vektoren, Slices und andere Sammlungen zu iterieren.

let numbers = [1, 2, 3, 4, 5];

for number in numbers.iter() {
    println!("Number: {}", number);
}

Dieser Code iteriert über das Array numbers und gibt jedes Element aus. Die iter()-Methode wird auf dem Array aufgerufen, um einen Iterator zu erhalten, der über die Elemente des Arrays läuft.

Eine andere Möglichkeit, über eine Sammlung zu iterieren, besteht darin, die enumerate()-Methode zu verwenden. Diese Methode gibt ein Tupel zurück, das den Index und das Element enthält.

let numbers = [1, 2, 3, 4, 5];

for (index, number) in numbers.iter().enumerate() {
    println!("Index: {}, Number: {}", index, number);
}

Dieser Code gibt sowohl den Index als auch das Element jedes Elements im Array numbers aus.

Es gibt auch andere Iteratormethoden wie map(), filter() und fold(), die verwendet werden können, um Transformationen und Filterungen auf den Elementen einer Sammlung durchzuführen.

let numbers = [1, 2, 3, 4, 5];

let doubled_numbers: Vec<i32> = numbers.iter().map(|x| x * 2).collect();

println!("Doubled numbers: {:?}", doubled_numbers);

Dieser Code verdoppelt jedes Element im Array numbers und speichert die verdoppelten Zahlen in einem Vektor. Der map()-Operator wird verwendet, um die Verdopplung durchzuführen, und die collect()-Methode wird verwendet, um die Ergebnisse in einem Vektor zu sammeln.

Die Iteration über eine Sammlung in Rust ist einfach und flexibel, und die verschiedenen Iteratormethoden bieten eine Vielzahl von Möglichkeiten zur Transformation und Filterung von Daten.

// Iterate through a vector
let my_fav_fruits = vec!["banana", "raspberry"];
let mut my_iterable_fav_fruits = my_fav_fruits.iter();
assert_eq!(my_iterable_fav_fruits.next(), Some(&"banana"));
assert_eq!(my_iterable_fav_fruits.next(), Some(&"raspberry"));
assert_eq!(my_iterable_fav_fruits.next(), None); // When it's over, it's none

// One line iteration with action
my_fav_fruits.iter().map(|x| capitalize_first(x)).collect()

// Hashmap iteration
for (key, hashvalue) in &*map {
for key in map.keys() {
for value in map.values() {

Rekursive Box

Eine rekursive Box ist eine Technik, bei der eine Box in einer Box verschachtelt wird, um eine Kette von Aktionen auszuführen. Dies wird oft verwendet, um komplexe Aufgaben zu automatisieren oder um wiederholte Aktionen auf mehreren Ebenen durchzuführen.

Die rekursive Box kann verwendet werden, um eine Reihe von Befehlen oder Aktionen auszuführen, indem sie sich selbst aufruft. Dies ermöglicht es, dass eine Aktion wiederholt ausgeführt wird, bis eine bestimmte Bedingung erfüllt ist oder ein bestimmtes Ergebnis erzielt wird.

Die rekursive Box kann auch verwendet werden, um eine Hierarchie von Aktionen zu erstellen, bei der jede Aktion eine andere Aktion aufruft. Dies ermöglicht es, komplexe Aufgaben in kleinere, leichter zu handhabende Schritte zu unterteilen.

Die Verwendung einer rekursiven Box erfordert ein gutes Verständnis der Programmierung und der Logik, um sicherzustellen, dass die rekursive Funktion ordnungsgemäß funktioniert und nicht in einer Endlosschleife stecken bleibt.

Es ist wichtig, die Abbruchbedingung sorgfältig zu definieren, um sicherzustellen, dass die rekursive Box nicht unendlich weiterläuft. Eine schlecht definierte Abbruchbedingung kann zu einem Absturz des Systems oder zu unerwünschten Ergebnissen führen.

Die rekursive Box ist eine leistungsstarke Technik, die in verschiedenen Bereichen der Softwareentwicklung und des Hackings eingesetzt werden kann. Sie ermöglicht die Automatisierung von Aufgaben und die effiziente Verarbeitung von Daten.

enum List {
Cons(i32, List),
Nil,
}

let list = Cons(1, Cons(2, Cons(3, Nil)));

if

Die if-Anweisung wird verwendet, um eine Bedingung zu überprüfen und Code auszuführen, wenn die Bedingung wahr ist. Der Code innerhalb der if-Anweisung wird nur ausgeführt, wenn die Bedingung erfüllt ist.

Die Syntax für die if-Anweisung in Rust ist wie folgt:

if Bedingung {
    // Code, der ausgeführt wird, wenn die Bedingung wahr ist
}

Hier ist ein Beispiel:

fn main() {
    let zahl = 5;

    if zahl > 0 {
        println!("Die Zahl ist positiv");
    }
}

In diesem Beispiel wird der Code innerhalb der if-Anweisung nur ausgeführt, wenn die Variable zahl größer als 0 ist.

let n = 5;
if n < 0 {
print!("{} is negative", n);
} else if n > 0 {
print!("{} is positive", n);
} else {
print!("{} is zero", n);
}

Übereinstimmung

Die match-Anweisung in Rust ermöglicht es, einen Wert mit verschiedenen Mustern zu vergleichen und entsprechende Aktionen auszuführen, basierend auf dem übereinstimmenden Muster. Es ähnelt dem switch-Statement in anderen Programmiersprachen.

Die Syntax für match sieht folgendermaßen aus:

match Wert {
    Muster1 => {
        // Aktionen für Muster1
    },
    Muster2 => {
        // Aktionen für Muster2
    },
    // Weitere Muster und Aktionen
    _ => {
        // Standardaktion, wenn kein Muster übereinstimmt
    }
}

Hier sind einige wichtige Punkte zu beachten:

  • Jedes Muster kann entweder ein Wert, eine Variable oder ein Platzhalter sein.
  • Das _-Muster wird als Platzhalter verwendet, um alle anderen Fälle abzudecken, die nicht explizit angegeben sind.
  • Die Aktionen für jedes Muster werden in geschweiften Klammern {} definiert.
  • Nachdem eine Aktion ausgeführt wurde, wird die match-Anweisung beendet.

Die match-Anweisung ist eine leistungsstarke Möglichkeit, verschiedene Fälle zu behandeln und den Code lesbarer zu machen. Es ist besonders nützlich, wenn Sie mit Enumerationen arbeiten, da Sie alle möglichen Varianten abdecken können.

match number {
// Match a single value
1 => println!("One!"),
// Match several values
2 | 3 | 5 | 7 | 11 => println!("This is a prime"),
// TODO ^ Try adding 13 to the list of prime values
// Match an inclusive range
13..=19 => println!("A teen"),
// Handle the rest of cases
_ => println!("Ain't special"),
}

let boolean = true;
// Match is an expression too
let binary = match boolean {
// The arms of a match must cover all the possible values
false => 0,
true => 1,
// TODO ^ Try commenting out one of these arms
};

Schleife (unendlich)

Eine unendliche Schleife ist eine Schleife, die ohne eine Bedingung, die sie beendet, immer wieder ausgeführt wird. Dies kann nützlich sein, um bestimmte Aufgaben kontinuierlich auszuführen, wie z.B. das Überwachen von Ereignissen oder das Warten auf bestimmte Bedingungen.

In Rust kann eine unendliche Schleife mit dem Schlüsselwort loop erstellt werden. Der Code innerhalb der Schleife wird immer wieder ausgeführt, bis er explizit unterbrochen wird. Dies kann durch die Verwendung von break erreicht werden, um die Schleife zu verlassen, oder durch die Verwendung von return, um die Funktion zu verlassen, in der sich die Schleife befindet.

Hier ist ein Beispiel für eine unendliche Schleife in Rust:

loop {
    // Code, der immer wieder ausgeführt wird
}

Es ist wichtig, sicherzustellen, dass es eine Möglichkeit gibt, die Schleife zu beenden, da sonst das Programm in einer Endlosschleife stecken bleiben kann.

loop {
count += 1;
if count == 3 {
println!("three");
continue;
}
println!("{}", count);
if count == 5 {
println!("OK, that's enough");
break;
}
}

während

The while statement is used in Rust to create a loop that continues executing as long as a certain condition is true. It is a control flow construct that allows you to repeat a block of code multiple times.

Die while Anweisung wird in Rust verwendet, um eine Schleife zu erstellen, die so lange ausgeführt wird, wie eine bestimmte Bedingung wahr ist. Es handelt sich um eine Kontrollflusskonstruktion, die es ermöglicht, einen Codeblock mehrmals zu wiederholen.

let mut n = 1;
while n < 101 {
if n % 15 == 0 {
println!("fizzbuzz");
} else if n % 5 == 0 {
println!("buzz");
} else {
println!("{}", n);
}
n += 1;
}

für


Variables

Declaration
let x: i32 = 5;
Mutable
let mut y: i32 = 10;
Constants
const Z: i32 = 15;

Data Types

Integer
let a: i8 = 127;
let b: i16 = 32767;
let c: i32 = 2147483647;
let d: i64 = 9223372036854775807;
let e: i128 = 170141183460469231731687303715884105727;
Unsigned Integer
let f: u8 = 255;
let g: u16 = 65535;
let h: u32 = 4294967295;
let i: u64 = 18446744073709551615;
let j: u128 = 340282366920938463463374607431768211455;
Floating Point
let k: f32 = 3.14;
let l: f64 = 3.141592653589793;
Boolean
let m: bool = true;
let n: bool = false;
Character
let o: char = 'a';
String
let p: &str = "Hello, World!";
let q: String = String::from("Hello, World!");

Operators

Arithmetic
let sum = 5 + 3;
let difference = 5 - 3;
let product = 5 * 3;
let quotient = 5 / 3;
let remainder = 5 % 3;
Comparison
let equal = 5 == 3;
let not_equal = 5 != 3;
let greater_than = 5 > 3;
let less_than = 5 < 3;
let greater_than_or_equal = 5 >= 3;
let less_than_or_equal = 5 <= 3;
Logical
let and = true && false;
let or = true || false;
let not = !true;
Assignment
let mut x = 5;
x += 3;
x -= 3;
x *= 3;
x /= 3;
x %= 3;

Control Flow

If-else
let x = 5;

if x > 10 {
    println!("x is greater than 10");
} else if x < 10 {
    println!("x is less than 10");
} else {
    println!("x is equal to 10");
}
Loop
let mut x = 0;

loop {
    println!("x is {}", x);
    x += 1;

    if x == 5 {
        break;
    }
}
While
let mut x = 0;

while x < 5 {
    println!("x is {}", x);
    x += 1;
}
For
let numbers = [1, 2, 3, 4, 5];

for number in numbers.iter() {
    println!("The number is {}", number);
}

Functions

fn add(x: i32, y: i32) -> i32 {
    x + y
}

fn main() {
    let result = add(5, 3);
    println!("The result is {}", result);
}
for n in 1..101 {
if n % 15 == 0 {
println!("fizzbuzz");
} else {
println!("{}", n);
}
}

// Use "..=" to make inclusive both ends
for n in 1..=100 {
if n % 15 == 0 {
println!("fizzbuzz");
} else if n % 3 == 0 {
println!("fizz");
} else if n % 5 == 0 {
println!("buzz");
} else {
println!("{}", n);
}
}

// ITERATIONS

let names = vec!["Bob", "Frank", "Ferris"];
//iter - Doesn't consume the collection
for name in names.iter() {
match name {
&"Ferris" => println!("There is a rustacean among us!"),
_ => println!("Hello {}", name),
}
}
//into_iter - COnsumes the collection
for name in names.into_iter() {
match name {
"Ferris" => println!("There is a rustacean among us!"),
_ => println!("Hello {}", name),
}
}
//iter_mut - This mutably borrows each element of the collection
for name in names.iter_mut() {
*name = match name {
&mut "Ferris" => "There is a rustacean among us!",
_ => "Hello",
}
}

if let

Die if let-Anweisung in Rust ermöglicht es, ein Muster mit einem Wert zu vergleichen und den Codeblock auszuführen, wenn das Muster übereinstimmt. Es ist eine verkürzte Schreibweise für das match-Statement, das nur einen Fall abdeckt.

Die Syntax für if let ist wie folgt:

if let pattern = expression {
    // Codeblock, der ausgeführt wird, wenn das Muster übereinstimmt
}

Hier ist ein Beispiel, um das Konzept zu verdeutlichen:

fn main() {
    let value = Some(5);

    if let Some(x) = value {
        println!("Der Wert ist: {}", x);
    }
}

In diesem Beispiel wird überprüft, ob value den Wert Some enthält. Wenn ja, wird der Wert in x gebunden und der Codeblock innerhalb des if let-Statements wird ausgeführt. In diesem Fall wird "Der Wert ist: 5" gedruckt.

Die if let-Anweisung ist besonders nützlich, wenn Sie nur an einem bestimmten Fall interessiert sind und nicht alle möglichen Fälle abdecken müssen. Es kann den Code lesbarer machen, indem es unnötige Verzweigungen vermeidet.

let optional_word = Some(String::from("rustlings"));
if let word = optional_word {
println!("The word is: {}", word);
} else {
println!("The optional word doesn't contain anything");
}

while let

Die while let-Schleife ist eine spezielle Art der while-Schleife in Rust, die verwendet wird, um eine Schleife auszuführen, solange ein bestimmtes Pattern mit einem Wert übereinstimmt.

Die Syntax für die while let-Schleife ist wie folgt:

while let pattern = expression {
    // Code, der ausgeführt wird, solange das Pattern übereinstimmt
}

Hier ist eine kurze Erklärung, wie die while let-Schleife funktioniert:

  1. Die expression wird ausgewertet.
  2. Wenn das Pattern mit dem Wert der expression übereinstimmt, wird der Code innerhalb der Schleife ausgeführt.
  3. Nachdem der Code ausgeführt wurde, wird die expression erneut ausgewertet.
  4. Wenn das Pattern immer noch mit dem Wert der expression übereinstimmt, wird der Code erneut ausgeführt.
  5. Dieser Vorgang wird wiederholt, bis das Pattern nicht mehr mit dem Wert der expression übereinstimmt.

Die while let-Schleife ist besonders nützlich, wenn Sie eine Schleife ausführen möchten, solange ein bestimmtes Pattern mit einem Wert übereinstimmt, und Sie den Wert innerhalb der Schleife verwenden möchten.

let mut optional = Some(0);
// This reads: "while `let` destructures `optional` into
// `Some(i)`, evaluate the block (`{}`). Else `break`.
while let Some(i) = optional {
if i > 9 {
println!("Greater than 9, quit!");
optional = None;
} else {
println!("`i` is `{:?}`. Try again.", i);
optional = Some(i + 1);
}
// ^ Less rightward drift and doesn't require
// explicitly handling the failing case.
}

Traits

Eine neue Methode für einen Typen erstellen

trait AppendBar {
fn append_bar(self) -> Self;
}

impl AppendBar for String {
fn append_bar(self) -> Self{
format!("{}Bar", self)
}
}

let s = String::from("Foo");
let s = s.append_bar();
println!("s: {}", s);

Tests

Tests sind ein wesentlicher Bestandteil der Softwareentwicklung. Sie dienen dazu, die Funktionalität und Korrektheit des Codes zu überprüfen. In Rust können Tests mit der #[test]-Annotation geschrieben werden. Diese Annotation kennzeichnet eine Funktion als Testfunktion. Um die Tests auszuführen, kann das cargo test-Kommando verwendet werden.

Einfacher Test

Ein einfacher Test in Rust könnte wie folgt aussehen:

#[test]
fn test_addition() {
    assert_eq!(2 + 2, 4);
}

In diesem Beispiel wird die Funktion test_addition als Testfunktion markiert. Die assert_eq!-Makro wird verwendet, um zu überprüfen, ob die Addition von 2 und 2 gleich 4 ergibt. Wenn der Test erfolgreich ist, wird keine Ausgabe erzeugt. Wenn der Test fehlschlägt, wird eine Fehlermeldung angezeigt.

Testergebnisse

Beim Ausführen von Tests gibt Rust Informationen über die Anzahl der Tests, die erfolgreich waren, die fehlgeschlagen sind und die übersprungen wurden. Diese Informationen werden in einer übersichtlichen Zusammenfassung angezeigt.

Testabdeckung

Rust bietet auch die Möglichkeit, die Testabdeckung zu überprüfen. Mit dem --coverage-Flag kann das cargo test-Kommando ausgeführt werden, um Informationen über den Prozentsatz des Codes anzuzeigen, der von den Tests abgedeckt wird.

Testorganisation

Um den Code besser zu organisieren, können Tests in separate Module gruppiert werden. Dies kann mit dem mod-Schlüsselwort erreicht werden. Hier ist ein Beispiel:

mod math {
    #[test]
    fn test_addition() {
        assert_eq!(2 + 2, 4);
    }

    #[test]
    fn test_subtraction() {
        assert_eq!(4 - 2, 2);
    }
}

In diesem Beispiel werden die Tests für die Addition und Subtraktion in einem separaten Modul namens math gruppiert. Dies erleichtert die Organisation und Lesbarkeit des Codes.

Testfixtures

Manchmal ist es erforderlich, vor jedem Test bestimmte Vorbereitungen zu treffen. Dies kann mit Testfixtures erreicht werden. Testfixtures sind Funktionen, die vor oder nach jedem Test ausgeführt werden. Sie können verwendet werden, um beispielsweise Datenbankverbindungen herzustellen oder Testdaten zu initialisieren.

#[test]
fn test_with_fixture() {
    setup();
    // Testcode
    teardown();
}

fn setup() {
    // Vorbereitungen vor dem Test
}

fn teardown() {
    // Aufräumarbeiten nach dem Test
}

In diesem Beispiel wird die Funktion setup vor jedem Test und die Funktion teardown nach jedem Test ausgeführt. Dadurch können spezifische Vorbereitungen und Aufräumarbeiten durchgeführt werden, um sicherzustellen, dass die Tests in einer sauberen Umgebung ausgeführt werden.

Testparameter

Manchmal ist es nützlich, Tests mit verschiedenen Parametern auszuführen. Dies kann mit Testparametern erreicht werden. Testparameter sind Funktionen, die als Parameter an Testfunktionen übergeben werden. Dadurch können Tests mit verschiedenen Eingabewerten ausgeführt werden.

#[test]
#[cfg(test)]
fn test_with_parameter() {
    for param in get_params() {
        assert_eq!(param + 2, 4);
    }
}

fn get_params() -> Vec<i32> {
    vec![2, 3, 4]
}

In diesem Beispiel wird die Funktion get_params verwendet, um eine Liste von Parametern zurückzugeben. Die Testfunktion test_with_parameter wird dann für jeden Parameter in der Liste ausgeführt. Dadurch können Tests mit verschiedenen Eingabewerten durchgeführt werden.

#[cfg(test)]
mod tests {
#[test]
fn you_can_assert() {
assert!(true);
assert_eq!(true, true);
assert_ne!(true, false);
}
}

Threading

Arc

Ein Arc kann Clone verwenden, um weitere Referenzen auf das Objekt zu erstellen und sie an die Threads zu übergeben. Wenn der letzte Referenzzeiger auf einen Wert außerhalb des Gültigkeitsbereichs liegt, wird die Variable verworfen.

use std::sync::Arc;
let apple = Arc::new("the same apple");
for _ in 0..10 {
let apple = Arc::clone(&apple);
thread::spawn(move || {
println!("{:?}", apple);
});
}

Threads

In diesem Fall übergeben wir dem Thread eine Variable, die er ändern kann.

fn main() {
let status = Arc::new(Mutex::new(JobStatus { jobs_completed: 0 }));
let status_shared = Arc::clone(&status);
thread::spawn(move || {
for _ in 0..10 {
thread::sleep(Duration::from_millis(250));
let mut status = status_shared.lock().unwrap();
status.jobs_completed += 1;
}
});
while status.lock().unwrap().jobs_completed < 10 {
println!("waiting... ");
thread::sleep(Duration::from_millis(500));
}
}