Skip to content

rustlings

Day 01 - rustlings 00-10

Selected questions

Variables

  1. must use let, type can be inferred automatically, except const.
  2. const must be named with a type.
  3. Only var with mut can be mutable.
  4. vars can be shadowing with a new type/ value.

Move Semantics

Just remind:
1. One variable one ownership
2. Variable can be borrowed, but there is at most 1 mutable borrow simultaneously

Structs

  • why we need "UnitStructs"?

String and &str

  • The methods are weird... Some methods can change the type while somes are not...

Vec

input.iter().map(|element| element + 1).collect(), just like lambda in python.

Modules

  • Like a class without attribute members?
  • No... It is same as #include while class methods are defined by impl.

For the general cases:

概念 C++ 对应 Rust 对应
类型定义 struct, class struct, enum
方法 成员函数 impl 块中的方法
接口/抽象 抽象类, 虚函数 trait + impl
模块/命名空间 namespace, .h 文件 mod, use, crate
#define, template macro_rules!, macro
可选值 指针或 std::optional Option<T>
错误处理 异常 (try/catch) Result<T, E>
内存管理 手动或智能指针 所有权系统 + 生命周期

Day 02 - rustlings 11-13

Hashmaps

  1. use
  2. Create: let mut map = HashMap::new();
  3. Insert:
    1. Must exist: map.insert(key, value)
    2. Not sure: map.entry(key).or_insert(value)
  4. Get: map.get(key) -> Some(value)
  5. Loop:
    1. Readonly: for (key, value) in &map {}
    2. Mutable: for (key, value) in &mut map {}
    3. for keys / values only: for key in map.keys(), for value in map.values()
    4. map.iter()
    5. Consumable iteration (move the ownership, i.e. cannot be used next time): map.into_iter()
  6. Update:
    1. Must exist: map.insert(key, value)
    2. Not sure: map.and_modify(key, value)
  7. Remove: map.remove(key, value)

hashmaps2.rs 似乎有问题,47加一个use可以解决

