Notes taken while learning Rust. https://doc.rust-lang.org/book/
25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Saura Sambit 93306af364 updated readme 9 달 전
ch01/hello_cargo Delete 'ch01/hello_cargo/.gitignore' 11 달 전
ch02/guessing_game Delete 'ch02/guessing_game/.gitignore' 11 달 전
ch03 Ch03 Homework 11 달 전
ch04/ownership Slice Type 10 달 전
ch05/structure Method Syntax 10 달 전
ch06/pattern The match Operator 10 달 전
ch07/restaurant separated modules into diff files 10 달 전
.gitignore Chapter 3 Completed 11 달 전
LICENSE Added LICENSE and README 11 달 전
README.md updated readme 9 달 전

README.md

The Rust Programming Language

Table of Contents

Chapter 01 and 02

Setting up a new project
cargo new project_name
cd project_name
Compile and run the project
cargo run
I/O Binary
use std::io;

//take user input
let mut far = String::new();
io::stdin().read_line(&mut far).expect("Failed to read line.");
Program entry point
fn main() {}
Println is a not a fn but a macro is RUST, so we use an !
println!();
Variables are immutable by default, so we use mut
let mut var_name = String::new();
Building without compiling
cargo build
Comparing two values
use std::cmp::Ordering;
match var1.cmp(var2) {
    Ordering::Less => println!("Too small!),
    Ordering::Greater => println!("Too bog!"),
    Ordering::Equal => println!("Equal!"),
}
Converting String input to Integer
let num: u32 = num.trim().parse().expect("Please type a number!");
Looping
loop {
    //code to be repeated
}
Handling invalid input
let var: u32 = match var.trim().parse() {
    Ok(num) => num,
    Err(_) => continue,
};

Chapter 03: Common Programming Concepts

Not allowed to use mut with constants

Rust’s naming convention for constants is to use all UPPERCASE with _ between words, _ between numeric literals for improved readability.

const VAR: u32 = 100_000;
Shadowing

Immutable variables’ values can be changed by re declaring the variables with let. The next declaration shadows the prev declaration.

let x = 5;
let x = x*2; //doesn't give an error
Compound data types

Tuple They have a fixed length, they cannot grow or shrink in size, can contain values of separate datatypes.

let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x , y, z) = tup;
println!("The value of y is: {}", y);

Values stored in a tuple can be accessed by there index position using a period . like:

tup.0
tup.1
tup.2

Array Cannot grow or shrink in size. Every element must have the same datatype.

let a = [1, 2, 3, 4, 5];
let a: [i32; 5] = [1, 2, 3, 4, 5];

For repeating a value throughout an array,

let a = [3; 5];

is equivalent to:

let a = [3, 3, 3, 3, 3];

Indexing:

a[0]
a[3]
Functions

Parameters

fn another_function(x: i32) {
    println!("{}", x);
}

Statements & Expressions

let x = 5; //statement
let y = {
    let x =3;
    x + 1 //expression value is returned to be stored in y
};

Expressions do not include ending semicolons. Adding a ; will turn it into a statement.

Return Values

They are not named. Functions return the last expression implicitly. Type declaration after ->

fn five() -> i32 {
    5
}

This also works:

fn five() -> i32 {
    return 5;
}

If a function has a defined return type but no expression to return, it returns an empty tuple () and that gives an error.

Control Flow

If:

let num = 3;
if num < 5 {
    //instructions
}
else {
    //instructions
}

Blocks of code associated with the conditions in if expressions are sometimes called arms. 0 and 1 won’t work instead of false and true. Using if in a let statement:

let cond = true;
let num = if cond {
    5
}
else {
    6
};
println!("{}", number);

if and else arms must have value types that are the same. Rust needs to know during compile time what the type of each variable is.

Loops

break is the return statement for loops.

let mut counter = 0;
let result = loop {
    counter += 1;
    if counter == 10 {
        break counter*2; //returns the value to be stored in result
    }
};

For loop:

for _i in 1..10 {  //
    //statements to be repeated
}

