The Rust Programming Language

Level up not just your knowledge of Rust, but also your reach and confidence as a programmer in general.

Source: https://doc.rust-lang.org/1.30.0/book/2018-edition/


Common Programming Concepts

let tup: (i32, f64, u8) = (500, 6.4, 1);
A tuple is a general way of grouping together some number of other values with a variety of types into one compound type.

Understanding Ownership

let s = String::from("hello world");
let hello = &s[0..5];
A string slice is a reference to part of a String. The type that signifies “string slice” is written as &str.

let s = "Hello, world!";
String literals are slices. Other types of collections, like arrays, may also be sliced.

Using Structs to Structure Related Data

struct User { username: String, email: String };
A struct, or structure, is a custom data type that lets you name and package together multiple related values that make up a meaningful group. A field is the name and type of a piece of data in the struct.

let user1 = User { email: String::from("someone@example.com"),
                   username: String::from("someusername123") };
We create an instance by stating the name of the struct and then add curly brackets containing key: value pairs.

let user2 = User { ..user1 };
The syntax .. specifies that the remaining fields not explicitly set should have the same value as the fields in the given instance.

struct Color(i32, i32, i32);
Tuple structs have the added meaning the struct name provides but don’t have names associated with their fields.

println!("rect1 is {:?}", rect1);
Putting the specifier :? inside the curly brackets tells println! we want to use an output format called Debug.

impl Rectangle { fn area(&self) -> u32 { self.width * self.height } }
Methods are similar to functions, but they're defined within the context of a struct (or an enum or a trait object) and their first parameter is always self, which represents the instance of the struct.

Associated Functions are functions defined within impl blocks that do not take self as a parameter. They let you namespace functionality (::) that is particular to your struct without having an instance available.

Like from in String::from, associated functions are often used for constructors that will return a new instance of the struct.

Enums and Pattern Matching

enum IpAddr { V4(u8,u8,u8,u8), V6(String), }
Enums allow you to define a type by enumerating its possible values. The possible kinds of an enum are called variants.

enum Option<T> { Some(T), None, }
Rust does not have nulls, but it does have an enum that can encode the concept of a value being present or absent.

let absent_number: Option<i32> = None;
The <T> generic type parameter represents any one piece of data of any type.

The compiler won't let us use an Option<T> value as if it were definitely a valid value. In order to have a value that can possibly be null, you must explicitly opt in by making the type of that value Option<T> and handle the possibility of it being null.

match is a powerful control flow operator similar to an if, but it can match any type.

match coin { Coin::Penny => 1, Coin::Nickel => 5, }
An arm of a match expression is a pattern and the code to execute.

The _ pattern will match any value.

if let Some(3) = some_u8_value { println!("three"); }
if let may be used instead of match to run code that should only execute if one specific pattern matches. An else in if let is like _ in match.

Using Modules to Reuse and Organize Code

The mod keyword declares a new module. The use keyword brings modules into scope. mod client; means "the contents of file client.rs".

If an item is private, it can be accessed only by its immediate parent module and any of the parent's child modules

use a::series::of; fn main() { of::nested_modules(); }
Rather than using the full a::series::of path we can now use of. The use keyword brings only what we’ve specified into scope: it does not bring children of modules into scope.

use TrafficLight::{Red, Yellow};
Because enums also form a sort of namespace like modules, we can bring an enum's variants into scope as well.

use foo::{ bar::{self, Foo}, baz::{*, quux::Bar}, };
The use declaration supports nesting to help you group imports in the same declaration and avoid repeating the base modules' name.

use TrafficLight::*;
To bring all the items in a namespace into scope at once, we can use the * glob operator. Use this sparingly. It might pull in more items than you expect, causing naming conflicts.

For use, paths are relative to the crate root by default. Otherwise paths are always relative to the current module. Use super:: to access the parent module and thus get from the current module to sibling modules.

Common Collections

Collections, like vector, string and hash map, can contain multiple values. Their data lives in the heap and does not need to be known at compile time.

let v = vec![1, 2, 3];
let mut vv = Vec::new();
When values are provided, Rust will infer the types the vector holds.

let out_of_range = 100;
let does_not_exist = &v[out_of_range];
let does_not_exist = v.get(out_of_range);
Using indexing syntax will have your program crash, while using get() allows you to handle the resulting None.

for i in &v { println!("{}", i); }
Iterating over immutable references to elements of a vector versus iterating over mutable ones:
for i in &mut matuble_v { *i += 50; }
The dereference operator * gets us the value in i.

et mut s = String::from("lo"); s.push('l');
The push method takes a single character as a parameter and adds it to the String. posh_str pushes a slice.

A String is a wrapper over a Vec<u8>.

for b in "नमस्ते".bytes() { println!("{}", b); }
The bytes method returns each raw byte, while .chars() separates out and returns six values of type char.

use std::collections::HashMap; [...] scores.insert(String::from("Blue"), 10);
The type HashMap<K, V> stores a mapping of keys of type K to values of type V. You can create an empty hash map with new and add elements with insert.

let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
Given two separate vectors, we can use zip create a vector of tuples, then, with collect turn that vector of tuples into a hash map.

let score = scores.get(&team_name);
We can get a value out of the hash map by providing its key to the get method.

The return value of the entry function is an enum called Entry that represents a value that might or might not exist. This only inserts a value if the key has none yet.

Error Handling

let f = File::open("hello.txt").unwrap();
If the Result value is the Ok variant, unwrap will return the value inside the Ok. If the Result is the Err variant, unwrap will call the panic! macro for us.

let f = File::open("hello.txt").expect("Failed to open hello.txt");
Using expect instead of unwrap and providing good error messages can convey your intent and make tracking down the source of a panic easier.

File::open("hello.txt")?.read_to_string(&mut s)?;
Values taken by ? go through the from function, which is used to convert errors from one type into another.

As long as each error type implements the from function to define how to convert itself to the returned error type, ? takes care of converting errors from one type to another automatically.

Functions often have contracts their behavior is only guaranteed if the inputs meet particular requirements. Panicking when the contract is violated makes sense because a contract violation always indicates a caller-side bug.

continue starts the next iteration of the current loop.

pub fn value(&self) -> i32 { self.value }
The value getter method borrows self, doesn't have any other parameters and returns an i32.

A getter method on a structs allows the value field to remain readable while code using the encapsulating struct is denied changing value directly, which would evade the checks in Guess:new.

Generic Types, Traits, and Lifetimes (TODO)

Writing Automated Tests

#[derive(PartialEq, Debug)]
For structs and enums that you define, you’ll need to implement PartialEq and Debug in order to use them in the likes of assert_eq!.

assert!( result.contains("Carol"), "Failed. Value was `{}`", result);
Tests can contain adding custom failure Messages.

#[should_panic(expected = "must be less than 101")]
The should_panic attribute makes a test pass if the code inside the function panics. expected makes sure that the failure message contains the provided text.

fn it_works() -> Result<(), String> { ... }
Tests can be asserted using Result<T, E>, returning Ok(()) for the success case, and an Err with a String inside for the failure case.

We can run all the tests in a module by filtering on the module's name.

annotation on the tests module tells Rust to compile and run the test code only when you run cargo test, not when you run cargo build.

mod common; [...] common::setup()
Files in subdirectories of the tests directory don't get compiled as separate crates or have sections in the test output. By putting helper functions into tests/common/mod.rs, we avoid them being considered and executed as tests.

Rust projects that provide a binary have a straightforward src/main.rs file that calls logic that lives in the src/lib.rs file. This allows the functionality in lib.rs to be tested via integration tests.