44     #[cfg(test)]
45     mod tests {
46         use super::*;
47 +++     use std::iter::FromIterator;

hashmaps3.rs is quite interesting

Here is my implementation, which is obviously stupid...

scores.entry(team_1_name)
    .and_modify(|v| {v.goals_scored += team_1_score; v.goals_conceded += team_2_score})
    .or_insert(TeamScores{goals_scored: team_1_score, goals_conceded: team_2_score});
scores.entry(team_2_name)
    .and_modify(|v| {v.goals_scored += team_2_score; v.goals_conceded += team_1_score})
    .or_insert(TeamScores{goals_scored: team_2_score, goals_conceded: team_1_score});

The solution is quite interesting:

// Insert the default with zeros if a team doesn't exist yet.
let team_1 = scores.entry(team_1_name).or_default();
// Update the values.
team_1.goals_scored += team_1_score;
team_1.goals_conceded += team_2_score;

// Similarly for the second team.
let team_2 = scores.entry(team_2_name).or_default();
team_2.goals_scored += team_2_score;
team_2.goals_conceded += team_1_score;

  1. or_default() will insert a default value into hash maps when there is no such entry. The default values of some types are:
    • 类型 Default::default() 的值
      u8 0
      bool false
      String ""
      Vec<T> []
      Option<T> None
  2. or_default -> mut &, i.e. it will return a mutable reference, so changing team_1 is changing the bucket.

Options

Just Some(...) and None

However, the usage of match is appalled: match will move the ownership when matching!

Error Handling (Results)

1-3 and 5 are basic questions.

errors4.rs

Understanding:
1. #[derive(...)] is a macro, using it can add common traits to structs and enums.
2. #[derive(PartialEq, Debug)] add traits PartialEq and Debug to the struct PositiveNonzeroInteger, so that:
1. PartialEq can deal with equality test without changing types of both sides to be exactly same. This is wrong! The types of sides of equality test must be the same. However, it is the trait that enables the equality test to make sense. Without it, there is no defination in the struct and its implementation for , !.
- For example, assert_eq!(PositiveNonzeroInteger::new(10), Ok(PositiveNonzeroInteger(10))); will not yield an error, although the left is a struct and the right is the struct wrapped into an result. They are in the same types. The new impl returns an result as well! Remember, both sides must be in the same type. However, without PartialEq, the == cannot be recognised.
- Comparing PositiveNonzeroInteger::new(10) >= PositiveNonzeroInteger::new(8) is not applicable here, as PartialEq only deal with equality test, while the comparisons are handled by the trait*s PartialOrd (may fail and return None Best effort. If not compariable, return None. e.g. comparing any float number with NaN) or Ord (must use with PartialEq and PartialOrd. Will never fail but may be over-compared Will always give a boolean result or the program will crash. e.g. we may not need a real comparison for a float number and a string We cannot compare a float with a string, the both sides must be in the same type. e.g. comparing any float number with NaN in Ord will crash the program as there is not such trait applied, which is because this camparison is logically nonsense).
2. Debug enables {:?} and {:#?} in println!
3. Self means the *impl
PositiveNonzeroInteger. (Nothing special here, just like self in python.)

For implementation, if value >/== is trivial:

if value > 0 {
    // value is i64 and we need u64 inside the struct
    // use `as` to cast the type
    Ok(PositiveNonzeroInteger(value as u64)) 
} else if value == 0 {
    Err(CreationError::Zero)
} else {
    Err(CreationError::Negative)
}
Although complicated, there is no issue. match is useful as well, the comparison can be handled in braches:
match value {
    n if n > 0 => Ok(PositiveNonzeroInteger(n as u64)), // handle the postive part
    0 => Err(CreationError::Zero),
    _ => Err(CreationError::Negative),
}
However, there is a fancy and idiomatic (地道的, 习语的, 成语的, 合乎语言习惯的) method, given in the sample solution:
use std::cmp::Ordering;
match value.cmp(&0) {
    Ordering::Less => Err(CreationError::Negative),
    Ordering::Equal => Err(CreationError::Zero),
    Ordering::Greater => Ok(Self(value as u64)),
}
impl Ord for i64 gives fn cmp, the return value is of type Ording, which is a enum with only 3 values:
#[repr(i8)]
pub enum Ordering {
    Less = -1,
    Equal = 0,
    Greater = 1,
}

errors6.rs

Now how can we handle errors?
  1. We can (obviously we do not want) let it crash, by compiler errors, or panic (just like assert in python)
  2. Just like try and except in python, we can handle the known issue we might have and let the program continues. We use Result<T, E> syntax in rust with reasons in E, like TypeError in python.
    1. For catch-all. Sometimes we do not care about why, just like except without specific error in python. we can use Box<dyn Error> in errors5.rs or just self-buit message like Err(format!("Empty names aren't allowed")) in errors1.rs. However, these are not standarised for re-use.
    2. Preferably, we define the error types in enum (now like C) and then map the error for readability.

Here, we first create the enum if error types

use std::num::ParseIntError;

#[derive(PartialEq, Debug)]
enum CreationError {
    Negative,
    Zero,
}

Then, we need an impl to parse the error - just like in python, to give definition of the process that 5 + "1" will raise TypeError. But *impl*s cannot exist themself, we need a enum to place them:

#[derive(PartialEq, Debug)]
enum ParsePosNonzeroError {
    Creation(CreationError),
    ParseInt(ParseIntError),
}

impl ParsePosNonzeroError {
    fn from_creation(err: CreationError) -> Self {
        Self::Creation(err)
    }

    // TODO: Add another error conversion function here.
    fn from_parse_int(err: ParseIntError) -> Self {
        Self::ParseInt(err)
    }
}

The enum ParsePosNonzeroError gives almost nothing, just wrap the enum CreationError and ParseIntError (using from std) again. However, with their helps, we can now give impl to translate them. Note that there is noway to give one enum only, as the CreationError and ParseIntError are handlers where ParsePosNonzeroError is the process.

The impl will take the handler in as an argument and then return an error of its *variant*s.

CreationError internal

Lets see the process of

#[test]
fn test_zero() {
    assert_eq!(
        PositiveNonzeroInteger::parse("0"),
        Err(ParsePosNonzeroError::Creation(CreationError::Zero)),
    );
}

The "0" is parsed by PositiveNonzeroInteger::parse()

impl PositiveNonzeroInteger {
    fn new(value: i64) -> Result<Self, CreationError> {
        match value {
            x if x < 0 => Err(CreationError::Negative),
            0 => Err(CreationError::Zero),
            x => Ok(Self(x as u64)),
        }
    }

    fn parse(s: &str) -> Result<Self, ParsePosNonzeroError> {
        // TODO: change this to return an appropriate error instead of panicking
        // when `parse()` returns an error.
        let x: i64 = s.parse().unwrap()...;
        Self::new(x).map_err(ParsePosNonzeroError::from_creation)
    }
}

First, the fn parse deal with "0".parse(). str.parse() is to parse any &str into another type, wrapped by Result <T,E>, which is so general, so the type must be known for compiler. For example

let x = "0".parse(); // Error, the compiler does not know the type of x
let x:i32 = "0".parse(); // Error, the return value of `parse()` is wrapped in `Result <T, E>` 
use std::num::ParseIntError;
let x:Result<i32, ParseIntError> = "0".parse(); // Works, but rare. x = Ok(0)

let x:i32 = "0".parse().unwrap(); // Good, the compiler infer "0".parse() to be i32
let x = "0".parse::<i32>().unwrap(); // Good, the compiler infer x to be i32, as the type of "0".parse() is specified by generic syntax

So now, we ignore the error handling ... after s.parse().unwrap() and get x:i64=0. We now pass the x to Self::new(x), which is PositiveNonzeroInteger:new(x). The new() does match and find return Err(CreationError::Zero). Now, we handle the error: Err(CreationError::Zero).map_err(ParsePosNonzeroError::from_creation). From the basics, the map_err just map all errors (here is Err(CreationError::Zero)) to a specific error Err<e>, where e is returned by a process (here is ParsePosNonzeroError::from_creation). The process ParsePosNonzeroError::from_creation is a impl fn, which is just like function pointer in C. The process ParsePosNonzeroError::from_creation get CreationError::Zero as an argument and find the type is correct (as the parameter is defined as err: CreationError). Now, it will give the return value as e = ParsePosNonzeroError::Creation(CreationError::Zero). The map_err() will wrap the e into an Err<>, therefore, the final result is Err(ParsePosNonzeroError::Creation(CreationError::Zero)), which is correct!

Fix to do

Now, we need to think about what we ignore. If &str.parse() fails, the program will crash when unwrap(). To deal with this, we can match the result:

// this is how we init Result<T,E> var
let res = s.parse::<i64>();
match res {
    Ok(v) => Self::new(v).map_err(ParsePosNonzeroError::from_creation),
    Err(e) => Err(e).map_err(ParsePosNonzeroError::from_parse_int)
}

However, a better way is:

fn parse(s: &str) -> Result<Self, ParsePosNonzeroError> {
    // Return an appropriate error instead of panicking when `parse()`
    // returns an error.
    let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parse_int)?;
    Self::new(x).map_err(ParsePosNonzeroError::from_creation)
}

If s.parse() is successful, then value inside Ok() is extracted and assigned to x; Otherwise, it raises Err(ParseIntError). Then Err(ParseIntError) will be mapped by the process of ParsePosNonzeroError::from_parse_int, from Err(ParseIntError) to Err(Self::ParseInt(err)). Therefore, the final result of this line should be either
- x is assigned to some i64 and then use new() to find the creation error,
- or return Err(ParsePosNonzeroError::ParseInt(ParseIntError)).

A Standarised Way - From Trait

This is actually a manual built From trait. A better way is:

#[derive(PartialEq, Debug)]
enum ParsePosNonzeroError {
    Creation(CreationError),
    ParseInt(ParseIntError),
}

// 标准化的 From 实现
impl From<CreationError> for ParsePosNonzeroError {
    fn from(err: CreationError) -> Self {
        ParsePosNonzeroError::Creation(err)
    }
}

impl From<ParseIntError> for ParsePosNonzeroError {
    fn from(err: ParseIntError) -> Self {
        ParsePosNonzeroError::ParseInt(err)
    }
}

Now, the creation error is handled in a standarised way. We can now simplify the fn parse():

fn parse(s: &str) -> Result<Self, ParsePosNonzeroError> {
    let x: i64 = s.parse()?; // the mapping is handled via `From` automatically
    let v = Self::new(x)?; // the mapping is handled via `From` automatically
    Ok(v) // The v is now unwrapped if successful. To make return value in the same type, wrap it again.
}
map_err Internal

What is map_err? What are the parameters?

pub fn map_err<F, O>(self, op: O) -> Result<T, F>
where O: FnOnce(E) -> F,

Maps a Result to Result by applying a function to a contained Err value, leaving an Ok value untouched.

This function can be used to pass through a successful result while handling an error.

Examples

fn stringify(x: u32) -> String { format!("error code: {x}") }  

let x: Result<u32, u32> = Ok(2);  
assert_eq!(x.map_err(stringify), Ok(2));  

let x: Result<u32, u32> = Err(13);  
assert_eq!(x.map_err(stringify), Err("error code: 13".to_string()));  

Given a:<Result<T,E>>, a.map_err(Op) is equivlent to a match statement:

match a {
    Ok(v) => Ok(v), // leave the T part not touched
    Err(e) => Err(Op(e)),
};

where Op: FnOnce is a one-time (means the ownership will be moved after the first call - we do not want to touch an error twice) function pointer or closure pointer. Op(e) will return another e, which is why it is called map_err().

Generic

Traits

fn some_func(item: impl SomeTrait + OtherTrait) -> bool {
    item.some_function() && item.other_function()
}

If your function is generic over a trait but you don't mind the specific type, you can simplify the function declaration using impl Trait as the type of the argument.

特性 / Feature 静态分发(泛型) / Static Dispatch (Generics) 动态分发(dyn Trait) / Dynamic Dispatch (dyn Trait)
类型已知时间 / Type Known 编译时 / Compile-time 运行时 / Runtime
性能 / Performance 更快,可内联 / Faster, can be inlined 稍慢(vtable 查询) / Slightly slower (vtable lookup)
二进制大小 / Binary Size 可能变大 / May increase 更小(单份代码) / Smaller (single code copy)
灵活性 / Flexibility 不能混多种类型 / Cannot mix multiple concrete types 可以存放多种类型 / Can store multiple different types
常见场景 / Common Use Cases 数值计算、性能敏感代码 / Numeric computation, performance-critical code 插件系统、GUI 组件、多态容器 / Plugin systems, GUI components, heterogeneous containers
特性 / Feature 泛型参数 / Generic Parameter 关联类型 / Associated Type
定义位置 / Definition Location trait MyIterator<T> { fn next(&mut self) -> Option<T>; } trait MyIterator { type Item; fn next(&mut self) -> Option<Self::Item>; }
类型绑定时间 / Type Binding Time 使用时指定 / At usage: fn foo<I: MyIterator<i32>>(iter: I) 实现时指定 / At implementation: impl MyIterator for Counter { type Item = i32; ... }
灵活性 / Flexibility 同一类型可多次实现不同版本 / Same type can have multiple versions 一个类型只能有一个固定版本 / One fixed version per type
可读性 / Readability 多个 trait 会很啰嗦 / Verbose with multiple traits 更简洁 / Cleaner syntax
示例 / Example impl MyIterator<i32> for Counter { ... } impl MyIterator for Counter { type Item = i32; ... }

Lifetimes

If a member in struct is a reference, it must explicitly specified with lifetime!

Tests

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn correct_width_and_height() {
        // TODO: This test should check if the rectangle has the size that we
        // pass to its constructor.
        let rect = Rectangle::new(10, 20);
        assert_eq!(rect.width, 10); // Check width
        assert_eq!(rect.height, 20); // Check height
    }

    // TODO: This test should check if the program panics when we try to create
    // a rectangle with negative width.
    #[test]
    #[should_panic]
    fn negative_width() {
        let _rect = Rectangle::new(-10, 10);
    }
}

