Benutzer-Werkzeuge

Webseiten-Werkzeuge


einfuehrung_in_rust

Dies ist eine alte Version des Dokuments!


Einführung in Rust

Buch: Programmierung sicherer Systeme mit Rust: Eine Einführung - vom 01. April 2020

Die folgenden Beispiele wurden alle mit rustc 1.48.0 auf FreeBSD 13.0 RELEASE getestet:

> rustc -V
rustc 1.48.0

> uname -a
FreeBSD erde.lan 13.0-RELEASE FreeBSD 13.0-RELEASE #6 releng/13.0-n244733-ea31abc261f: Sat Apr 10 08:09:33 CEST 2021     root@erde.lan:/usr/obj/usr/src/amd64.amd64/sys/MYKERNEL  amd64

hier wird die Variable a nicht überschrieben, sondern überdeckt, das nennt sich shadowing:

fn main() {
    let a = 12;
    println!("a = {}", a);
    
    let a = 33;
    println!("a = {}", a);
}

das geschied durch das vorangestellte Schlüsselwort let, denn eine "normale" Variable (ist eine "unveränderbare Variable") kann man in Rust aus Sicherheitsgründen nicht überschrieben werden. Will man eine Variable, die sich verändern läßt, dann benötigt man noch das Schlüsselwort mut:

fn main() {
    let mut a = 12;
    println!("a = {}", a);
    
    a = 33;
    println!("a = {}", a);
}

und dann gibt es noch die "Konstanten".

3,1415926535897932384626433832795

Nur worin besteht der Unterschied zwischen "unveränderbare Variablen" und "Konstanten"? "Konstanten" können nicht mit dem Schlüsselwort mut benutzt werden und im Gegensatz zu "Variablen", muß man ihnen immer einen Typ zuweisen.

fn main() {
    const PI: f32 = 3.1416;
    println!("Pi = {}", PI);
}

"Konstanten" können auch global bekannt gemacht werden:

const PI: f32 = 3.1416;

fn main() {
    println!("Pi = {}", PI);
}

Ausgabe

println! gibt auf stdout aus;

eprintln! gibt auf stderr aus;

siehe auch:

Ausgabe_nach_stderr.txt
use std::io::{self, Write};
 
fn main() -> io::Result<()> {
    let stderr = io::stderr();
    let mut handle = stderr.lock();
 
    handle.write_all(b"Hallo Werlt\n")?;
 
    Ok(())
}

stderr auf stdout umleiten

Wenn man ".output()" statt ".status()" verwendet, dann kann man die Daten aus stderr auslesen.

stderr2stdout.rs
fn main() {
    let ausgabe = std::process::Command::new("ffprobe")
        .arg("-i")
        .arg("Film.mp4")
        .output()
        .expect("ffprobe konnte nicht ausgeführt werden");
    println!("beendet mit {}", ausgabe.status);
    let s = String::from_utf8_lossy(&ausgabe.stderr);
    println!("{}", s);
}
ffprobe_Stream.rs
fn main() {
    let ausgabe = std::process::Command::new("ffprobe")
        .arg("-i")
        .arg("Film.mp4")
        .output()
        .expect("process failed to execute");
    println!("completed with {}", ausgabe.status);
    let s = String::from_utf8_lossy(&ausgabe.stderr);
    for line in s.lines() {
        if line.starts_with("    Stream") {
            println!("{}", &line);
        }
    }
}

Hier werden Daten verarbeitet, die "ffprobe" auf stderr ausgibt:

> rustc ffprobe_Stream.rs
> ./ffprobe_Stream
completed with exit code: 0
    Stream #0:0(eng): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, smpte170m), 500x376 [SAR 376:375 DAR 4:3], 1223 kb/s, 15 fps, 15 tbr, 15360 tbn, 30 tbc (default)

Datentypen

Ganze Zahlen mit Vorzeichen:
  i8, i16, i32, i64, i128, isize

Ganze Zahlen ohne Vorzeichen:
  u8, u16, u32, u64, u128, usize

Dezimalzahlen (Kommazahlen):
  f32, f64

Unicode-Zeichen:
  char

Wahrheitswerte:

Der promitive Typ "Unit" -> Dieser Typ hat genau einen Wert, nämlich das leere Tupel "()".
                            Dieser Wert wird dann benutzt, wenn kein anderer Wert zurückgegeben werden kann.
                            In anderen Programmiersprachen würde man den Wert "()" als "nil" oder "null" wiederfinden.
