Hinweis: Rust 1.0 Release

Crysis nerd

Freizeitschrauber(in)
Hi Leute!

Heute ist endlich der Tag, an dem die erste "stabile" Version der Programmiersprache Rust veröffentlicht wird. Und ich wollte diese Gelegenheit nutzen, um euch Rust mal kurz vorzustellen! Warum? Einfach nur, weil ich von der Sprache sehr begeistert bin und mir denke, dass einige von euch auch Gefallen daran finden könnten.

Also: Was ist Rust?
"Rust is a systems programming language that runs blazingly fast, prevents nearly all segfaults, and guarantees thread safety. " -- das steht zumindest auf der offiziellen Website. Das hört sich ja schon mal gut an, aber was genau bedeutet es? Man könnte es auch so sagen: Rust möchte C++ ablösen, oder zumindest eine moderne Alternative zu C++ sein. In vielen Anwendungsbereichen ist es (trotz Prophezeiungen) immer noch notwendig, maximale Performance zu haben oder direkt auf die Hardware zuzugreifen. In solchen Fällen soll man in Zukunft neben C++ auch noch zu Rust greifen können.
Aber ich möchte nicht lange reden, ihr wollt sicher alle mal Code sehen. Also das klassische "Hello World":
Code:
fn main() {
    println!("Hello World!");
}
So gut, so langweilig. Naja, was geht hier vor? "fn main()" definiert eine Funktion namens "main", das ist bereits aus vielen anderen Sprachen bekannt. Wichtig ist hier, dass "fn" ein Keyword ist, um Funktionen zu definieren. Man nutzt hier also nicht die C-Form, bei der man den Rückgabewert an den Anfang schreibt. "println!" ist ein Makro... was das ist, ist an dieser Stelle erstmal egal, stellt euch einfach vor, es sein ein Funktionsaufruf. "println" ist Teil der Rust Standardbibliothek... aber wo ist das "#include"/"import"? Braucht man in diesem Fall nicht... die Standardbibliothek hat einen besonderen Bereich namens "Prelude", welcher automatisch importiert wird und somit nutzbar ist. Dort sind aber nur sehr oft verwendete Komponenten drin.

Den Code gibt es auch auf Rust Playground. Dort könnt ihr ihn anpassen und direkt ausführen!

Ein komplexeres Beispiel:
Code:
fn add_seven(number: i32) -> i32 {
    number + 7
}


fn divide_remainder(a: i32, b: i32) -> (i32, i32) {
    (a / b, a % b)
}


fn get_greeting(hour_of_day: i32) -> String {
    match hour_of_day {
        6  ... 11 => "Guten Morgen",
        12 ... 14 => "Mahlzeit",
        21 ... 23 | 0 ... 5 => "Na, so spät noch wach?",
        _ => "Hallo", 
    }.to_string()
}


fn main() {
    println!("5 + 7 is: {}", add_seven(5));


    let (div, remainder) = divide_remainder(13, 3);
    println!("13 / 3 = {}, 13 % 3 = {}", div, remainder);


    // Wir treffen eine Person um eine Uhrzeit (Stunde ist angegeben)
    let persons = vec![("Torben", 8), 
                       ("Willi Wacker", 13),
                       ("Susi Sorglos", 23)];


   for (name, hour) in persons {
    println!("{} {}", get_greeting(hour), name);
   }
}
Playground

Hier passiert einiges. Ganz am Anfang bauen wir uns eine Funktion "add_seven", die auf den Parameter einfach nur 7 addiert und das Ergebnis zurück gibt. Ein paar Dinge fallen hier auf:
  • Argumente werden, wie in vielen Sprachen, in die runden Klammern geschrieben... aber anders. Erst kommt der Variablenname, dann ein Doppelpunkt und danach der Typ.
  • Der Rückgabewert wird mit einem "->" angegeben
  • "i32" ist also scheinbar ein Typ. Richtig. Und zwar ein Integer-Typ mit 32 bits. In Rust gibt es tatsächlich keinen Typen, der "int" heißt. Es gibt nur sog. fixed size integer (i32, i64, u64, u8) und die CPU-Abhängigen Typen (isize und usize). Dabei steht das "u" immer für unsigned. In vielen Systemsprachen ist "int" quasi "isize".
  • Der Funktionsname ist in snake_case ... das ist kein Muss aber eine Konvention in Rust.
  • Kein "return"? Richtig: In Rust wird automatisch das letzte Statement zurückgegeben. Hier ist "number + 7" das letzte Statement, also wird genau das zurück gegeben.