Day 03

I'm lazy...

Day 04

Closure

In the Rust book, closure is introduced before iterators. Therefore, I would like to first take a look of it.

Is closure a function?

Chatgpt tells me that the closure is not as same as function:

let add_one_v2 = |x: i32| -> i32 { x + 1 };

Does it move the value of closure to a function, so we now have a named function instead of an anonymous closure?
No! Closure is a anonymous struct with trait, while fn is a type already.

Lets take a test

fn print_type_of<T>(_: &T) {
    println!("{}", std::any::type_name::<T>());
}

fn add_fn(x: i32) -> i32 { x + 1 }

fn main() {
    let add_closure = |x: i32| -> i32 { x + 1 };

    print_type_of(&add_fn);
    print_type_of(&add_closure);
}
playground::add_fn
playground::main::{{closure}}

Difference between closures and functions

The most important one is closure can capture other vars!

In function, one must declare the parameters before using, while in closure, one might add more known variables into process.

let factor = 10;
let multiply_by_factor = |x: i32| x * factor;

There is no need to add factor in | |, and compile smoothly.

Also, it is not same implementation to the version with 2 params:

let multiply = |x: i32, y: i32| x * y;  

println!("{}", multiply_by_factor(5)); // the factor is capture automatically. It will always be that factor.  
println!("{}", multiply(5, factor)); // the multiplier can be changed now, however, it must be passed in all the time.  