isize = -9223372036854775808..9223372036854775807
usize =                    0..18446744073709551615

Will man zwei Zahlen miteinander verarbeiten (vergleichen oder addieren), dann geht das nur, wenn beide Zahlen vom gleichen Typ sind. Deshalb bietet es sich an immer den größten Typ zu verwenden (außer man will speicherplatz sparen oder ein kleinwenig mehr Geschwindigkeit erzielen), und diesen dann im ganzen Code für alle Zahlen, die miteinander in Beziehung stehen, zu verwenden. So kann man sich Typumwandlungen sparen.

Im Bereich der Zahlen, die im Alltag üblich sind, stehen dann diese Datentypen u128, i128 und f64 zur Auswahl. Allerdings sollte man die Unterschiede in ihrer Genauigkeit kennen. u128 und i128 können bis zu 38 Stellen darstellen. f64 kann nur 15 Stellen genau darstellen, ab der 16. Stelle wird gerundet.

Beispielsweise gibt dieses Programm:

fn main() {
    let a: u128 = 99999999999999999999999999999999999999;
    let b: i128 = -99999999999999999999999999999999999999;
    let c: f64  = 9999999999999999.9;
    let d: f64  = 999999999999999.9;
    let e: f64  = 99999999999999.9;
    println!("a = {};\nb = {};\nc = {};\nd = {};\ne = {}", a, b, c, d, e);
}

diese Ausgabe zurück:

a = 99999999999999999999999999999999999999;
b = -99999999999999999999999999999999999999;
c = 10000000000000000;
d = 999999999999999.9;
e = 99999999999999.9

Weiterhin ist anzumerken, dass auf der Doku-Seite von Rust Module std:: steht, dass für die Module i8-i128 und u8-u128 eine Abkündigung geplant ist (Deprecation planned), so dass man als Standard-Zahlen-Typ im Alltag lieber f64 verwenden sollte. 15 Stellen Genauigkeit sollte für die meisten Fälle im Alltag auch ausreichen, das reicht sogar für viele Wissenschaftliche Berechnungen aus.

Arrays und Tupel