An der zweiten Funktion kann eine weitere Sache erkennen: Es gibt in Rust sog. "Tupel"-Typen... das sind Typen, die aus mehreren Typen bestehen. Geschrieben werden sie einfach nur durch "(typ1, typ2, ...)".

Funktion "get_greeting" hat jetzt weitere neue Dinge. Zum einen ein "match"-Block. Das ist sowas wie "switch", nur deutlich besser. Wie man sieht, kann man Bereiche angeben und mit "|" kann man mehrere Möglichkeiten angeben. Nun wundert man sich eventuell über das "to_string()" am Ende... das ist eine Sache, die am Anfang ein wenig verwirrend ist. Nur so viel: Strings sind kompliziert, wenn man sie schnell haben möchte. Daher gibt es in Rust zwei String-Typen: "String" und "&str". Weil wir es einfach haben wollen, nehmen wir hier "String"... das geben wir mit "to_string()" an.

Fehlt nur noch die main-Funktion: Dort sehen wir erstmal, dass println erlaubt, Argumente mit auszugeben... und zwar indem man ein "{}" in den sog. Format-String schreibt. Erinnert z.B. an printf() aus C. Und am Ende sehen wir noch eine for Schleife... for Schleifen in Rust sind quasi immer "for each" Schleifen. Eine Schleife, die einfach Zahlen hochzählt wäre z.B. "for i in 1..9 { }".


Und nun?
Das waren jetzt nur zwei Beispiele. Ich könnte euch natürlich noch sehr vieles über Rust erzählen, aber ich möchte nicht zu viel auf einmal schreiben.

Was habt ihr für einen ersten Eindruck? Wollt ihr mehr darüber erfahren oder lässt euch das kalt?
Wenn ihr Fragen zu meinen Beispielen habt, raus damit. Wenn ihr ein paar mehr Infos von mir wollt, kann ich gerne noch was schreiben. Wenn ihr selber lernen wollt, könnt ihr mal das Rust Book durchlesen. Das sollte eine komplette Einführung sein.

Bin auf eure Meinungen gespannt!

Viele Grüße
 
Danke für die, leider sehr kurze, Einführung. Hatte schon eine Weile vor, mir Rust anzusehen, aber bin letztendlich doch nie dazu gekommen. Vielleicht kannst du noch etwas mehr auf die Besonderheiten/Vorteile von Rust eingehen.
 
Ich habe mir mal die Dokumentation angesehen. An sich ist die Sprache für sich selbst interessant. Besonders das fehlen der strikten Objektorientierung trifft bei mir ins schwarze :D Allerdings fehlt mir persönlich der rapide Charakter. Es hat Potenzial für Systementwickler und welche, die direkt auf der Maschine arbeiten müssen. Mir persöhnlich fehlt aber wie schon gesagt die Möglichkeit, rapide Anwendungen zu schreiben. Und da in der Dokumentation noch das Modul für die externen dynamischen Libs als unstable markiert ist, kann es etwas komplizierter sein, die Funktionalitäten zu erweitern. Für mich als Hobbycoder ist das daher noch nichts.
 
Allerdings fehlt mir persönlich der rapide Charakter. Es hat Potenzial für Systementwickler und welche, die direkt auf der Maschine arbeiten müssen. Mir persöhnlich fehlt aber wie schon gesagt die Möglichkeit, rapide Anwendungen zu schreiben.
Jein... Also Ja: Es gibt natürlich Sprachen, die es weitaus einfacher machen schnell Anwendungen zu entwickeln. Wenn man auf ein wenig Performance verzichten kann, absolut. Dennoch finde ich, dass man (ich) mit Rust deutlich schneller entwickeln kann als z.B. mit C++. Rust hat ein eingebautes Test-Framework, Dependency-Manager und Build-Script-Dingens, was alles sehr nützlich ist. Deutlich besser als C++ ;)