Looping through each element of a collection using a for loop:

let a = [10, 20, 30, 40, 50];
for element in a.iter() {
    println!("{}", element);
}

Countdown:

for num in (1..4).rev() {
    println!("{}", num);
}
println!("LIFTOFF!!!);

Chapter 04: Ownership

Accessing data in a heap is slower than accessing data in a stack because you have to follow a pointer to get there. Each value in rust has a variable that’s called it’s owner, can only be one owner at a time. When owner goes out of scope, value will be dropped.

let s = "hello";  //cannot be mutated

s refers to a string literal where the value of the string is hardcoded.

The String Type

Create String from a string literal like:

let mut s = String::from("hello");  //can be mutated
s.push_str("world");  //append a literal to the string

String is made up of three parts: a pointer to the memory, length and capacity. The metadata is stored in a stack while the string is stored in a heap.

let s1 = String::from("hello");
let s2 = s2;
println!("{}",s1); //will give an error

To ensure memory safety, instead of trying to copy the allocated memory Rust considers s1 to no longer be valid and, therefore, Rust moved s1 into s2.

Clone
let s1 = String::from("hello");
let s2 = s1.clone();

clone is used to deeply copy the heap data of the string and not just the stack data.

Mutable References
fn main() {
    let mut s = String::from("hello");
    change(&mut s);
}
fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

We created a mutable reference with &mut s and accept a mutable reference with some_string: &mut String.

But mutable references have one big restriction: you can have only one mutable reference to a particular piece of data in a particular scope. The following code fails:

let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; //trying to borrow for the second time
println!("{}, {}", r1, r2);

If a variable is borrowed as a mutable once, it cannot be borrowed and used again in the same scope. But, if it is borrowed then the previous borrow gets destroyed and hence cannot be used again.

let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used after this point

let r3 = &mut s; // no problem
println!("{}", r3);

The benefit of having this restriction is that Rust can prevent data races at compile time. A data race is similar to a race condition and happens when these three behaviours occur:

  • Two or more pointers access the same data at the same time.
  • At least one of the pointers is being used to write to the data.
  • There’s no mechanism being used to synchronize access to the data.
Dangling References

In Rust, the compiler guarantees that references will never be dangling references: if you have a reference to some data, the compiler will ensure that the data will not go out of scope before the reference to the data does.

The Slice Type

slice does not have ownership. It lets you reference a contiguous sequence of elements in a collection rather than the whole collection.

A string slice is a reference to a part of a String:

let s = String::from("hello world");
let hello = &s[0..5];  //hello
let world = &s[6..11];  // world

Write a function that takes a string and returns the first word it finds in that string. If the function doesn’t find a space in the string, the whole string must be one word, so the entire string should be returned:

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

Chapter 05: Structs

Defining Struct
struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}
Instantiating Struct
let user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

Creating instances from other instances with struct update syntax:

let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    active: user1.active,
    sign_in_count: user1.sign_in_count,
};

This works too:

let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    ..user1 //rest values are copied from user1
};

Using Tuple Structs w/o named fields to create different types

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

Even if the values are same, black and origin are instances of different tuple structs.

Struct Ownership

Structs require the use of lifetimes to store references to data owned by something else. Lifetimes ensure that the data referenced by a struct is valid for as long as the struct is.

struct User {
    username: &str,
    email: &str,
    sign_in_count: u64,
    active: bool
}

fn main() {
    let user1 = User {
        email: "someone@example.com",
        username: "dasdasd",
        active: true,
        sign_in_count: 1,
    };
}

The above program will give errors saying expected lifetime parameter.

Example program using Struct
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle{width: 30, height: 50};
    println!("The area of the rectangle is {} square pixels.", area(&rect1));
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}
Adding functionality with Derived Traits

Rust cannot use println!() to output the value of a struct unless we use #[derive(Debug)] before the struct definition, like shown:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    println!("rect1 is {:?}", rect1);
}
Method Syntax