补充捕获方式是自动推断
编译器会为第一个闭包生成一个带字段 factor 的结构体,而第二个闭包是空结构体

Three Closures

As said closures are anonymous structs with trait, and there is actually 3 type of traits: 1. Fn: capture environments as &T - environments are immutable.
2. FnMut: capture environments as &mut T, environments are mutated.
3. FnOnce: capture environments as T - environments are moved, and thus the closure can be used once only.

|参数列表| 里的是调用时的值,不受闭包捕获方式影响。
捕获方式只和闭包用到的外部变量有关(比如 factor)。

Iterators

There are three common methods which can create iterators from a collection:

  • iter(), which iterates over &T.
  • iter_mut(), which iterates over &mut T.
  • into_iter(), which iterates over T.

iterators3.rs - short-circuiting in collect()

fn divide(a: i64, b: i64) -> Result<i64, DivisionError> {...};

// TODO: Add the correct return type and complete the function body.
// Desired output: `[Ok(1), Ok(11), Ok(1426), Ok(3)]`
fn list_of_results() -> Vec<Result<i64, DivisionError>> {
    let numbers = [27, 297, 38502, 81];
    numbers.into_iter().map(|n| divide(*n, 27)).collect::<Vec<Result<i64, DivisionError>>>()
}

For a normal loop method, we need to: + changed numbers into iterator,
+ for each iterator, dereference it and match it to unwrap or return Err,
+ Other wise return Ok<Vec<i64>>.