Und da in der Dokumentation noch das Modul für die externen dynamischen Libs als unstable markiert ist, kann es etwas komplizierter sein, die Funktionalitäten zu erweitern.
Ich schätze du meinst dynamic_lib in der std? So wie ich das verstehe, ist dieses Modul nur für sehr spezielles Laden von dynamischen Libs, und zwar wo man den Lib-Namen erst zur Laufzeit weiß. Was viel öfters gemacht wird (also was man meist unter dynamischen Libraries versteht), ist ja das angeben des Lib-Namen zur Compile-Zeit... trotzdem wird die eigentliche Library erst zur Laufzeit geladen. Kannst noch mal hier lesen.
Und das normale dynamische Linken kann Rust natürlich. Für viele Libraries gibt es auch schon ein "crate", die die Library in ein Rust-Interface packt. Z.B. "libc" gibt Zugriff auf die Standard C library... auch die wird dynamisch gelinkt.

Danke für die, leider sehr kurze, Einführung. Hatte schon eine Weile vor, mir Rust anzusehen, aber bin letztendlich doch nie dazu gekommen. Vielleicht kannst du noch etwas mehr auf die Besonderheiten/Vorteile von Rust eingehen.

Viel Text schreckt immer Menschen ab :P

Die Eigenschaft, die immer hervorgehoben wird ist der sog. Borrow Checker. Dieser sorgt dafür, dass sehr viele Fehler, die mit Resource Management zu tun haben, schon zur Compile-Zeit abgefangen werden. Aber bevor ich darauf näher eingehe: Der Borrow Checker ist absolut nicht das einzige, was Rust lernenswert macht. Ich z.B. interessiere mich gar nicht so sehr für den BC, sondern finde eher andere Features der Sprache gut. Unter anderem: Traits, Generics, viele Sachen die aus der funktionalen Welt übernommen wurde, viele Sachen die aus Ruby-ähnlichen Sprachen übernommen wurden, usw... Mein Punkt ist nur: Ich finde, es wird zu viel Werbung mit dem Borrow Checker gemacht, obwohl man mit den anderen Sprach Features genau so angeben könnte. Aber genug von meiner Meinung.

Leider fühle ich mich noch nicht in der Lage den Borrow Checker gut zu beschreiben... Ich möchte ja nicht falsches oder schlecht erklärtes Wissen verbreiten. Daher von mir nur eine sehr grobe Zusammenfassung und einen Link. Sorry...

Rust schafft es zur Compile-Zeit viele Fehler abzufangen, indem Rust für jedes Objekt immer einen Owner kennt, der diese besitzt. Der Owner kann alles mit dem Objekt machen. Wenn nun andere auch was mit dem Objekt machen wollen, können sog. Borrows rausgegeben werden. Das sind Referenzen auf das originale Objekt, die in zwei Arten kommen: Mutable (veränderbar) und Immutable (nicht veränderbar). Der Rust Compiler versichert jetzt folgendes:
  1. Wenn eine oder mehrere immutable References rausgegeben wurden, kann das Objekt vom Owner auch nicht geändert werden
  2. Es darf nur eine mutable Reference rausgegeben werden und auch dann kann der Owner das Objekt nicht mehr verändern
Man sieht also: Zu jeder Zeit kann nur einer das Objekt verändern. Und das fängt extrem viele Fehler ab. Und nicht nur mit Speicherfehlern, sondern das selbe Konzept ist auch sinnvoll beim Multithreading und anderen Sachen.

Um eine bessere Erklärung zu lesen: https://doc.rust-lang.org/book/ownership.html

Man muss sich definitiv an dieses Konzept gewöhnen, aber wenn man das geschafft hat, ist es sehr sinnvoll. Wenn es einmal kompiliert treten quasi kaum noch Fehler auf... Natürlich nur noch die Logik Fehler...
 
Jein... Also Ja: Es gibt natürlich Sprachen, die es weitaus einfacher machen schnell Anwendungen zu entwickeln. Wenn man auf ein wenig Performance verzichten kann, absolut. Dennoch finde ich, dass man (ich) mit Rust deutlich schneller entwickeln kann als z.B. mit C++. Rust hat ein eingebautes Test-Framework, Dependency-Manager und Build-Script-Dingens, was alles sehr nützlich ist. Deutlich besser als C++ ;)
Naja unter C++ würde mir jetzt spontan auch nur Qt einfallen, was das schreiben von Anwendung rapide erleichtert. Aber war in C bzw. C++ auch nie wirklich drin. Da kennst du wahrscheinlich deutlich mehr. Das mit der Performance ist natürlich so eine Sache. Das .NET-Framework oder Java kommen an C/C++ natürlich nicht heran. Aber es gibt auch native Sprachen, die das durchaus schon recht gut drauf haben. Kann aber auch sein, das ich da von PureBasic etwas zu verwöhnt bin. :D