Methods are similar to functions: they’re declared with the fn keyword and their name, they can have parameters and a return value, and they contain some code that is run when they’re called from somewhere else. However, methods are different from functions in that 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 the method is being called on.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {         //method defined in the context of a struct
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle{width: 30, height: 50};
    println!("The area of the rectangle is {} square pixels.", area(&rect1));
}

impl (implementation) block now contains the area fn. The first parameter is changed to self and used further. Methods can take ownership of self, borrow self immutably, or borrow self mutably. &self was used because we didn’t want to take ownership as only reading the struct data was needed. If instance change was required the &mut self would have been used instead.

Methods with more parameters
impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle{width: 30, height: 50};
    let rect2 = Rectangle{width: 10, height: 40};
    let rect3 = Rectangle{width: 60, height: 45};
    
    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("CAn rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
Associated functions

impl block allows to define functions within it that don’t take self as a parameter. These are called the associated functions because they’re associated with the struct. They’re not methods because they don’t hava an instance of the struct to work with. They are often used as constructors that will return a new instance of the struct.

impl Rectangle {
    fn square(size: u32) -> Rectangle {         //associated function
        Rectangle { width: size, height: size }
    }
}

Each struct is allowed to have multiple impl blocks.

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

Struct, if used efficiently can make the code clearer and more meaningful.

Chapter 06: Enums and Pattern Matching

Defining and Enum
enum IpAddrKind {
	V4,
	V6,
}
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

Both values IpAddrKind::V4 and IpAddrKind::V6 are of the same type: IpAddrKind

Function that takes any IpAddrKind:

fn route(ip_kind: IpAddrKind) {}

route(IpAddrKind::V4);  //function call
route(IpAddrKind::V6);

Enum inside a struct:

enum IpAddrKind {
	V4,
	V6,
}

struct IpAddr {
	kind: IpAddrKind::V4,
	address: String,
}

let home = IpAddr {
	kind: IpAddr::V4,
	address: String::from("127.0.0.1"),
};

let loopback = IpAddr {
	kindL IpAddrKind::V6,
	address: String::from("::1"),
};

This is the same implementation:

enum IpAddr {
	V4(String),
	V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1));

let loopback = IpAddr::V6(String::from("::1"));

Any kind of data can be put inside an enum variant: strings, numeric or structs for example. Another enum can also be included.

enum Message {
	Quit,                         //no data type associated
	Move {x: i32, y: i32},        //anonymous struct
	Write(String),                //String
	ChangeColor(i32, i32, i32),   //tuple with three i32 values
}
Methods on Enum using impl
impl Message {
	fn call(&self) {

	}
}
let m = Message::Write(String::from("hello"));
m.call();
The Option Enum

Rust does not have null, but it does have an enum that can encode the concept of a value being present or absent. This enum is Option<T> and is defined as:

enum Option<T> {
	Some(T),
	None,
}

It’s included in the prelude, doesn’t need to be brought into the scope explicitly.

let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None;  //type needs to be specified for None
The match Control Flow Operator

Rust has an extremely powerful control flow operator called match that allows you to compare a value against a series of patterns and then execute code based on which pattern matches. Patterns can be made up of literal values, variable names, wildcards, and many other things.

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

fn main() {
    let n = Coin::Penny;
    println!("{}", value_in_cents(n));
}
Patterns that Bind to Values

match arms can bind to the parts of the values that match the pattern:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {:?}!", state);
            25
        },
    }
}
Matching with Option<T>
fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}
let five = Some(5);
let six = plus_one(five);  //Some(5) => Some(5+1)
let none = plus_one(None); //None => None

Matches in Rust are exhaustive, .i.e. we must exhaust every last possibility in order for the code to be valid. Especially in the case of Option<T> with the None case.

The _ Placeholder: We can use _ when we don’t want to list all the possible values. For example when we don’t want to list all 255 values of u8 and only care about 1,3,5,7:

