Using the Rust-book: https://doc.rust-lang.org/book
https://doc.rust-lang.org/book/ch01-00-getting-started.html
- Writing the first lines of code (hello_world)
- Using Cargo, the Rust-package manager
rustup
is a CLI for managing Rust versions and tools.- Rust is ahead-of-time compiled
- Source code can be compiled using the (rustup native)
rustc
-command - Formatting code is easy using the (rustup native) command
rustfmt
- Function-like calls using
!
-signs as suffix indicate the use of a "macro" instead of a "function". For instanceprintln!("hello world")
. cargo
is Rust's Swiss army knife => among others it is a package (crate
) managercargo new
automatically creates a git-repository- Cargo-configuration files follow the TOML-standard
- Convenient commands:
cargo build | run | check
https://doc.rust-lang.org/stable/book/ch02-00-guessing-game-tutorial.html
- The chapter-title is pretty much self-descriptive
- Rust automatically brings a small set of functions into scope, referred to as "the prelude": https://doc.rust-lang.org/stable/std/prelude/index.html
- Bringing other libraries into scope can be done by using the reserved keyword
use
(e.g.use std::io;
) - Rust's (module) namespace-separator is
::
. It can also be used for calling "associated functions" (a.k.a. static functions). For exampleString::new();
. - Variables are immutable by default <3. Use
let
to create a new variable (e.g.let x = "hello world";
) - To allow a variable to be modifyable, use the explicit keyword
mut
(e.g.let mut x = "hello world";
) - Passing variables to other functions explicitly requires the reference-symbol
&
. By default, referenced variables are immutable (even if declared asmut
-variable). To allow the function to edit the referenced variable, explicitly mentionmut
again as follows:let mut some_variable = String::new(); some_function(&mut some_variable);
- Functions may return
Result
-objects (of typeenum
). AResult
is eitherOk
orErr
. IfErr
is returned, it can be caught using the functionexpect("alternative error message here");
. - Print variables using inline
{}
, e.g.:println("x = {}, y = {}", x, y);
- Library-Crates can be declared in
Cargo.toml
in the[dependencies]
-section (+ semantic version number) - Rust's Crate-registry is located at crate.io
Cargo.lock
pins Crate version-numbers, unless the newest patches are explicitly fetched usingcargo update
. In other words, updating from 3.5.5 to 3.5.7 requirescargo update
. Otherwise,cargo.lock
will preserve the installed package with version3.5.5
.- Rust uses
match
expressions to express conditions simlar to Python's lambda-expressions. It consists of "arms", and each arm consists of a "pattern". If the arm's pattern is matched, the corresponding command is executed. A match-expression is finished if one of the arm patterns evaluated to "true". - Rust allows "shadowing" existing variables, e.g. overriding an existing variable "var_a" of type string with a new variable "var_a" with type int.
- Rust knows multiple signed and unsigned integer types, such as i32, u32, i64 and u64. By default, an integer is 32-bit and signed (
i32
). - Types can be expressed using
:
, e.g.let x: i32 = 42
loop { }
is an endless condition-less loop, which can be exited usingbreak
- Match can also be used as inline expression, for example:
let x = match guess.trim().parse() { Ok(num) => num, Err(_) => { // handle error here } };
https://doc.rust-lang.org/stable/book/ch03-00-common-programming-concepts.html
Reseved keywords: https://doc.rust-lang.org/stable/book/appendix-01-keywords.html
- Besides (mutable) variables, there's also the concept of "constants" (
const
). These require type-annotation. - Variables can't be declared in global scope. Constants can.
- Integers range from 8-bit (
[u/i]8
) to 128-bit ([u/i]128
) isize
andusize
adapt to the restrictions of the OS (in most cases 32-bit vs 64-bit)- Possible notations:
Type Value Decimal 98_222
Hex 0xff
Octal 0o77
Binary 0b1111_0000
Byte (u8 only) b'A'
- Rust panics in debug mode if an overflow occurs. In Release-mode, the value wraps around (thus for 8-bit integers, 256 => 0, 257 => 1, etc.)
- There are two type of reserved keywords for floating-points:
f32
andf64
(default) - Boolean type can be used with keyword
bool
- Rust's
char
type is a four-byte Unicode Scalar Value - A "Scalar type" represents a single value (such as int, string, bool and char), whereas "Compound types" groups multilpe values into one type.
- Rust has two Compound types: "array"
[]
and "tuple" (tup
). Example:let x: (i32, f64, u8) = (500, 6.4, 1); let five_hundred = x.0; let six_point_four = x.1; let one = x.2; let test_array = [1, 2, 3]; let two = test_array[1];
- Array-types and length can be declared inline:
let a: [i32; 5] = [1, 2, 3, 4, 5];
- Repeating the same number in an array:
let a = [3; 5]; // a = [3, 3, 3, 3, 3]
- Using array-indices that are out of bounds will produce an error at compile-time. E.g.:
let a: [i32; 3] = [1, 2, 3]; println!("{}", a[9]); // This will not compile
snake case
is the conventional style for declaring functions.- It doesn't matter in which order functions are declared in a source-file.
- Function-parameters require static typing
- Rust distinguishes between "Statements" and "Expressions". Statements are instructions that perform some action and do not return a value. Expressions evaluate to a resulting value.
- Rule of thumb: statements end with a semicolon, whereas expressions do not
- Use of the
return
keyword is optional. A function with return type works as follows:fn five() -> i32 { 5 // Return value is implicit }
- Comments:
//
. There are no block comments in Rust. if
andelse
-expressions have the following shape:if some_condition { // handle some_condition } else if some_other_condition { // handle some_other_condition } else { // handle other cases }
- Inline
if
/else
expressions are also possible:// Note: arms should return the same data-type let some_str = if some_no > 5 { "high" } else { "low" };
- Besides
loop
, Rust haswhile
andfor
keywords to create repeating code blocks loop
-statements are able to return a value using thebreak
-keyword + return-value.for
-loops can be compared with C#'s foreach:let a = [10, 20, 30, 40, 50]; for element in a.iter() { println!("the value is: {}", element); }
- A
Range
can be used in combination with afor
-loop:for number in (1..4) { } // Looping over numbers 1, 2, 3
https://doc.rust-lang.org/stable/book/ch04-00-understanding-ownership.html
- Each value in Rust has a variable that's its "owner". The relation between values and owners is strictly 1-to-1. If the owner goes out of scope, the value will be dropped.
- Besides string literals (stored on the Stack) there is the
String
-type, which is stored on the heap. String
s are mutated via thepush_str
-method:let mut s = String::from("hello"); s.push_str(", world!"); // push_str() appends a literal to a String
- The difference between these two string-types is that string-literals are known at compile-time, whereas
String
is only known at run-time. - Copying the contents of one variable into another, will invalidate the initial variable. For example:
let s1 = String::from("some string"); let s2 = s1; // Invalidate s1, use s2 from now on // This doesn't hold for integer-variables, for which the size is known at compile-time and are therefore stored on the Stack
- Use
clone()
to explicityly copy heap-data to another variable. - Variables that store data on the heap and that are passed into a function are invalid after the function ends (by default) because it is freed at the end of the scope (= end of function). Example:
fn main() { let s = String::from("hello"); // s comes into scope some_function(s); // use s in some_function() and hand over the ownership // s is no longer valid here }
- Variables can be passed to a function without the function taking ownership of the variable by using a reference-indicator
&
. This is called "borrowing":fn main() { some_var = String::from("some_value"); some_function(&some_var); println!("reuse some_var: {}", some_var); } fn some_function(some_var: &String) { // "borrow" value some_var here ... }
- References are immutable by default. They can be made mutable by using the
&mut
-keyword. Only one mutable reference per scope is allowed:let mut some_var = String::from("some_value"); let some_var_ref = &mut some_var; some_var_ref.push_str("s");
- It is possible to pass a part of a string as a reference, also referred to as "string slices":
let s = String::from("some__value"); let hello = &s[0..5]; let world = &s[6..11];
- String literals are of type
&str
(e.g.let s = "some value" <- s is of type &str
);&str
is an immutable reference. - Slices also generalise to other types, such as lists.
https://doc.rust-lang.org/stable/book/ch05-00-structs.html
- A
struct
defines the structure of an object. It can hold together multiple related values. - It's possible to access fields using the dot notation (e.g. user.email)
- Fields can only be updated if the entire struct is marked mutable
- We can use "field init shorthand" to provide values to fields, e.g.:
// Default assignment notation fn build_user(email: String, username: String) -> User { User { email: email, username: username, active: true, sign_in_count: 1, } } // Field init shorthand notation fn build_user(email: String, username: String) -> User { User { email, // Note that we don't explicitly link the value here username, // Here neither active: true, sign_in_count: 1, } }
- This only works if the parameter-name matches the field name
- To copy over some fields from one object to another, use the
..
notation:let user2 = User { email: String::from("[email protected]"), // Set a unique e-mailadress ..user1 // Copy the remaining fields from user 1 }
- If you're in doubt whether to pick a Tuple or Struct, choose the Tuple Struct: a named Tuple:
struct Color(i32, i32, i32);
- To print entire structs, one could use pretty-print-options using
{:?}
or{:#?} instead of
{}and by annotating structs with
#[derive(Debug)]`. - There's a difference between methods and functions; methods are defined within the context of a struct, whereas functions are not. The first parameter of a method is always
self
. Methods are defined within animpl
-block self
can also be borrowed or referenced, just like any other parameterimpl
-blocks can also hold "associated functions", i.e. a function without the "self"-parameter. Calling associated functions can be done using the::
-syntax.- It's possible to have multiple
impl
-blocks
https://doc.rust-lang.org/stable/book/ch06-00-enums.html
- Enums can be used for enumerating several options, e.g.:
enum IpAddrKind { V4, V6 }
- It's also possible to pass parameters (any amount/kind) to enums:
enum IpAddr { V4(String), V6(String) }
- Just like structs, use
impl
to add methods to an enum - Rust doesn't have null values! :party:
- Matching enums and Options is also supported by Rust:
fn plus_one(x: Option<i32>) -> Option<i32> { match x { None => None, // Do nothing if value is None Some(i) => Some(i + 1), // Increment by 1 if value is Some } }
- Matches are exhaustive, thus if you miss one, Rust won't compile
- The "default" match is handled by
_
if let
is another way of expressing amatch
- Larger projects can be structured using Rust's building blocks:
- Packages: A Cargo feature that lets you build, test, and share crates
- Crates: A tree of modules that produces a library or executable
- Modules and use: Let you control the organization, scope, and privacy of paths
- Paths: A way of naming an item, such as a struct, function, or module
- A package consists of at least one crate, which could either be a library or a binary crate. A package can only have zero or one library crates, and any number of binary crates.
- The purpose of a module is mainly to describe the structure of the code and organise it. It also determines the accessability of modules and its content (e.g. a function) by using the keywords
pub
andpriv
. - Paths can be used to describe the location of items in the module tree. It can either be absolute or relative:
// Absolute path crate::front_of_house::hosting::add_to_waitlist(); // Relative path front_of_house::hosting::add_to_waitlist();
- We can use the
as
keyword to name module imports:use std::io::Result as IoResult;
- Imports from the same package can be nested (no
*
, butuse std::{cmp::Ordering, io};
). A wildcard*
brings all items into scope. - The module hierarchy can be separated into files and folders. A nested module is then saved in a separate, similarly named file in a folder with the name of the parent module.
https://doc.rust-lang.org/stable/book/ch08-00-common-collections.html
- Besides the built-in data structures such as list and array, Rust also contains the structures
vector
,string
andhash map
. - A Vector can hold any type of data.
- It can either hold data of a single type (default), or use enums to store different types.
- Strings can be concatenated using the
+
operator. However, this requires borrowing the second argument. - Another way to concat two or more strings is by using the
format!
-macro. It doesn't take ownership of any of its parameters. - Slicing a string in Rust requires a range of indices, not a single index as in other languages. This is because some characters take up more than a single byte, such as characters in the string
some_str="Здравствуйте"
. Slicing this string likesome_str[0..4]
returns the first two characters (because they both take up 2 bytes of space). - It's possible to use the string-operations
chars()
andbytes()
to iterate over a string. - Conclusion: Strings are complicated
TODO: continue @ https://doc.rust-lang.org/stable/book/ch08-02-strings.html