Arrays benötigen einen speziellen Platzhalter ({:?} für eine Ausgabe in einer Zeile oder {:#?} für eine Ausgabe in einer Spalte):

fn main() {
    let a = [5, 7, 12, 3241];
    println!("a = {:?}", a);
    println!("a = {:#?}", a);
}
fn main() {
    let a: [i32; 4] = [5, 7, 12, 3241];
    println!("Länge von a = {}", a.len());
    println!("1. Wert = {}", a[0]);
    println!("4. Wert = {}", a[3]);
}
fn main() {
    let a = [5, 7, 12, 3241];
    let [eins, zwei, drei, vier] = a;
    println!("{}, {}, {}, {}", eins, zwei, drei, vier);
    println!("eins={}, zwei{}, drei={}, vier={}", eins, zwei, drei, vier);
}

Tupel können, im Gegensatz zu Arrays, Elemente verschiedenen Datentyps enthalten:

fn main() {
    let t = (5, 7, 12, 3241);
    println!("t = {:?}", t);
    println!("4. Wert = {}", t.3);
}
fn main() {
    let t = (5, false, "Hallo");
    println!("t = {:?}", t);
    println!("3. Wert = {}", t.2);
}
fn main() {
    let t = (5, false, "Hallo");
    let (eins, zwei, drei) = t;
    println!("{}, {}, {}", eins, zwei, drei);
}

Slice

Der Slice-Typ

Ein anderer Datentyp, der keinen Besitz hat, ist das Slice. Mit Slice können Sie auf eine zusammenhängende Folge von Elementen in einer Sammlung verweisen, nicht auf die gesamte Sammlung.

test1.rs
fn main() {
    let vector = vec![1, 2, 3, 4, 5, 6, 7, 8];
    let slice = &vector[3..6];
    println!("length of slice: {}", slice.len()); // 3
    println!("slice: {:?}", slice); // [4, 5, 6]
}
test2.rs
fn string_slice(arg: &str) {
    println!("{}", arg);
}
 
fn string(arg: String) {
    println!("{}", arg);
}
 
fn main() {
    //string("blue");
    string_slice("blue");
 
    string("red".to_string());
    //string_slice("red".to_string());
 
    string(String::from("hi"));
    //string_slice(String::from("hi"));
 
    string("rust is fun!".to_owned());
    //string_slice("rust is fun!".to_owned());
 
    string("nice weather".into());
    string_slice("nice weather".into());
 
    string(format!("Interpolation {}", "Station"));
    //string_slice(format!("Interpolation {}", "Station"));
 
    //string(&String::from("abc")[0..1]);
    string_slice(&String::from("abc")[0..1]);
 
    //string("  hello there ".trim());
    string_slice("  hello there ".trim());
 
    string("Happy Monday!".to_string().replace("Mon", "Tues"));
    //string_slice("Happy Monday!".to_string().replace("Mon", "Tues"));
 
    string("mY sHiFt KeY iS sTiCkY".to_lowercase());
    //string_slice("mY sHiFt KeY iS sTiCkY".to_lowercase());
}

Dateien anlegen, lesen, schreiben, überschreiben und löschen

Dateien öffnen und schließen in Rust

Rusts Kernfeature ist das sogenannte Ownership. Es beschreibt die Idee, dass jeder Datentyp gleichzeitig eine Resource darstellt. Rust löst das darüber, dass es jedem Stück Daten einen sogenannten Owner zuweist. Erstelle ich mit File::create eine Datei, bekomme ich im Erfolgsfall nicht nur ein File-Objekt zurück, sondern auch die Verantwortung des Managements. Ich besitze die Datei und bin der einzige Besitzer. Gebe ich den Besitz auf irgendeine Art auf, wird die Datei geschlossen. Daten in Rust haben also einen modellierten Lebenszyklus. Im Gegensatz zu vielen anderen Sprachen ist dieses Konzept keine Konvention, sondern einer der Grundpfeiler des Typsystems. Deswegen begegnet es einem auch sofort und muss gelernt werden - es führt kein Weg daran vorbei.

test_golem_1.rs
use std::fs::File;
use std::io::Write;
use std::io;
 
fn main() -> Result<(), io::Error> {
    let open_file = File::create("hello_golem.txt");
    match open_file {
        Ok(mut file) => {
            file.write_all(b"Hallo Golem!\n")
            // hier wird die Datei aufgegeben und damit geschlossen
        }
        Err(e) => {
            eprintln!("Datei konnte nicht erstellt werden. Fehler: {}", e);
            Err(e)
        }
    }
}

vector + for

for-Schleife.rs
use std::env;
//let mut aaa: Vec<_>;
 
fn main() {
    let vector: Vec<String> = env::args().collect();
    println!("{:?}", vector);
 
    println!("-----------------------------------");
 
    let l = vector.len();
    println!("Es sind {} Elemente im Vektor.", l);
 
    println!("===================================");
 
    for i in 0..l {
        println!("Das {}. Vektorelement: {}", i, &vector[i]);
    }
}

externe Programme aufrufen

command_1.rs
fn main() {
    std::process::Command::new("ls")
        .arg("-l")
        .arg("-h")
        .arg("-a")
        .status()
        .expect("ls command failed to start");
}
command_2.rs
fn main() {
    use std::process::Command;
 
    Command::new("ls")
        .arg("-l")
        .arg("-h")
        .arg("-a")
        .spawn()
        .expect("ls command failed to start");
}
command_3.rs
fn main() {
    use std::process::Command;
 
    let mut child = Command::new("/bin/hostname")
        .arg("-s")
        .spawn()
        .expect("failed to execute child");
 
    let ecode = child.wait()
        .expect("failed to wait on child");
 
    assert!(ecode.success());
}
command_4.rs
fn main() {
    use std::process::Command;
 
    let mut list_dir = Command::new("ls");
 
    // Execute `ls` in the current directory of the program.
    list_dir.status().expect("process failed to execute");
 
    println!();
 
    // Change `ls` to execute in the root directory.
    list_dir.current_dir("/");
 
    // And then execute `ls` again but in the root directory.
    list_dir.status().expect("process failed to execute");
}

aus den FAQs

    • Ein wichtiges Detail ist, dass der Rückgabetyp einer Funktion, welche mit einem Semikolon endet, () ist. Dies deutet an, dass kein Wert zurückgegeben wird. Implizite Rückgaben funktionieren nur ohne abschließendes Semikolon, da sonst der Wert des Ausdrucks unterdrückt wird.
/home/http/wiki/data/attic/einfuehrung_in_rust.1638880617.txt · Zuletzt geändert: von manfred