let some_u8_value = 0u8;
match some_u8_value {
    1 => println!("one"),
    3 => println!("three"),
    5 => println!("five"),
    7 => println!("seven"),
    _ => (), // matches all possible values that aren't specified before
}
Concise Control Flow with if let
let some_u8_value = Some(0u8);
match some_u8_value {
	Some(3) => println!("three"),
	_ -> (),
}

The above code snippet can be re-written with if let as:

if let Some(3) = some_u8_value {
	println!("three");
}

else can be included. The block of code that goes with else is the same as the block of code that would go with the _ case in the equivalent match expression.

let mut count = 0;
match coin {
	Coin::Quarter(state) => println!("State quarter from {:?}!", state),
	_ => count += 1,
}

This can be written using if let as:

let mut count = 0;
if let Coin::Quarter(state) = coin {
	println!("State quarter from {:?}!" , state);
}
else {
	count += 1;
}

Chapter 07: Packages and Crates

A crate is a binary or a library. The crate root is a source file that in Rust compiler starts from and makes up the root module of the crate.

A package is one or more crates that provide a set of functionality.

Modules let us organize code within a crate into groups for readability and easy reuse. They also control the privacy of items, which is whether an item can be used by public or is an internal implementation .i.e. private.

Create a new library
cargo new --lib libname

src/lib.rs

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
        fn seat_at_table() {}
    }
    mod serving {
        fn take_order() {}
        fn serve_order() {}
        fn take_payment() {}
    }
}

A module named front_of_house is defined using the mod keyword. Inside modules, there are other modules or there can be definitions for other items, such as structs, enums, constants, traits, etc. By using modules, related definitions can be grouped together and named why they are related, thus making it easier for programmers to navigate through the definitions.

src/main.rs and src/lib.rs are crate roots because they form a module named crate at the root of the crate’s module structure.

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment
Paths for referring to an item in the module tree

A path can take two forms:

  • An absolute path starts from a crate root by using a crate name or a literal crate.
  • A relative path starts from the current module and uses self,super, or an identifier in the current module. Both absolute and relative paths are followed by one or more identifiers separated by double colons (::).
mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}
pub fn eat_at_restaurant() {
    //Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    //Relative path
    front_of_house::hosting::add_to_waitlist();
}

On compiling the code above an error saying that hosting module is private will be displayed.

The way privacy works in RUST is that all items (functions, methods, structs, enums, modules and constants) are private by default.

Exposing paths with the ‘pub’ keyword
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}
pub fn eat_at_restaurant() {
    //Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    //Relative path
    front_of_house::hosting::add_to_waitlist();
}

Adding the pub keyword to mod hosting and fn add_to_waitlist lets us call the function from eat_at_restaurant and the code compiles with no errors. The pub keyword on a module only lets code in its ancestor modules refer to it.

Starting relative paths with ‘super’

We can also construct relative paths that begin in the parent module by using super at the start of the path. This is like starting a filesystem path with the .. syntax.

fn serve_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::serve_order();
    }
    fn cook_order() {}
}
Making structs and enums public

pub can be used to designate structs and enums as public:

  • if we use it before a struct definition, we make the struct public, but the struct’s fields will still be private. We can make each field public or not on a case-by-case basis.
  • if we make an enum public, all of its variants become public.
Bringing pats into scope with ‘use’ keyword

We can bring a path into scope once and then call the items in that path as if they are local items with the use keyword.

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

Adding use and a path in a scope is similar to creating a symbolic link in the filesystem. Paths brought into scope with use also check privacy, like any other paths.

Providing new names with ‘as’ keyword

When there’s a case of name match in the same scope with use, after the path as and a new local name or alias for the type can be specified.

use std::fmt::Result;
use std::io::Result as IoResult;

fn function() -> Result {}

fn function() -> IoResult<()> {}
Re-exporting names with ‘pub use’

When a name is brought into the scope with use, the name available in the new scope is private. pub and use can be combined and used to make the name feel at home as if it has been defined in the current scope.

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

Re-exporting is useful when the internal structure of the code is different from how programmers calling the code would think about the domain.