Skip to content

Instantly share code, notes, and snippets.

@rust-play
Created November 23, 2019 15:10
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save rust-play/437e4fbd22f233d507e5fa6cb206da6f to your computer and use it in GitHub Desktop.
Code shared from the Rust Playground
// Super schnelles Anfänger Tutorial Copy/Borrowing/Ownership/Lifetimes/Move-Semantik/Affine Typsysteme
fn main() {
//schritt1(); // Ownership
//schritt2(); // Move-Semantik, Affines Typsystem und Copy
//schritt3(); // Borrowing
//schritt4(); // Lifetimes
}
//Ownership
#[allow(dead_code)]
fn schritt1() {
//jedes "Ding" was auf eine Speicherstelle zeigt (heap oder stack ..)
//wird von einem "Binding" "besessen" (Ownership). Ein solches Binding
//wird zum Beispiel mit "let" erzeugt
{
#[allow(unused_variables)] //wenn ich das nicht setze, dann warnt mich der Compiler, ich kann auch _a schreiben
let a = 10;
// das `a` binding ist der besitzer (Owner) des Speichers auf dem Stack
// an welchem der Wert 10 gespeichert ist. Das ist zum Beispiel fundamental
// anders als etwa in Java/Javascript. In solchen GC Sprachen wird im
// übertragenen Sinn der Speicher vom GC besessen und als Programmierer
// hat man "lediglich" eine Referenz/Pointer darauf. Der große Unterschied
// in der Benutzung liegt nun daran, dass Rust – sobald ein binding den
// Scope verlässt (eine `}` im Quellcode auftauch) – den Speicher wieder
// freigibt. Scopes müssen nicht unbedingt nur von Funktionen aufgespannt werden.
} // Rust popt den Stack hier und gibt den Speicher von `a` wieder frei.
// a existiert heir natürlich nicht mehr, erstens verlassen "namen" nicht den
// Scope und zweites wurde der Speicher an der Stelle auch schon bereinigt.
// Bei primitiven Datentypen klingt das trivial. Ein Integer wird auch in
// anderen Programmiersprachen von vom Stack gepopt wenn sie den Scope verlassen.
{
// Rust sorgt aber AUCH dafür, dass Daten vom Heap bereinigt werden.
// Hier mal ein etwas beginner freundlichere Personen Struktur ohne <'a> ...
// anstatt &'a str wird hier ein String benutzt. Der Unterschied ist, dass
// String auf dem Heap lebt und von der Person.name besessen wird.
// Der Unterschied zwischen str und String ist in etwa der wie
// String und StringBuilder in Java aber erstmal nicht so wichtig.
#[allow(dead_code)] // Rust warnt auch hier, dass man das eigentlich gar nicht benutzt
struct Person {
name: String,
age: u8,
}
// Man kann jetzt ein einfaches binding von einer Person auf dem Stack
// mit dem Namen peter erstellen. Wenn peter den Scope verlässt (`}`)
// wird der Stack abgeräumt. Aber auch der name! der eigentlich auf dem
// Heap lebt. Wir haben hier "Peter".to_string() geschrieben. "Peter" als
// StringLiteral ist vom typ her ein &str – das Literal "lebt"
// Datensegment der Binärdatei und ist lediglich ein "Pointer" auf diesen
// Wir haben in der Personendefinition aber gesagt, dass wir einen String
// haben wollen und müssen diesen umwandeln. Das geht mit *.to_string()
// oder auch mit *.into() – da Rust die konvertierung &str -> String kennt.
// *.to_string() erstellt also eine Kopie des StringLiterals aus dem
// Datensegment der ausgeführten Datei und "übergibt" sie dem Person.name
// als Owner. Geht ein binding aus dem Scope (`}`) werden auch alle
// "Teil"Daten – wie name – freigegeben und eventuell der "destructor"
// (heißt in Rust drop) aufgerufen.
#[allow(unused_variables)]
let peter: Person = Person{ name: "Peter".to_string(), age: 27 };
// Darum kann ich auch Daten direkt auf dem Heap per Box erstellen.
// Box ist ein sogenannter Wrappertyp und dient als eine Art "Smartpointer"
// wie du es vielleicht aus C++ kennst, nur besser :). Box erstellt also
// also eine Person auf dem Heap wie man es vielleicht unter Java gewohnt ist
// final Person maria = new Person("Maria", 30);
// Mit dem unterschied, dass beim verlassen eines Scopes (`}`) die Daten
// vom Heap freigegeben werden. Das ist in Java/Javascript oder eben auch
// in C++ nicht so.
#[allow(unused_variables)]
let maria: Box<Person> = Box::new(Person{ name: "Maria".into(), age: 30 });
}// Hier sind peter und maria vom Stack UND dessen Daten vom Heap freigegeben.
}
#[allow(dead_code)]
fn schritt2() {
// Was viele Anfänger von Rust verwirrt ist das Affine Typsystem. Jetzt muss
// man keine Angst haben wegen diesem Namen, es heißt lediglich so etwas wie
// das einem "Dinge weggenommen" werden und dieses Verhalten ist man von den
// meisten Programmiersprachen nicht gewohnt.
{
let name: String = "Johanna".to_string();
// in der nachfolgenden Zeile wird der Besitzer (Owner) der
// Daten an dem der Heap allokierte String "Johanna" lebt geändert.
// Wir haben hier NICHT den Fall, dass sowohl name und name_nochmal auf
// die gleichen Daten zeigen – und im Falle einer Veränderung beide
// verändert werden, noch das eine Kopie erstellt wird. Beides findet
// NICHT statt, wie man es von anderen Programmiersprachen gewohnt ist.
// Was wirklich passiert ist, dass "name" quasi "stirbt". Es wird für
// den weiteren Verlauf des Programms unbenutzbar gemacht. "name" war der
// ursprüngliche Besizter (Owner) der Daten des Strings "Johanna"
// auf dem Heap und nun wird "name" enteignet und "name_nochmal" ist jetzt
// der neue Besitzer. Auf "name" darf nicht mehr zugegriffen werden
// wie im auskommentierten println!
let name_nochmal = name;
// println!("name ist: {}", name); // compilier Fehler wenn kein Kommentar
println!("gemove'ter name ist: {}", name_nochmal);
}
// Man spricht in Rust davon, dass die Daten "gemoved" sind. Jede Zuweisung
// ist in Rust automatisch ein "move", solange die Typen die in involviert
// sind nicht das Copy Trait implementieren. Traits sind sowas ähnliches wie
// Interfaces in Java. Primitive Datentypen wie i64 implementieren zum Beispiel
// Copy automatisch.
{
let a = 10;
let _b = a; // hier findet kein move, sondern ein Kopieren statt
println!("ist integer a noch da?: {}", a);
}
{
// Selbstgebaute Typen implementieren vom Grunde her erstmal NICHT Copy/Clone
// und würden im unteren println! zu einem Fehler führen, da a nicht mehr
// im Besitz einer gültigen Speicherstelle ist. Die gehört dann b
// Man kann Rust aber automatisch diese Traits implementieren lassen.
// Rust kann das allerdings nur dann, wenn der selbstgebaute Typ
// nur aus Typen besteht die selber kopierbar sind. Bei der Person von
// oben geht das z.B. nicht, da String diese Traits nicht automatisch
// implementiert. Der Grund ist, dass es in Rust klar sein soll, wann
// aufwendiges kopieren auftritt oder ich mir zumindest ungefährt im klaren
// darüber bin wie aufwendig eine Kopie ist. Bei zwei i32 weiß ich wie viel
// kopiert wird, bei einem String nicht, der könnte auch 2GB groß sein.
// wenn man das #[derive(Copy, Clone)] wegnimmt, gibt es einen Fehler und Hinweis vom Compiler
#[derive(Debug)]
#[derive(Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
let a = Point {x: 10, y:20};
let _b = a;
println!("ist Point a noch da? {:?}", a);
}
{
// Move tritt insbesondere auch bei Funktionen auf und darüber stolpern
// die meisten Anfänger
// kein Copy/Clone derive!
#[allow(dead_code)]
struct Point {
x: i32,
y: i32,
}
//man darf übrigens auch einfach so Funktionen erstellen.
fn move_me(p: Point) {
println!("benutzer den Punkt p: {}", p.x);
// Achtung, am ende des Scopes (`}`) wird `p` freigegeben!
// wenn p der Owner ist, und das ist er, dann wird auch der Speicher
// auf den p bindet freigegeben -> der Point wird komplett vom
// Speicher entfernt, wenn die Funktion den Scope verlässt!
}
let point = Point{ x: 10, y: 20 };
println!("hier ist point noch gültig! {}", point.x);
move_me(point);
//println!("hier nicht mehr!! {}", point.x);
// point wurde hier in `move_me` hinein gemoved. Stell dir den Funktionskopf
// von move_me(p: Point) wie ein let Binding vor (let p: Point) der
// die lokale Variable p im Scope move_me erzeugt
// ähnlich wie:
//
// fn move_me() {
// let p: Point = param1_of_function;
// }
//
// wir haben gelernt, dass point nach einem move nicht mehr gültig ist.
// egal ob wir jetzt ein zweites binding erstellen (let point_nochmal = point;)
// oder ob wir es einer Funktion übergeben, was effektiv wie (let p = point;) ist
// wir wissen auch, dass p und dessen Speicher den es owned am Ende von
// move_me bereinigt wird. Das ist auch der Grund, warum man es nach der
// Funktion nicht mehr per println! benutzen darf. Nicht nur ist point
// nicht mehr der Besitzer, der eigentliche Punkt im Speicher von dem
// point der Besitzer vor dem move_me Aufruf war IST FREIGEGEBEN worden!
}
{
// Jetzt ist es natürlich blöd, wenn jede Variable die man in eine
// Funktion gibt immer gleich wieder "zerstört" wird. Ein einfacher "Trick"
// ist volgender
// kein Copy/Clone derive!
#[allow(dead_code)]
struct Point {
x: i32,
y: i32,
}
fn move_me_back(p: Point) -> Point {
println!("benutze den Punkt p: {}", p.x);
// wir geben den Besitz wieder zurück!
p
}
let point = Point{ x: 10, y: 20 };
println!("hier ist point noch gültig! {}", point.x);
let point = move_me_back(point); // move p wieder nach point - per shadowing
// let point2 = move_me_back(point); // geht natürlich auch, ohne shadowing
println!("hier immer noch!! {}", point.x);
// Jetzt ist das umherschieben aber auch nicht sonderlich elegant!
// Wie es besser geht -> Schritt 3 Borrowing!
}
}
//Borrowing
#[allow(dead_code)]
fn schritt3() {
{
// Wenn wir nicht immer jeden Parameter hin und her schieben wollen
// dann müssen wir "Dinge" ausleihen, mit `&`. Man kann sich das wie
// einen Pointer oder Referenz in anderen Sprachen vorstellen. Es gibt
// aber entscheidene Unterschiede.
// kein Copy/Clone derive!
#[allow(dead_code)]
struct Point {
x: i32,
y: i32,
}
fn borrow_me(p: &Point) {
println!("benutze den ausgeliehenen Punkt p: {}", p.x);
// p verlässt den Scope (`}`) und wird freigegeben. Da p aber NICHT
// Owner (Besitzer) ist – sondern Point nur geliegehn (&Point) hat
// wird der Speicher dahinter auch nicht freigegeben, sonder die
// Leihsache (Point im Speicher) an den Besitzer zurück gegeben.
}
let point = Point{ x: 99, y: 88 };
println!("point ist hier gültig: {}", point.x);
borrow_me(&point); // wir übergeben nicht point sondern &point, ein borrow
println!("point ist hier immernoch gültig: {}", point.x);
let borrow_point = &point;
println!("alle sind zufrieden und glücklich! {}, {}", point.x, borrow_point.y);
}
{
// Jetzt kommt aber hoffentlich die Frage auf, WARUM DAS GANZE? Wenn
// man sich das obrige Beispiel anguckt, dann scheint es so als sei alles
// was wir vorher gelernt haben einfach umgangen worden! Fast richtig!
// Wir haben aber im obrigen Beispiel eine wichtige Sache außer acht
// gelassen, `mut`. In keinen der obrigen Beispiele haben wir irgendwas
// verändert, sondern nur gelesen! Rust hat eine wichtige Eigenschaft
// wenn es ums Borgen geht. Man darf entweder VIELEN etwas LESEND borgen
// oder nur EINEM etwas SCHREIBEND!
// kein Copy/Clone derive!
#[allow(dead_code)]
struct Point {
x: i32,
y: i32,
}
fn borrow_me(p: &mut Point) {
p.x = 2323; // wir dürfen &mut Point verändern
// wir können uns auch in der Funktion sicher sein, dass NIEMAND
// sonst den Point von dem p ein borrow hat schreibend UND AUCH NICHT
// lesend zugreift – zum Beispiel in einem anderen Thread
println!("benutze den ausgeliehenen Punkt p: {}", p.x);
}
let mut point = Point{ x: 99, y: 88 }; //muss mut'able sein
println!("point ist hier gültig: {}", point.x);
//let borrow_point = &mut point;
//println!("schade! {}, {}", point.x, borrow_point.y); Verboten!
// wir wissen nicht was println! intern macht, es könnte einen Thread
// aufmachen und später darauf zugreifen – es darf nur ein &mut von Point
// existieren und den brauchen wir für borrow_me. Man darf zwar
// `let borrow_point = &mut point;` schreiben, weil Rust schlau genug ist
// zu erkennen, dass man es in diesen Scope nicht mehr verwendet, man darf
// es aber nicht "weitergeben", zum Beispiel an das Macro println!
borrow_me(&mut point); // wir übergeben nicht &point sondern &mut point, ein borrow mit Schreibrechten
println!("point ist hier immernoch gültig: {}", point.x);
}
}
// Lifetimes
#[allow(dead_code)]
#[allow(unused_variables)]
fn schritt4() {
{
// Möchten wir zum Beispiel eine Struktur erstellen, die seine Elemente
// nicht unbedingt besitzt, sondern nur ausgeliehen hat – weil man sie
// ansonsten ständig kopieren müsste – dann kann anstatt T auch &T
// verwenden oder für konkrete Typen anstatt i32 halt &i32
// Das Problem mit ausgeliehenen Sachen ist wie bei Pointern, dass man
// darauf achten muss, dass die Dinge/Speicher auf die man Zeigt IMMER
// gültig sind. Möchte man einen Punkt erstellen der seine Elemente nur
// leiht, dann muss man dem Copiler beweisen, dass diese Elemente immer
// gültig sind, solange der Punkt existiert. In Rust macht man das mit
// Lifetimes ( 'lifetime_name )
struct RefPoint<'lx, 'ly> {
x: &'lx i32,
y: &'ly i32,
}
let a = 10;
let b = 20;
let point = RefPoint{ x: &a, y: &b };
// heirmit beweisen wir Rust, dass a und b mindestens genauso lange "Leben"
// wie point. Bis zum Ende des Scopes (`}`) sind alle drei noch gültig
}
{
struct RefPoint<'lx, 'ly> {
x: &'lx i32,
y: &'ly i32,
}
// Das funktioniert nicht!
//
// fn create_point() -> RefPoint {
// let a = 10; // leben nur bis zum Ende des Scopes!!
// let b = 20;
//
// let point = RefPoint{ x: &a, y: &b };
// point
// }
//
// new_point = create_point();
// Wenn wir einen Point erzeugen wollen dessen Elemente nur Referenzen sind
// dann müssen wir sicher gehen, dass diese Referenzen immer gültig sind.
// In der obigen Funktion werden Variablen a und b erzeugt deren Gültigkeit
// aber nur bis zum Ende der Funktion ist (`}`). Man sagt ihre Lebenszeit
// (die Lifetime) ist auf die Funktion begrentzt! Wir können also kein
// RefPoint zurück geben, dessen Elemente aber nach der Rückgabe nicht mehr
// am Leben sind. new_point hätte, wenn man es benutzen würde Referenzen auf
// a und b die aber schon lange vom Stack abgeräumt sind.
//Wir müssen Rust also beweisen, dass die Refentenzen lange genug leben
// also mindestens so lange wie der Point.
let long_a = 10;
let long_b = 20;
fn create_point_long<'la, 'lb>(x: &'la i32, y: &'lb i32) -> RefPoint<'la, 'lb> {
let point = RefPoint{ x, y };
point
}
let new_point = create_point_long(&long_a, &long_b);
//'la und 'lb sind Lifetime Annotationen die sagen:
//
// fn create_point_long<'la, 'lb>
//
// es gibt eine Funktion create_point_long und ich führe die Lifetimes 'la und 'la
// im Rahmen dieser Funktion ein
//
// (x: &'la i32, y: &'lb i32)
//
// die Parameter x und y mit einem Borrow auf &i32 haben jeweils diese Lifetime
// 'la und 'lb
//
// -> RefPoint<'la, 'lb>
//
// die Funktion gibt ein RefPoint zurück mit den Lifetimes 'la, 'lb wobei
// gilt, dass diese genauso oder länger leben wie der RefPoint.
// Rust guckt nun in diesem Scope hier nach, wie lange long_a und long_b
// leben. Leben sie mindestens so lange wie new_point ist alles gut. Dem
// Compiler wurde bewiesen, dass die Elemente x,y in RefPoint lange genug
// leben. Im Gegensatz zur Version mit create_point. Der Compiler sieht,
// dass a und b nur bis zum Ende der Funktion leben, new_point lebt aber
// bis zum Ende dieses Scopes hier. Mit 'la und 'lb zeigt man dem Compiler
// lediglich wo welche Lifetimes herkommen. Man könnte diese auch vertauschen
// (x: &'lb i32, y: &'la i32) und es würde keinen großen unterschied in diesem
// Beispiel machen. Rust kann diese aber nicht immer erraten, darum muss man
// sie manchmal hinschreiben. In dem Beipsiel aus Schritt 3 haben wir zwar
// borrows gemacht und mussten keine Lifetimes hinschreiben, weil Rust ganz
// klar weiß welche Variablen wie lange Leben müssen.
// In diesem Beispiel kann Rust das aber nicht, weil der Compiler nicht wissen
// kann für welches Element welche Lebenszeit forgeschrieben ist. Man stelle sich
// vor in einem komplexeren Beispiel könnten 'la und 'lb auch von ganz anders
// her kommen. Man muss für RefPoint also genau sagen, 'lx für das x kommt daher und
// 'ly for das y kommt daher. Mit der Funktionsdefinition sagen wir dem Compiler
// für dein 'lx in RefPoint verwende bitte 'la und damit die Lebenszeit des
// übergebenen Parameters x/long_a. Das gleiche für den anderen Parameter.
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment