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/
Quotes
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 containingkey: 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 tellsprintln!
we want to use an output format calledDebug
.
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 alwaysself
, 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.
Likefrom
inString::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 anOption<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 valueOption<T>
and handle the possibility of it being null.
match
is a powerful control flow operator similar to anif
, but it can match any type.
match coin { Coin::Penny => 1, Coin::Nickel => 5, }An arm of amatch
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. Anelse
inif let
is like_
inmatch
.
Using Modules to Reuse and Organize Code
Themod
keyword declares a new module. Theuse
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 fulla::series::of
path we can now useof
. The use keyword brings only what we’ve specified into scope: it does not bring children of modules into scope.
use TrafficLight::{Red, Yellow};Becauseenums
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}, };Theuse
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.
Foruse
, paths are relative to the crate root by default. Otherwise paths are always relative to the current module. Usesuper::
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(); v.push(5);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 usingget()
allows you to handle the resultingNone
.
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 ini
.
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 aVec<u8>
.
for b in "नमस्ते".bytes() { println!("{}", b); }Thebytes
method returns each raw byte, while.chars()
separates out and returns six values of typechar
.
use std::collections::HashMap; [...] scores.insert(String::from("Blue"), 10);The typeHashMap<K, V>
stores a mapping of keys of typeK
to values of typeV
. 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 usezip
create a vector of tuples, then, withcollect
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 theget
method.
scores.entry(String::from("Blue")).or_insert(50);The return value of theentry
function is an enum calledEntry
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 theOk
variant,unwrap
will return the value inside theOk
. If the Result is theErr
variant,unwrap
will call thepanic!
macro for us.
let f = File::open("hello.txt").expect("Failed to open hello.txt");Usingexpect
instead ofunwrap
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 thefrom
function, which is used to convert errors from one type into another.
As long as each error type implements thefrom
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 }Thevalue
getter method borrowsself
, doesn't have any other parameters and returns ani32
.
A getter method on astructs
allows thevalue
field to remain readable while code using the encapsulatingstruct
is denied changingvalue
directly, which would evade the checks inGuess: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")]Theshould_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 usingResult<T, E>
, returningOk(())
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.
The#[cfg(test)]annotation on the tests module tells Rust to compile and run the test code only when you runcargo test
, not when you runcargo 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 straightforwardsrc/main.rs
file that calls logic that lives in thesrc/lib.rs file
. This allows the functionality in lib.rs to be tested via integration tests.