Ich schätze du meinst dynamic_lib in der std? So wie ich das verstehe, ist dieses Modul nur für sehr spezielles Laden von dynamischen Libs, und zwar wo man den Lib-Namen erst zur Laufzeit weiß. Was viel öfters gemacht wird (also was man meist unter dynamischen Libraries versteht), ist ja das angeben des Lib-Namen zur Compile-Zeit... trotzdem wird die eigentliche Library erst zur Laufzeit geladen. Kannst noch mal hier lesen.
Und das normale dynamische Linken kann Rust natürlich. Für viele Libraries gibt es auch schon ein "crate", die die Library in ein Rust-Interface packt. Z.B. "libc" gibt Zugriff auf die Standard C library... auch die wird dynamisch gelinkt.
Achsooo ok dann habe ich das ein wenig falsch aufgefasst. War noch zu früh. Ich habe mir die Dokumentation nur grob angesehen und war nicht im Detail. Mich hat erstmal die Syntax und die Lib-List interessiert :D
Dann geht das ja noch. Aber das mit den Crate's ist mir nicht ganz schlüssig. Sind das im Grunde die Header für die Lib's oder wie soll ich das verstehen?
 
Dann geht das ja noch. Aber das mit den Crate's ist mir nicht ganz schlüssig. Sind das im Grunde die Header für die Lib's oder wie soll ich das verstehen?
Mh... eine "crate" in Rust ist erstmal nur eine Compilation-Unit. Dies wiederum kann eine Ausführbare Anwendung sein oder eine Rust-Library. Meist meint man mit "crate" allerdings nur die Rust-Library. So eine "crate" kann man in seinem eigenen Projekt nutzen, indem man quasi nur eine Zeile in der Datei "Cargo.toml" hinzufügt, die sagt "ich will das nutzen".

Was ich jetzt bei der libc meinte ist: Das sind nur Wrapper- oder Bridge-Libraries. Die erfüllen selber nicht wirklich einen Zweck, sondern verbinden nur ein Interface anderer Sprache (meist C) mit Rust. Z.B. steht da nur drin:

Code:
extern {
    pub fn isalnum(c: c_int) -> c_int;
    pub fn isalpha(c: c_int) -> c_int;
}

Das sagt jetzt nur "Ok in der C-Library, die ich dynamisch binden will, gibt es diese beiden Funktionen". Und dann können die auch in Rust genutzt werden. Oft werden solche Teile auch automatisch generiert, weil sie quasi nur für jede C Funktion so eine Zeile brauchen.
 
Ah ok. Versteht sich denn Rust nur mit bestimmten Sprachen oder ist das im Bezug auf die Library erst einmal egal (natürlich meine ich dabei native Libraries und keine Bytecode-Suppe oder von anderen Interpreter-Frameworks ;) )?
 
Also das "mit welchen Sprachen verstehen" ist keine wirklich sinnvolle Frage... Also Rust kann (wie viele andere Sprachen auch) über ein sog. FFI (Foreign Function Interface) Funktionen anderer Binärschnittstellen aufrufen. Hierbei ist das C Interface nach wie vor das einfachste und wird daher für eigentlich alles genutzt. Funktionsaufrufe kann man sowieso quasi nur in nativ kompilierte Sprachen ausführen. Ich wüsste gerade nicht, ob Rust nativ schon andere binäre Interfaces unterstützt (C++ scheinbar nicht), aber grundsätzlich möchte man sehr selten etwas anderes als C.
 
Jetzt muss ich mal was Fragen: Wie kann man einen Datentyp (sagen wir mal f64) in einen anderen (sagen wir hier string) umwandeln?
In C# funktioniert das ja als Beispiel so:
string s = Convert.ToString(2.11) :)
 
