rustlings¶
Day 01 - rustlings 00-10¶
Selected questions
Variables¶
- must use
let, type can be inferred automatically, exceptconst. constmust be named with a type.- Only var with
mutcan be mutable. - 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
#includewhile class methods are defined byimpl.
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¶
use- Create:
let mut map = HashMap::new(); - Insert:
- Must exist:
map.insert(key, value) - Not sure:
map.entry(key).or_insert(value)
- Must exist:
- Get:
map.get(key)->Some(value) - Loop:
- Readonly:
for (key, value) in &map {} - Mutable:
for (key, value) in &mut map {} - for keys / values only:
for key in map.keys(),for value in map.values() map.iter()- Consumable iteration (move the ownership, i.e. cannot be used next time):
map.into_iter()
- Readonly:
- Update:
- Must exist:
map.insert(key, value) - Not sure:
map.and_modify(key, value)
- Must exist:
- 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;
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()的值u80boolfalseString""Vec<T>[]Option<T>None
-
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)
}
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),
}
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?¶
- We can (obviously we do not want) let it crash, by compiler errors, or
panic(just likeassertin python) - Just like
tryandexceptin python, we can handle the known issue we might have and let the program continues. We useResult<T, E>syntax in rust with reasons inE, likeTypeErrorin python.- For catch-all. Sometimes we do not care about why, just like
exceptwithout specific error in python. we can useBox<dyn Error>inerrors5.rsor just self-buit message likeErr(format!("Empty names aren't allowed"))inerrors1.rs. However, these are not standarised for re-use. - Preferably, we define the error types in
enum(now like C) and then map the error for readability.
- For catch-all. Sometimes we do not care about why, just like
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>
whereO: 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, whilefnis 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 theIterator::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¶
hashmap.values()orhashmap.keys()returns iterators, there is nohashmap.values().iter()orhashmap.keys()- return value of
hashmap.values()orhashmap.keys()is reference! One must dereference it to use the values. - 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| xrather than|acc, x| **x
- the reason why
fold(init_value, |acc, x|)should tell whataccshould be mutated, andacccannot 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!- the above
fold()can be simplified asfilter(), 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