However, collect() can do this job for us. It will change the type to what we want, and return the first error, which is called short-circuiting. To use this feature, the struct must have the trait FromIterator:

std::iter
Trait FromIteratorCopy item path

pub trait FromIterator<A>: Sized {
    // Required method
    fn from_iter<T>(iter: T) -> Self
       where T: IntoIterator<Item = A>;
}

Conversion from an Iterator.

By implementing FromIterator for a type, you define how it will be created from an iterator. This is common for types which describe a collection of some kind.

If you want to create a collection from the contents of an iterator, the Iterator::collect() method is preferred. However, when you need to specify the container type, FromIterator::from_iter() can be more readable than using a turbofish (e.g. ::<Vec<_>>()). See the Iterator::collect() documentation for more examples of its use.

into_iter() - return <&integer>

numbers.into_iter().map(|n| divide(*n, 27)).collect::<Vec<Result<i64, DivisionError>>>()

must be divide(*n, 27)

iterators3.rs

  1. hashmap.values() or hashmap.keys() returns iterators, there is no hashmap.values().iter() or hashmap.keys()
  2. return value of hashmap.values() or hashmap.keys() is reference! One must dereference it to use the values.
  3. the closure will add another reference to the value, so one must add && in the closure parameter!
    • the reason why (2..=num).fold(1, |acc, x| acc * x) is allows is because: + can dereference the value automatically!
    • it is safer to use |acc, &&x| x rather than |acc, x| **x
  4. fold(init_value, |acc, x|) should tell what acc should be mutated, and acc cannot be changed!
    for example,
    (2..=num).fold(1, |acc, x| acc * x) // good, acc * x, no assignment
    (2..=num).fold(1, |acc, &x| acc * x) // good, automatically dereferenced
    
    map
        .iter()
        .fold(0, |count, (_k, v)| {
            if **v == value {
                count + 1
            } else {
                count
            }
        })                  // good, return either count or count+1
    
    map
        .iter()
        .fold(0, |count, (_k, v)| {
            if **v == value {
                count += 1;
            }
            count 
        })                  // Wrong! You cannot change count!
    
  5. the above fold() can be simplified as filter(), when we need to filter according to values.

Day 05

Smart Pointers

Arc: Atomic operation Box: Dynamic allocation
Cow: Copy-on-write: borrow when read only, owned when mutation
rc: Counter of references

Threads

工具 作用 是否保证可变性 是否线程安全
Arc<T> 让多个线程共享数据所有权(引用计数) **仅引用计数**线程安全
Mutex<T> 在同一时刻只允许一个线程访问数据 是(通过互斥锁保证)
Arc<Mutex<T>> 多线程共享并安全修改数据

Day 06

finish rustlings

  • Notes are missing