diff --git a/404.html b/404.html new file mode 100644 index 0000000..f8414f0 --- /dev/null +++ b/404.html @@ -0,0 +1,3 @@ + +
This is a website for the JNP2: Rust course at MIM UW.
+The course's instructor is Wojciech Przytuła (GitHub: wprzytula, e-mail: wp418383[at]students[dot]mimuw[dot]edu[dot]pl
),
Instructori emeriti:
+pw394980[at]students[dot]mimuw[dot]edu[dot]pl
),agluszak[at]mimuw[dot]edu[dot]pl
).You can find lesson scenarios here.
+ + + ++ 2024-10-03 (last edit: 2024-10-06) +
+We will be using Github Classroom for task submission and Discord for discussions.
+Our main learning/teaching resource will be "The Book".
+Also worth mentioning: "Rust by Example".
++ 2024-10-03 (last edit: 2024-09-20) +
+Even if you don't end up using Rust, learning it expands your horizons
+Let's compare the same code written in C, C++ +and Rust.
+#include <iostream>
+#include <unordered_map>
+
+using name = std::string;
+using age = int;
+using person = std::pair<name, age>;
+using address = std::string;
+using address_book = std::unordered_map<person, address>;
+
+void print_address_book(const address_book &book)
+{
+ for (const auto &[person, address] : book)
+ {
+ std::cout << person.first << " is " << person.second << " years old and lives at " << address << std::endl;
+ }
+}
+
+int main()
+{
+
+ address_book people{};
+ people.insert({{"John", 20}, "221B Baker Street, London"});
+ people.insert({{"Mary", 30}, "Avenue des Champs-Élysées, Paris"});
+ people.insert({{"Jack", 73}, "Wall Street, New York"});
+ print_address_book(people);
+
+ return 0;
+}
+
+
+(Download the source code for this example: tudruj.cpp)
+# sample 1 - different ways of removing elements from the list while iterating
+list1 = [1, 2, 3, 4]
+for idx, item in enumerate(list1):
+ del item
+list1
+
+# [1, 2, 3, 4]
+
+list2 = [1, 2, 3, 4]
+for idx, item in enumerate(list2):
+ list2.remove(item)
+list2
+
+# [2, 4]
+
+list3 = [1, 2, 3, 4]
+for idx, item in enumerate(list3[:]):
+ list3.remove(item)
+list3
+
+# []
+
+list4 = [1, 2, 3, 4]
+for idx, item in enumerate(list4):
+ list4.pop(idx)
+list4
+
+# [2, 4]
+
+# sample 2 - string interning
+a = "abc"
+b = "abc"
+a is b
+
+# True
+
+a = ''.join(['a', 'b', 'c'])
+b = ''.join(['a', 'b', 'c'])
+a is b
+
+# False
+
+a = "abc!"
+b = "abc!"
+a is b
+
+# False
+
+# sample 3 - chained operations
+(False == False) in [False]
+
+# False
+
+False == (False in [False])
+
+# False
+
+False == False in [False] # unexpected...
+
+# True
+
+# sample 4 - is operator
+a = 256
+b = 256
+a is b
+
+# True
+
+a = 257
+b = 257
+a is b
+
+# False
+
+a, b = 257, 257
+a is b
+
+# True
+
+257 is 257
+
+# <>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
+# <>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
+# C:\Users\kgasinsk\AppData\Local\Temp\ipykernel_15776\331119389.py:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
+# 257 is 257
+
+# sample 5 - local variables
+def f(trufel):
+ if trufel:
+ y = 1
+ y += 1
+
+f(True) # everything is fine
+
+f(False) # gives error: local variable 'y' referenced before assignment
+
+# ---------------------------------------------------------------------------
+# UnboundLocalError Traceback (most recent call last)
+# Input In [17], in <cell line: 1>()
+# ----> 1 f(False)
+
+# Input In [15], in f(trufel)
+# 3 if trufel:
+# 4 y = 1
+# ----> 5 y += 1
+
+# UnboundLocalError: local variable 'y' referenced before assignment
+
+(Download the source code for this example: gasinska.py)
+// mutowalność jest wbudowana w język
+
+type S struct {
+ A string
+ B []string
+}
+
+func main() {
+ x := S{"x-A", []string{"x-B"}}
+ y := x // copy the struct
+ y.A = "y-A"
+ y.B[0] = "y-B"
+
+ fmt.Println(x, y)
+ // Outputs "{x-A [y-B]} {y-A [y-B]}" -- x was modified!
+}
+
+// slices i kwestia append
+
+func doStuff(value []string) {
+ fmt.Printf("value=%v\n", value)
+
+ value2 := value[:]
+ value2 = append(value2, "b")
+ fmt.Printf("value=%v, value2=%v\n", value, value2)
+
+ value2[0] = "z"
+ fmt.Printf("value=%v, value2=%v\n", value, value2)
+}
+
+func main() {
+ slice1 := []string{"a"} // length 1, capacity 1
+
+ doStuff(slice1)
+ // Output:
+ // value=[a] -- ok
+ // value=[a], value2=[a b] -- ok: value unchanged, value2 updated
+ // value=[a], value2=[z b] -- ok: value unchanged, value2 updated
+
+ slice10 := make([]string, 1, 10) // length 1, capacity 10
+ slice10[0] = "a"
+
+ doStuff(slice10)
+ // Output:
+ // value=[a] -- ok
+ // value=[a], value2=[a b] -- ok: value unchanged, value2 updated
+ // value=[z], value2=[z b] -- WTF?!? value changed???
+}
+
+// error handling
+
+len, err := reader.Read(bytes)
+if err != nil {
+ if err == io.EOF {
+ // All good, end of file
+ } else {
+ return err
+ }
+}
+
+
+// interfejs nil
+
+type Explodes interface {
+ Bang()
+ Boom()
+}
+
+// Type Bomb implements Explodes
+type Bomb struct {}
+func (*Bomb) Bang() {}
+func (Bomb) Boom() {}
+
+func main() {
+ var bomb *Bomb = nil
+ var explodes Explodes = bomb
+ println(bomb, explodes) // '0x0 (0x10a7060,0x0)'
+ if explodes != nil {
+ println("Not nil!") // 'Not nil!' What are we doing here?!?!
+ explodes.Bang() // works fine
+ explodes.Boom() // panic: value method main.Bomb.Boom called using nil *Bomb pointer
+ } else {
+ println("nil!") // why don't we end up here?
+ }
+}
+
+// ubogie struktury danych, takie customowe tracą type safety m.in poprzez castowanie do interface{}
+// kiedyś brak generyków, choć teraz w znacznym stopniu problem został rozwiązany.
+
+(Download the source code for this example: koszowski.go)
+def add_contents(input_list, contents=[]):
+ for val in input_list:
+ contents.append(val)
+ return contents
+
+print(add_contents([1])) # [1]
+print(add_contents([2])) # [1, 2]
+
+(Download the source code for this example: grodzicki.py)
+cargo clippy
(for static analysis)cargo check
, but it's less powerful than clippycargo fmt
(for code formatting)fn main() {
+ let name = "World";
+ println!("Hello, {}!", name); // using the println! macro
+}
+
+
+(Download the source code for this example: hello_world.rs)
+#![allow(unused_variables)]
+#![allow(unused_assignments)]
+
+fn main() {
+ let x = 40; // inferred type
+ let y: i32 = 100; // specified type
+
+ {
+ let x = 40 + 2; // shadowing
+ println!("x is {}", x); // prints 42
+ }
+
+ // x = 0; // compilation error, variables are by default immutable
+ let mut x = 40; // declare as mutable
+ x = 0; // now we can reassign
+
+ x += 1; // x = x + 1
+}
+
+
+(Download the source code for this example: variables.rs)
+#![allow(unused_variables)]
+
+fn main() {
+ let x = 42;
+
+ if x == 42 {
+ println!("x is 42");
+ } else if x == 43 {
+ println!("x is 43");
+ } else {
+ println!("x is not 42 or 43");
+ }
+
+ // we can also use ifs as expressions
+ let a_or_b = if x == 0 {
+ "a" // notice no semicolon at the end
+ } else {
+ "b"
+ };
+}
+
+
+(Download the source code for this example: conditionals.rs)
+#![allow(unused_variables)]
+
+fn main() {
+ for i in 0..10 {
+ println!("i is {}", i); // i in [0, 10)
+ }
+
+ let mut x = 0;
+
+ while x < 50 {
+ x += 1;
+ }
+
+ let mut y = 0;
+ let mut iterations = 0;
+ loop {
+ iterations += 1;
+ if iterations % 2 == 0 {
+ continue;
+ }
+ y += 1;
+ if y == 10 {
+ break;
+ }
+ }
+
+ // we can use labels to refer to a specific loop
+ let mut count = 0;
+ 'counting_up: loop {
+ let mut remaining = 10;
+
+ loop {
+ if remaining == 9 {
+ break;
+ }
+ if count == 2 {
+ break 'counting_up; // ends the outer loop
+ }
+ remaining -= 1;
+ }
+
+ count += 1;
+ }
+
+ // We can use break with a value.
+ // Because loops are expressions too,
+ // the value we break with will be returned from the functions
+ let mut counter = 0;
+ let value = loop {
+ counter += 1;
+ if counter == 10 {
+ break 32;
+ }
+ };
+}
+
+
+(Download the source code for this example: loops.rs)
+fn get_5() -> u32 {
+ 5 // we could also write "return 5;"
+}
+
+fn print_sum(a: u32, b: u32) {
+ println!("a + b = {}", a + b);
+}
+
+fn main() {
+ let a = 100;
+ print_sum(a, get_5());
+}
+
+
+(Download the source code for this example: functions.rs)
+Click here
++ You're seeing this page because we couldn't find a template to render. +
+
+ To modify this page, create a page.html file in the templates directory or
+ install a theme.
+
+ You can find what variables are available in this template in the documentation.
+
+ 2024-10-10 (last edit: 2024-10-08) +
+Even if you've never seen Rust code before, chances are you still heard the term borrow checker or something about Rust's ownership. Indeed, Rust's ownership model lies at the very core of its uniqueness. But to fully understand it and appreciate it, let's first take a look at how memory management is handled in most popular languages.
+Garbage Collection - in many high-level programming languages, like Java, Haskell or Python, memory management is done fully by the language, relieving the programmer from this burden. This prevents memory leaks and memory related errors (like use after free), but does come at a cost - there is a runtime overhead, both memory and performance wise, caused by the constantly running garbage collection algorithms and the programmer usually has very little control over when the garbage collection takes place. Also, garbage collection does not prevent concurrency-related errors, such as data races, in any way.
+Mind your own memory - in low-level languages and specific ones like C++, performance comes first so we cannot really afford to run expansive bookkeeping and cleaning algorithms. Most of these languages compile directly to machine code and have no language-specific runtime environment. That means that the only place where memory management can happen is in the produced code. While compilers insert these construction and destruction calls for stack allocated memory, it generally requires a lot of discipline from the programmer to adhere to good practices and patterns to avoid as many memory related issues as possible and one such bug can be quite deadly to the program and a nightmare to find and fix. These languages basically live by the "your memory, your problem" mantra.
+And then we have Rust. Rust is a systems programming language and in many ways it's akin to C++ - it's basically low-level with many high-level additions. But unlike C++, it doesn't exactly fall into either of the categories described above, though it's way closer to the second one. It performs no additional management at runtime, but instead imposes a set of rules on the code, making it easier to reason about and thus check for its safety and correctness at compile time - these rules make up Rust's ownership model.
+In a way, programming in Rust is like pair-programming with a patient and very experienced partner. Rust's compiler will make sure you follow all the good patterns and practices (by having them ingrained in the language itself) and very often even tell you how to fix the issues it finds.
+Disclaimer: when delving deeper into Rust below we will make heavy use of concepts like scopes, moving data, stack and heap, which should have been introduced as part of the C++ course. If you need a refresher of any of these, it's best to do so now, before reading further.
+In the paragraph above we mentioned a set of rules that comprise Rust's ownership model. The book starts off with the following three as its very foundation:
+Each value in Rust is tied to a specific variable - we call that variable its owner.
+There can only be one owner at a time.
+When the owner goes out of scope, the value will be destroyed (or in Rust terms - dropped).
+The third point might make you think about C++ and its automatic storage duration. We will later see that, while very similar at first, Rust expands on these mechanics quite a bit. The following code illustrates the basic version of this:
+{
+ let a: i32 = 5; // allocation on the stack, 'a' becomes an owner
+
+ // do some stuff with 'a'
+
+} // 'a', the owner, goes out of scope and the value is dropped
+
+So far, so good. Variables are pushed onto the stack when they enter the scope and destroyed during stack unwinding that happens upon leaving their scope. However, allocating and deallocating simple integers doesn't impress anybody. Let's try something more complex:
+{
+ let s = String::from("a string"); // 's' is allocated on the stack, while its contents ("a string")
+ // are allocated on the heap. 's' is the owner of this String object.
+
+ // do some stuff with 's'
+
+} // 's', the owner, goes out of scope and the String is dropped, its heap allocated memory freed
+
+If you recall the RAII (Resource Acquisition Is Initialization) pattern from C++, the above is basically the same thing. We go two for two now in the similarity department, so... is Rust really any different then? There is a part of these examples that we skipped over - actually doing something with the values.
+Let's expand on the last example. The scoping is not really important for that one, so we don't include it here.
+let s = String::from("a string"); // same thing, 's' is now an owner
+
+let s2 = s; // easy, 's2' becomes another owner... right?
+
+println!("And the contents are: {}", s); // this doesn't work, can you guess why?
+
+At first glance everything looks great. If we write this code (well, an equivalent of it) in basically any other popular language, it will compile no issue - but it does not here and there's a good reason why.
+To understand what's happening, we have to consult the rules again, rule 2 in particular. It says that there can only be one owner of any value at a given time. So, s
and s2
cannot own the same object. Okay, makes sense, but what is happening in this line then - let s2 = s;
? Experience probably tells you that s
just gets copied into s2
, creating a new String object. That would result in each variable owning its very own instance of the string and each instance having exactly one owner. Sounds like everyone should be happy now, but wait - in that case the last line should work no issue, right? But it doesn't, so can't be a copy. Let's see now what the compiler actually has to say:
error[E0382]: borrow of moved value: `s`
+ --> src/main.rs:6:42
+ |
+2 | let s = String::from("a string");
+ | - move occurs because `s` has type `String`, which does not implement the `Copy` trait
+3 |
+4 | let s2 = s;
+ | - value moved here
+5 |
+6 | println!("And the contents are: {}", s);
+ | ^ value borrowed here after move
+
+"value moved here" - gotcha! So s
is being moved to s2
, which also means that s2
now becomes the new owner of the string being moved and s
cannot be used anymore. In Rust, the default method of passing values around is by move, not by copy. While it may sound a bit odd at first, it actually has some very interesting implications. But before we get to them, let's fix our code, so it compiles now. To do so, we have to explicitly tell Rust to make a copy by invoking the clone
method:
let s = String::from("a string"); // 's' is an owner
+
+let s2 = s.clone(); // 's2' now contains its own copy
+
+println!("And the contents are: {}", s); // success!
+
+The compiler is happy now and so are we. The implicit move takes some getting used to, but the compiler is here to help us. Now, let's put the good, old C++ on the table again and compare the two lines:
+let s2 = s;
is equivalent to auto s2 = std::move(s);
let s2 = s.clone()
is equivalent to auto s2 = s
There are a few important things to note here:
+Making a copy is oftentimes not cheap. Memory needs to be allocated and copied, and a call to the system has to be made. We should prefer to move things as much as possible to avoid this cost - in C++ we have a myriad of language features like std::move
and r-references to achieve this. Every programmer worth their salt needs to be well versed in all of them to write efficient C++ code and simply forgetting one move can lead to significant performance loss (and this happens to even the most senior devs ever existing, let's not pretend). On the contrary, in Rust you need to make an effort to make a copy and that makes you very aware of the cost you're paying - something that we'll see quite a lot of in the language. Also, if you forget a clone there's no harm done - it just won't compile!
Hidden in all of this is another nice thing Rust gives us. In C++, nothing prevents you from using variables after they've been moved from, leading to unexpected errors in a more complex code. In Rust, that variable (in our case s
) simply becomes invalid and the compiler gives us a nice error about it.
A good question to ask. Copying primitives is cheap. And it's not convenient for the programmer to have to always write .clone()
after every primitive. If we take a look at the error from the previous example:
move occurs because `s` has type `String`, which does not implement the `Copy` trait`
+
+It says that s
was moved because the String
type doesn't have the Copy
trait. We will talk about traits more in depth in the future lessons, but what this basically means is that String
is not specified to be copied by default. All primitive types (i32
, bool
, f64
, char
, etc.) and tuples consisting only of primitive types implement the Copy
trait.
How to fix that code?
+fn count_animals(num: u32, animal: String) {
+ println!("{} {} ...", num, animal);
+}
+
+fn main() {
+ let s = String::from("sheep");
+
+ count_animals(1, s);
+ count_animals(2, s);
+ count_animals(3, s);
+}
+
+We now know how to move things around and how to clone them if moving is not possible. But what if making a copy is unnecessary - maybe we just want to let someone look at our resource and keep on holding onto it once they're done. Consider the following example:
+fn read_book(book: String) {
+ println!("[Reading] {}", book);
+}
+
+fn main() {
+ let book = String::from("Merry lived in a big old house. The end.");
+
+ read_book(book.clone());
+
+ println!("Book is still there: {}", book);
+}
+
+Cloning is pretty excessive here. Imagine recommending a book to your friend and instead of lending it to them for the weekend, you scan it and print an exact copy. Not the best way to go about it, is it? Thankfully, Rust allows us to access a resource without becoming an owner through the use of references and the &
operator. This is called a borrow.
The adjusted code should look like this:
+fn read_book(book: &String) {
+ println!("[Reading] {}", book);
+}
+
+fn main() {
+ let book = String::from("Merry lived in a big old house. The end.");
+
+ read_book(&book);
+
+ println!("Book is still there: {}", book);
+}
+
+As with everything, references are too, by default, immutable, which means that the read_book
function is not able to modify that book passed into it. We can also borrow something mutably by specifying it both in the receiving function signature and the place it gets called. Maybe you want to have your book signed by its author?
fn sign_book(book: &mut String) {
+ book.push_str(" ~ Arthur Author");
+}
+
+fn main() {
+ // note that the book has to be marked as mutable in the first place
+ let mut book = String::from("Merry lived in a big old house. The end.");
+
+ sign_book(&mut book); // it's always clear when a parameter might get modified
+
+ println!("{}", book); // book is now signed
+}
+
+Pretty neat, but doesn't seem that safe right now. Let's try to surprise our friend:
+fn erase_book(book: &mut String) {
+ book.clear();
+}
+
+fn read_book(book: &String) {
+ println!("[Reading] {}", book);
+}
+
+fn main() {
+ let mut book = String::from("Merry lived in a big old house. The end.");
+
+ let r = &book; // an immutable borrow
+
+ erase_book(&mut book); // a mutable borrow
+
+ read_book(r); // would be pretty sad to open a blank book when it was not
+ // what we borrowed initially
+
+ println!("{}", book);
+}
+
+Fortunately for us (and our poor friend just wanting to read), the compiler steps in and doesn't let us do that, printing the following message:
+error[E0502]: cannot borrow `book` as mutable because it is also borrowed as immutable
+ --> src/main.rs:14:14
+ |
+12 | let r = &book; // an immutable borrow
+ | ----- immutable borrow occurs here
+13 |
+14 | erase_book(&mut book); // a mutable borrow
+ | ^^^^^^^^^ mutable borrow occurs here
+15 |
+16 | read_book(r); // would be pretty sad to open a blank book when it was not
+ | - immutable borrow later used here
+
+This is where the famous borrow checker comes in. To keep things super safe, Rust clearly states what can and cannot be done with references and tracks their lifetimes. Exactly one of the following is always true for references to a given resource:
+There exists only one mutable reference and no immutable references, or
+There is any number of immutable references and no mutable ones.
+You may notice a parallel to the readers - writers problem from concurrent programming. In fact, the way Rust's borrow checker is designed lends itself incredibly well to preventing data race related issues.
+Rust also checks for dangling references. If we try to compile the following code:
+fn main() {
+ let reference_to_nothing = dangle();
+}
+
+fn dangle() -> &String {
+ let s = String::from("hello");
+
+ &s
+}
+
+we will get an adequate error:
+error[E0106]: missing lifetime specifier
+ --> src/main.rs:5:16
+ |
+5 | fn dangle() -> &String {
+ | ^ expected named lifetime parameter
+ |
+ = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
+help: consider using the `'static` lifetime
+ |
+5 | fn dangle() -> &'static String {
+ | ^^^^^^^^
+
+The message above suggests specifing a lifetime for the returned string. In Rust, the lifetime of each variable is also a part of its type, but we will talk more about it later.
+Our previous solution using clone()
was pretty inefficient. How should this code look now?
fn count_animals(num: u32, animal: String) {
+ println!("{} {} ...", num, animal);
+}
+
+fn main() {
+ let s = String::from("sheep");
+
+ count_animals(1, s.clone());
+ count_animals(2, s.clone());
+ count_animals(3, s); // we could've ommitted the clone() here. Why?
+}
+
+The last part of working with references that we will cover in this lesson are slices. A slice in Rust is a view over continuous data. Let us start with a string slice - the &str
type.
Note: for the purposes of these examples we assume we are working with ASCII strings. More comprehensive articles on handling strings are linked at the end of this lesson.
+To create a string slice from the String
object s
, we can simply write:
let slice = &s[1..3]; // creates a slice of length 2, starting with the character at index 1
+
+This makes use of the &
operator and Rust's range notation to specify the beginning and end of the slice. Thus, we can also write:
let slice = &s[2..]; // everything from index 2 till the end
+let slice = &s[..1]; // only the first byte
+let slice = &s[..]; // the whole string as a slice
+let slice = s.as_str(); // also the whole string
+
+You might have noticed that we always built String
values using the from()
method and never actually used the string literals directly. What type is a string literal then? Turns out it's the new string slice we just learned about!
let slice: &str = "string literal";
+
+In fact, it makes a lot sense - string literals, after all, are not allocated on the heap, but rather placed in a special section of the resulting binary. It's only natural we just reference that place with a slice.
+Slices can also be taken from arrays:
+let array: [i32; 4] = [42, 10, 5, 2]; // creates an array of four 32 bit integers
+let slice: &[i32] = &array[1..3]; // results in a slice [10, 5]
+
+Can this code still be improved from the previous version utilizing references? Think about the signature of count_animals
.
fn count_animals(num: u32, animal: &String) {
+ println!("{} {} ...", num, animal);
+}
+
+fn main() {
+ let s = String::from("sheep");
+
+ count_animals(1, &s);
+ count_animals(2, &s);
+ count_animals(3, &s);
+}
+
+Deadline: 16.10.2024 23:59
+ + ++ You're seeing this page because we couldn't find a template to render. +
+
+ To modify this page, create a page.html file in the templates directory or
+ install a theme.
+
+ You can find what variables are available in this template in the documentation.
+
+ You're seeing this page because we couldn't find a template to render. +
+
+ To modify this page, create a page.html file in the templates directory or
+ install a theme.
+
+ You can find what variables are available in this template in the documentation.
+
+ 2024-10-17 (last edit: 2024-10-17) +
+Below is a compact overview of Rust's structs
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+struct Position(i32, i32); // tuple struct
+
+// Could Hero derive the Copy trait?
+#[derive(Clone, Debug, Eq, PartialEq)]
+struct Hero {
+ name: String,
+ level: u32,
+ experience: u32,
+ position: Position,
+}
+
+// we can add methods to structs using the 'impl' keyword
+impl Hero {
+ // static method (in Rust nomenclature: "associated function")
+ fn new(name: String) -> Hero {
+ Hero {
+ name,
+ level: 1,
+ experience: 0,
+ position: Position(0, 0),
+ }
+ }
+}
+
+// multiple impl blocks are possible for one struct
+impl Hero {
+ // instance method, first argument (self) is the calling instance
+ fn distance(&self, pos: Position) -> u32 {
+ // shorthand to: `self: &Self`
+ // field `i` of a tuple or a tuple struct can be accessed through 'tuple.i'
+ (pos.0 - self.position.0).unsigned_abs() + (pos.1 - self.position.1).unsigned_abs()
+ }
+
+ // mutable borrow of self allows to change instance fields
+ fn level_up(&mut self) {
+ // shorthand to: `self: &mut Self`
+ self.experience = 0;
+ self.level += 1;
+ }
+
+ // 'self' is not borrowed here and will be moved into the method
+ fn die(self) {
+ // shorthand to: `self: Self`
+ println!(
+ "Here lies {}, a hero who reached level {}. RIP.",
+ self.name, self.level
+ );
+ }
+}
+
+fn main() {
+ // Calling associated functions requires scope (`::`) operator.
+ let mut hero: Hero = Hero::new(String::from("Ferris"));
+ hero.level_up(); // 'self' is always passed implicitly
+
+ // fields other than 'name' will be the same as in 'hero'
+ let steve = Hero {
+ name: String::from("Steve The Normal Guy"),
+ ..hero
+ };
+
+ assert_eq!(hero.level, steve.level);
+
+ let mut twin = hero.clone();
+
+ // we can compare Hero objects because it derives the PartialEq trait
+ assert_eq!(hero, twin);
+ twin.level_up();
+ assert_ne!(hero, twin);
+ hero.level_up();
+ assert_eq!(hero, twin);
+
+ // we can print out a the struct's debug string with '{:?}'
+ println!("print to stdout: {:?}", hero);
+
+ hero.die(); // 'hero' is not usable after this invocation, see the method's definiton
+
+ // the dbg! macro prints debug strings to stderr along with file and line number
+ // dbg! takes its arguments by value, so better borrow them not to have them
+ // moved into dbg! and consumed.
+ dbg!("print to stderr: {}", &twin);
+
+ let pos = Position(42, 0);
+ let dist = steve.distance(pos); // no clone here as Position derives the Copy trait
+ println!("{:?}", pos);
+ assert_eq!(dist, 42);
+}
+
+
+(Download the source code for this example: data_types.rs)
+It is often the case that we want to define a variable that can only take
+a certain set of values and the values are known up front. In C you can use an enum
for this.
#include <stdio.h>
+
+enum shirt_size {
+ small,
+ medium,
+ large,
+ xlarge
+};
+
+void print_size(enum shirt_size size) {
+ printf("my size is ");
+ switch (size) {
+ case small:
+ printf("small");
+ break;
+ case medium:
+ printf("medium");
+ break;
+ case large:
+ printf("large");
+ break;
+ case xlarge:
+ printf("xlarge");
+ break;
+ default:
+ printf("unknown");
+ break;
+ }
+ printf("\n");
+}
+
+int main() {
+ enum shirt_size my_size = medium;
+ print_size(my_size);
+}
+
+
+(Download the source code for this example: enums.c)
+However, in C enums are just integers. Nothing prevents us from writing
+int main() {
+ enum shirt_size my_size = 666;
+ print_size(my_size);
+}
+
+C++ introduces enum classes which are type-safe. Legacy enums are also somewhat safer than in C (same code as above):
+<source>:27:31: error: invalid conversion from 'int' to 'shirt_size' [-fpermissive]
+ 27 | enum shirt_size my_size = 666;
+ | ^~~
+ | |
+ | int
+
+Some programming languages (especially functional ones) allow programmers to define
+enums which carry additional information. Such types are usually called tagged unions
+or algebraic data types
.
In C++ we can use union
with an enum
tag to define it:
#include <iostream>
+
+// Taken from: https://en.cppreference.com/w/cpp/language/union
+
+// S has one non-static data member (tag), three enumerator members (CHAR, INT, DOUBLE),
+// and three variant members (c, i, d)
+struct S
+{
+ enum{CHAR, INT, DOUBLE} tag;
+ union
+ {
+ char c;
+ int i;
+ double d;
+ };
+};
+
+void print_s(const S& s)
+{
+ switch(s.tag)
+ {
+ case S::CHAR: std::cout << s.c << '\n'; break;
+ case S::INT: std::cout << s.i << '\n'; break;
+ case S::DOUBLE: std::cout << s.d << '\n'; break;
+ }
+}
+
+int main()
+{
+ S s = {S::CHAR, 'a'};
+ print_s(s);
+ s.tag = S::INT;
+ s.i = 123;
+ print_s(s);
+}
+
+
+(Download the source code for this example: tagged_union.cpp)
+C++17 introduced a new feature called variant
which generalizes this concept.
+You can read more about it here.
Java has a more or less analogous feature called sealed classes
+since version 17.
Let's see how they are defined in Rust.
+#![allow(unused_assignments)]
+#![allow(unused_variables)]
+#![allow(dead_code)]
+
+#[derive(Debug)]
+enum NamedSize {
+ Small,
+ Medium,
+ Large,
+ XL,
+}
+
+#[derive(Debug)]
+enum ShirtSize {
+ Named(NamedSize),
+ Numeric(u32),
+}
+
+fn main() {
+ println!(
+ "Isn't it strange that some clothes' sizes are adjectives like {:?},",
+ ShirtSize::Named(NamedSize::Small)
+ );
+ println!(
+ "but sometimes they are numbers like {:?}?",
+ ShirtSize::Numeric(42)
+ );
+}
+
+
+(Download the source code for this example: enums.rs)
+In Rust, enums are a core feature of the language.
+You may have heard that one of Rust's defining characteristics is
+the absence of "the billion dollar mistake".
+So what can we do to say that a value is missing if there is no null
?
In Rust, we can use the Option
type to represent the absence of a value.
Option is defined as:
+enum Option<T> {
+ Some(T),
+ None,
+}
+
+The <T>
part is called the "type parameter" and it causes Option to be generic.
+We won't go deeper into this for now.
The fact that variables which could be null
in other languages have a different type in Rust is
+the solution to the billion dollar mistake!
#![allow(unused_assignments)]
+#![allow(unused_variables)]
+#![allow(dead_code)]
+
+fn main() {
+ let mut not_null: i32 = 42;
+ not_null = 43;
+ // not_null = None; // this won't compile because it's a different type!
+
+ let mut nullable: Option<i32> = Some(42);
+ nullable = None;
+ nullable = Some(43);
+
+ // such construction is rare, but possible
+ let mut double_nullable: Option<Option<i32>> = Some(Some(42));
+ // assert_ne!(double_nullable, Some(42)); // this won't even compile because it's a different type!
+ double_nullable = None;
+ double_nullable = Some(None);
+
+ // None and Some(None) are different!
+ assert_ne!(double_nullable, None);
+
+ // Now recall that division by 0 *panics*
+ // A panic is an unrecoverable error
+ // It is not an exception!
+ // And in Rust there are no exceptions, so there are no try/catch blocks
+ // Now let's imagine that we want to divide one number by another
+ fn divide(dividend: i32, divisor: i32) -> i32 {
+ dividend / divisor
+ }
+
+ // We get the divisor from the user, so it can be 0
+ // We want to handle this situation gracefully - we don't want to crash the program!
+ // We can do this by using the Option<T> type
+ fn safe_divide(dividend: i32, divisor: i32) -> Option<i32> {
+ if divisor == 0 {
+ None
+ } else {
+ Some(dividend / divisor)
+ }
+ }
+
+ // Fortunately, such a function is already included in the standard library
+ let number: i32 = 42;
+ // We need to specify the type explicitly
+ // because checked_div is implemented for all integer types
+ // and Rust won't know which type we want to use
+ assert_eq!(number.checked_div(2), Some(21));
+ assert_eq!(number.checked_div(0), None);
+
+ // Now let's imagine we search for a value in an array.
+ let numbers = [1, 2, 3, 4, 5];
+ let three = numbers.iter().copied().find(|&x| x == 3);
+ assert_eq!(three, Some(3));
+ let seven = numbers.iter().copied().find(|&x| x == 7);
+ assert_eq!(seven, None);
+ // We won't delve deeper into the details of how iterators work for now,
+ // but the key takeaway is that there are no sentinel or special values like `nullptr` in Rust
+
+ // Usually there are two kinds of methods:
+ // ones that will panic if the argument is incorrect,
+ // numbers[8]; // this will panic!
+ // and `checked` ones that return an Option
+ assert_eq!(numbers.get(8), None);
+
+ // We can use `unwrap` to get the value out of an Option
+ // but we must be absolutely sure that the Option is Some, otherwise we'll get a panic
+ // numbers.get(8).unwrap(); // this will panic!
+ assert_eq!(numbers.get(8).copied().unwrap_or(0), 0); // or we can provide a default value
+
+ // Usually instead of unwrapping we use pattern matching, we'll get to this in a minute
+ // but first let's see what else we can do with an option
+ let number: Option<i32> = Some(42);
+ // We can use `map` to transform the value inside an Option
+ let doubled = number.map(|x| x * 2);
+ assert_eq!(doubled, Some(84));
+ // We can use flatten to reduce one level of nesting
+ let nested = Some(Some(42));
+ assert_eq!(nested.flatten(), Some(42));
+ // We can use `and_then` to chain multiple options
+ // This operation is called `flatmap` in some languages
+ let chained = number
+ .and_then(|x| x.checked_div(0))
+ .and_then(|x| x.checked_div(2));
+ assert_eq!(chained, None);
+
+ // The last two things we'll cover here are `take` and `replace`
+ // They are important when dealing with non-Copy types
+ // `take` will return the value inside an Option and leave a None in its place
+ let mut option: Option<i32> = None;
+ // Again, we need to specify the type
+ // Even though we want to say that there is no value inside the Option,
+ // this absent value must have a concrete type!
+ assert_eq!(option.take(), None);
+ assert_eq!(option, None);
+
+ let mut x = Some(2);
+ let y = x.take();
+ assert_eq!(x, None);
+ assert_eq!(y, Some(2));
+
+ // `replace` can be used to swap the value inside an Option
+ let mut x = Some(2);
+ let old = x.replace(5);
+ assert_eq!(x, Some(5));
+ assert_eq!(old, Some(2));
+
+ let mut x = None;
+ let old = x.replace(3);
+ assert_eq!(x, Some(3));
+ assert_eq!(old, None);
+}
+
+
+(Download the source code for this example: option.rs)
+Pattern matching is a powerful feature of Rust and many functional languages, but it's slowly making +its way into imperative languages like Java and Python as well.
+#![allow(dead_code)]
+#![allow(unused_variables)]
+
+fn main() {
+ // Pattern matching is basically a switch on steroids.
+ let number = rand::random::<i32>();
+ match number % 7 {
+ 0 => println!("{number} is divisible by 7"),
+ 1 => println!("{number} is *almost* divisible by 7"),
+ _ => println!("{number} is not divisible by 7"),
+ }
+
+ #[derive(Debug)]
+ enum Color {
+ Pink,
+ Brown,
+ Lime,
+ }
+
+ let color = Color::Lime;
+ match color {
+ Color::Pink => println!("My favorite color!"),
+ _ => println!("Not my favorite color!"), // _ is a wildcard
+ // Rust will statically check that we covered all cases or included a default case.
+ }
+
+ // We can also use pattern matching to match on multiple values.
+ match (color, number % 7) {
+ (Color::Pink, 0) => println!("My favorite color and number!"),
+ (Color::Pink, _) => println!("My favorite color!"),
+ (_, 0) => println!("My favorite number!"),
+ (_, _) => println!("Not my favorite color or number!"),
+ }
+ // (This is not special syntax, we're just pattern matching tuples.)
+
+ // But we can also *destructure* the value
+ struct Human {
+ age: u8,
+ favorite_color: Color,
+ }
+
+ let john = Human {
+ age: 42,
+ favorite_color: Color::Pink,
+ };
+
+ match &john {
+ Human {
+ age: 42,
+ favorite_color: Color::Pink,
+ } => println!("Okay, that's John!"),
+ Human {
+ favorite_color: Color::Pink,
+ ..
+ } => println!("Not John, but still his favorite color!"),
+ _ => println!("Somebody else?"),
+ }
+
+ // Note two things:
+ // 1. Color is *not* Eq, so we can't use == to compare it, but pattern matching is fine.
+ // 2. We *borrowed* the value, so we can use it after the match.
+
+ println!("John is {} years old and still kicking!", john.age);
+
+ // To save some time, we can use `if let` to match against only one thing
+ // We could also use `while let ... {}` in the same way
+ if let Color::Pink = &john.favorite_color {
+ println!("He's also a man of great taste");
+ }
+
+ // We can match ranges...
+ match john.age {
+ 0..=12 => println!("John is a kid!"),
+ 13..=19 => println!("John is a teenager!"),
+ 20..=29 => println!("John is a young adult!"),
+ 30..=49 => println!("John is an adult!"),
+ 50..=69 => println!("John is mature!"),
+ _ => println!("John is old!"),
+ }
+
+ // We can use match and capture the value at the same time.
+ match john.age {
+ age @ 0..=12 => println!("John is a kid, age {}", age),
+ age @ 13..=19 => println!("John is a teenager, age {}", age),
+ age @ 20..=29 => println!("John is a young adult, age {}", age),
+ age @ 30..=49 => println!("John is an adult, age {}", age),
+ age @ 50..=69 => println!("John is mature, age {}", age),
+ age => println!("John is old, age {}", age),
+ }
+
+ // We can use guards to check for multiple conditions.
+ match john.age {
+ age @ 12..=19 if age % 2 == 1 => println!("John is an *odd* teenager, age {}", age),
+ age if age % 2 == 0 => println!("John is an *even* man, age {}", age),
+ _ => println!("John is normal"),
+ }
+
+ // Finally, let's look at some references now
+ let reference: &i32 = &4;
+
+ match reference {
+ &val => println!("Value under reference is: {}", val),
+ }
+
+ // `ref` can be used to create a reference when destructuring
+ let Human {
+ age,
+ ref favorite_color,
+ } = john;
+ // `john` is still valid, because we borrowed using `ref`
+ if let Color::Pink = &john.favorite_color {
+ println!("John still has his color - {:?}!", favorite_color);
+ }
+
+ let mut john = john;
+
+ // `ref mut` borrows mutably
+ let Human {
+ age,
+ ref mut favorite_color,
+ } = john;
+ // We use `*` to dereference
+ *favorite_color = Color::Brown;
+ println!(
+ "Tastes do change with time and John likes {:?} now.",
+ john.favorite_color
+ );
+}
+
+
+(Download the source code for this example: pattern_matching.rs)
+We said there are no exceptions in Rust and panics mean errors which cannot be caught.
+So how do we handle situations which can fail? That's where the Result
type comes in.
#![allow(dead_code)]
+#![allow(unused_variables)]
+
+use std::fs::File;
+use std::io;
+use std::io::Read;
+
+// Let's try reading from a file.
+// Obviously this can fail.
+fn first_try() -> io::Result<String> {
+ let file = File::open("/dev/random");
+ match file {
+ Ok(mut file) => {
+ // We got a file!
+ let mut buffer = vec![0; 128];
+ // Matching each result quickly become tedious...
+ match file.read_exact(&mut buffer) {
+ Ok(_) => {
+ let gibberish = String::from_utf8_lossy(&buffer);
+ Ok(gibberish.to_string())
+ }
+ Err(error) => Err(error),
+ }
+ }
+ Err(error) => {
+ Err(error) // This is needed in order to change the type from `io::Result<File>` to `io::Result<()>`
+ }
+ }
+}
+
+// The '?' operator allows us to return early in case of an error
+// (it automatically converts the error type)
+fn second_try(filename: &'static str) -> io::Result<String> {
+ let mut file = File::open(filename)?;
+ let mut buffer = vec![0; 128];
+ file.read_exact(&mut buffer)?;
+ let gibberish = String::from_utf8_lossy(&buffer);
+ Ok(gibberish.to_string())
+}
+
+fn main() {
+ let filenames = [
+ "/dev/random",
+ "/dev/null",
+ "/dev/cpu",
+ "/dev/fuse",
+ "there_certainly_is_no_such_file",
+ ];
+ for filename in filenames {
+ println!("Trying to read from '{}'", filename);
+ match second_try(filename) {
+ Ok(gibberish) => println!("{}", gibberish),
+ Err(error) => println!("Error: {}", error),
+ }
+ }
+}
+
+
+(Download the source code for this example: result.rs)
+Deadline: 23.10.2024 23:59
+ + +