Created
November 23, 2019 15:10
Star
You must be signed in to star a gist
Code shared from the Rust Playground
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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