Sehr viele Datentypen (alle primitiven) lassen sich mit "to_string()" umwandeln: Playground. Man kann sogar direkt Float-Literale so umwandeln, auch wenn es komisch aussieht... "3.14.to_string()".
Code:
fn main() {
    let a = 3.1415926;
    let sa : String = a.to_string();
    println!("{} == {}", a, sa);
    let s : String = 3.14.to_string();
}

Andersrum geht das übrigens mit der Funktion "from_str()", funktioniert nur ein wenig anders. Diese Funktion gibt allerdings den Typen "Result<T, E>" zurück... das ist ein Typ, der entweder das Ergebnis vom Typ "T" hat oder einen Fehler von Typ "E". Beispiel (Playground):
Code:
use std::str::FromStr;


fn main() {
    let works = f64::from_str("3.1415926");
    let wrong = f64::from_str("3.1xxx");
    println!("float: {:?}, integer: {:?}", works, wrong);
}
Ein paar Erklärungen:
  • Die "use" Anweisung am Anfang ist ähnlich wie z.B. "import" in Java. Warum genau wir jetzt das "usen" müssen, wäre gerade zu kompliziert.
  • "f64::from_str()" ist eine statische Methode, wenn man in Java Slang reden möchte. Die Doppel-Doppelpunkt Notation kennt man aus C++.
  • Bei der Ausgabe nutzen wir nicht nur "{}" sondern "{:?}" im format-string. Das bedeutet, dass wir die Variable als "Debug" ausgeben möchten. Bei dieser Art von Ausgabe werden weitere Informationen ausgegeben (führt es einfach im Playground aus). Das nutzen wir hier, weil die Ergebnisse ("works" und "wrong") wie gesagt vom Typ "Result" sind.
  • Das erste Parsing funktioniert und ist daher "Ok(3.1415926)". Das zweite Parsing funktioniert natürlich nicht und ist ein "Err" mit weiteren Informationen.

Die Typen "Result" und "Option" sind ein wenig aus der funktionalen Welt übernommen und bieten eine andere Art der Fehlerbehandlung. Sehr interessant... wird im Rust Book erklärt.
 
Zuletzt bearbeitet:
Ah ok, danke :)
Das mit dem to_string() habe ich bereits erfolgreich ausprobiert und als Funktion eingebaut, das funktioniert sehr schön :)

Jetzt möchte ich auch from_str() in eine eigene Funktion packen, die folgend aussieht:
Code:
fn to_the_float(s :String) -> f64 {    f64::from_str(s)
}
Doch diese liefert mir folgende Fehler:
Code:
[COLOR=#993311][FONT=Source Code Pro]<anon>:8:19: 8:20 error: mismatched types:[/FONT] expected `&str`,
    found `collections::string::String`
(expected &-ptr,
    found struct `collections::string::String`) [E0308]
<anon>:8     f64::from_str(s)
                           ^
<anon>:8:5: 8:21 error: mismatched types:
 expected `f64`,
    found `core::result::Result<f64, core::num::ParseFloatError>`
(expected f64,
    found enum `core::result::Result`) [E0308]
<anon>:8     f64::from_str(s)
             ^~~~~~~~~~~~~~~~
error: aborting due to 2 previous errors [COLOR=#993311][FONT=Source Code Pro]playpen: application terminated with error code 101[/FONT]

Was verstehe ich da gerade falsch? :)
 
Code:
fn to_the_float(s :String) -> f64 { f64::from_str(s) }
Doch diese liefert mir folgende Fehler:
Ok ja, also gehen wir das mal einzeln alles durch.

Code:
<anon>:8:19: 8:20 error: mismatched types: 
expected `&str`,
    found `collections::string::String`
(expected &-ptr,
    found struct `collections::string::String`) [E0308]
<anon>:8     f64::from_str(s)
                           ^
Deine Variable "s" ist ja vom Typ "String". Dieser Typ lebt in der Standardbibliothek und seiner voller Pfad ist "collections::string::String". Scheinbar erwartet die Funktion "from_str" jedoch einen anderen Typen als Argument, nämlich "&str". Strings sind wie gesagt kein einfaches Thema, daher müssen wir uns hier mit den zwei unterschiedlichen String Typen rumärgern. Aber zum glück gibt es eine einfache Lösung: Den String einfach in &str umwandeln. Und zwar so:
Code:
f64::from_str(&*s)
Sieht komisch aus... gewöhnt man sich aber dran. Das "*" ist ein besonderer Operator, der etwas "dereferenziert". Dieser Operator kann überladen werden, was String tut. Dann kommt dabei ein "str" raus... um jetzt einen "&str" zu bekommen, einfach noch ein "&" davor ;)


Code:
<anon>:8:5: 8:21 error: mismatched types:
 expected `f64`,
    found `core::result::Result<f64, core::num::ParseFloatError>`
(expected f64,
    found enum `core::result::Result`) [E0308]
<anon>:8     f64::from_str(s)
             ^~~~~~~~~~~~~~~~
Das ist ein wenig komplizierter. Erstmal markiert der Compiler die ganze Anweisung in der Funktion. Das tut er weil ja die letzte Anweisung automatisch returned wird. Du hast den Return-Typ als "f64" angegeben. Scheinbar ist also der Typ von deiner letzten Anweisung nicht der selbe, wie den, den du angegeben hast (f64). Der Typ deiner letzten Anweisung entspricht ja dem Return-Typen von der Funktion "from_str" und dieser ist: "core::result::Result<f64, core::num::ParseFloatError>".
Ok das ist ein langer Typ... Mal schauen:
"core::result::Result" ist der volle Pfad vom Typen "Result"... ein Typ der entweder einen Erfolg-Wert oder einen Fehler-Wert zurück gibt. Und in den spitzen Klammern wird nun angegeben, was denn der Erfolg-Typ und was der Fehler-Typ ist. Der Erfolg Typ ist hier "f64" (erster Typ in den spitzen Klammern). Macht ja auch Sinn: Wenn das parsen erfolgreich war, wollen wir gerne einen "f64" haben.
Der Fehlertyp ist "core::num::ParseFloatError". Was genau das ist, ist eigentlich egal: Es kann den Fehler näher beschreiben, der aufgetreten ist.

Und wie lösen wir das jetzt?
Es geht auf mehrere Arten: Es kommt drauf an, wie du mögliche Fehler behandeln willst.

Zuerst die Holzhammer Methode: Du bist dir absolut sicher, dass der String immer als f64 parse-bar ist und nie ein Fehler auftritt? Dann kannst du die Methode ".unwrap()" von deinem "Result" aufrufen. Die gibt einfach den "Erfolg-Wert" zurück, wenn es wirklich Erfolg war. Wenn nicht, beendet sich dein Programm sofort. Daher: Sollte man selten verwenden, außer man ist sich 100% sicher, dass nie ein Fehler auftritt. Hier mal als Code:
Code:
fn to_the_float(s :String) -> f64 { f64::from_str(&*s).unwrap() }

Ok... eventuell willst du auch einen Default-Wert zurück geben, wenn ein Fehler aufgetreten ist (z.b. "0.0"). Das kann man jetzt schön mit dem "match" lösen:
Code:
fn to_the_float(s :String) -> f64 {
    match f64::from_str(&*s) {
        Ok(v) => v,
        Err(_) => 0.0,
    }
}
Was macht das? Erklärung: Wir matchen das Ergebnis von "from_str", also den Result. Im Falle, dass das Ergbnis "Ok" war, wollen wir den Erfolg-Wert zurück geben. Der Erfolg-Wert ist "in dem Ok" drin. Wenn das Ergebnis aber ein "Err" war (und uns interessiert nicht, was für ein Error, daher der Unterstrich), wollen wir 0.0 zurück geben.
Dieses Pattern, dass man einen default-Wert zurück geben möchte, kommt aber so oft vor, dass "Result" direkt eine eigene Methode hat: "unwrap_or". Das sieht dann so aus und tut, was man erwartet:
Code:
fn to_the_float(s :String) -> f64 { f64::from_str(&*s).unwrap_or(0.0) }

Eine weitere Möglichkeit ist natürlich den Fehler "nach oben" weiter zuleiten. Das ginge, indem du einfach den Return-Typ änderst:
Code:
fn to_the_float(s :String) -> Result<f64, core::num::ParseFloatError>{ f64::from_str(&*s) }

Ich hoffe das war verständlich, sonst frag einfach nach!
 
So bin das im Moment "nur" überflogen, aber nach gut drei Jahren Unterricht in C, C# und Java (abwechselnd natürlich ;) ) verstehe ich das meiste, danke :)
 
Zurück