Personal notes on my journey to mastering Rust.
- default types:
i32
,f64
usize
is big enough to store ANY pointer / offset in a data structureassert!(0.1+0.2==0.3);
fails coz0.1
,0.2
’s default type isf64
so0.1 = 0.100000000000002
etc, but(0.1_f32 + 0.2+f32 == 0.3)
passes- for
i
in-3..2
{sum += i
} =>sum = -5
char
size = 4 bytes and can hold ANY unicode charbool
size = 1 byte- fn not returning any value returns ‘unit’ type implicitly:
()
. Its size is 0 bytes let x = 5u32; let z = { x = 2 * x; } => z = ()
(unit type, sincex = 2 * x
is a statement, hence returns nothing)let z = { 2 * x } => z = 10_u32
(since2 * x
is an expression, so returns its value)- REMEMBER, ‘;’ makes an expression, a statement. Expressions might also end in ‘;’, making them a statement:
fn main(x: i32, y: i32) -> () { x+y;}
=> main’s return type is()
asx+y;
is a statement
- fn parameters MUST define their types
- diverging fn’s return type is ‘!’ implying it’ll never return to the caller
fn main() -> ! { }
unimplemented!()
,todo!()
,panic!()
are ways to implement diverging fn- Ownership
- a value can only have 1 owner at all times, be it a variable or a function
- once ownership is ‘moved’ from a variable, accessing this variable after will throw error
- mutability cab be changed while ‘moving’ ownership
- once ownership is ‘referenced’ from a variable, accessing this variable after will NOT throw error
- Move: transfers ownership of the value, Reference: creates a reference to owner of value (aka borrowing), Copy: sends copy of value
- Only 1 mutable reference possible OR many immutable references possible, but NOT both, in a given scope. Lemma: But possible in the same scope if immutable reference is not being accessed after definition of mutable reference and vice versa. Lemma is true also for 2 mutable references, not just 1 mut and 1 immut reference
*
- dereference,&
- reference,ref
- reference
let s: Box<str> = "hello, world".into();
into()
- converts static type to heap here (similar to ‘as’)&str
: pronounced string slice- Can ONLY append literals to a string, not another string
(to_string(), String::from())
: &str to String,(as_str(), &)
: String to &strr"Escapes don't work here: \x3F \u{211D}"
printsEscapes don't work here: \x3F \u{211D}
- String slice (i.e. &str) is a view into a heap-allocated string
let s1 = String::from("hi,中国"); let h1 = &s1[3..6];
=>h1
is中
, as中
is 3 bytes long&String -> &str
is implicitly convertible by compiler in rust- tuples with size > 12 cannot be printed directly with
println!()
-
All fields of a struct have to be initialised at the same time. Enum variants can be initialised separately in any order
-
Accessing
- Struct:
struct_instance_name.var_name
- Enum:
EnumName::EnumVariable
- Struct:
-
Struct fields are
name: value
vs Enum variants are mostly onlyname
, but can bename = value
too -
#[derive(Debug)]
trait andprintln!(“{:?}”,struct_instance_name OR EnumName::EnumVariable);
to print compound types -
names being accessed after the loop below will throw error coz ownership of its elements was taken by
name
let names = [String::from("liming"),String::from("hanmeimei")]; for name in names { // Do something with name... }
-
Iterate the indexing and value in
a
let a = [4, 3, 2, 1]; for (i, v) in a.iter().enumerate() { println!("The {}th element is {}", i + 1, v); }
-
Expression is supposed to return a value, statement is not.
-
fn main() { let alphabets = ['a', 'E', 'Z', '0', 'x', '9' , 'Y']; // Fill the blank with `matches!` to make the code work for ab in alphabets { assert!(matches!(ab, 'A'..='Z' | 'a'..='z' | '0'..='9')) } println!("Success!"); }
-
Use
matches!
to compare compound typesenum MyEnum { Foo, Bar } fn main() { let mut count = 0; let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo]; for e in v { if matches!(e, MyEnum::Foo) { // note count += 1; } } assert_eq!(count, 2); println!("Success!"); }
-
if let
>match
when comparing compound types (enums etc) that have max 1 value.match
otherwise. -
match
andif let
can introduce shadowed variables -
Note the partial destructurings in
match
arms (x, y, x and y respectively)struct Point { x: i32, y: i32, } fn main() { // Fill in the blank to let p match the second arm let p = Point { x: 4, y: 10 }; match p { Point { x, y: 0 } => println!("On the x axis at {}", x), // Second arm Point { x: 0..=5, y: y@ (10 | 20 | 30) } => println!("On the y axis at {}", y), Point { x, y } => println!("On neither axis: ({}, {})", x, y), } }
-
if x < split
is called a 'match guard'// Fill in the blank to make the code work, `split` MUST be used fn main() { let num = Some(4); let split = 5; match num { Some(x) if x < split => assert!(x < split), Some(x) => assert!(x >= split), None => (), } println!("Success!"); }
-
On comparing mutable reference in a
match
, the value in the match arm is dereferenced before comparison// FIX the error with least changing // DON'T remove any code line fn main() { let mut v = String::from("hello,"); let r = &mut v; match r { value => value.push_str(" world!") } }
-
Methods of structs are called on the instance of the struct. Functions are called on the type.
struct Circle { x: f64, y: f64, radius: f64 } impl Circle { fn area(&self) -> f64 { std::f64::consts::PI * (self.radius * self.radius) } fn diameter() -> f64 { 2 * 10.0 } } let c = Circle { x: 0.0, y: 0.0, radius: 2.0 }; println!("{}", c.area()); // => Method println!("{}", Circle::diameter()); // => Function
-
self
will take the ownership of current struct instance, however,&self
will only borrow a reference from the instance.// Only fill in the blanks, DON'T remove any line! #[derive(Debug)] struct TrafficLight { color: String, } impl TrafficLight { pub fn show_state(&self) { println!("the current state is {}", self.color); } } fn main() { let light = TrafficLight{ color: "red".to_owned(), }; // Don't take the ownership of `light` here. light.show_state(); // ... Otherwise, there will be an error below println!("{:?}", light); }
- how generic impl for generic sruct is defined
// Add generic for Val to make the code work, DON'T modify the code in `main`. struct Val<T> { val: T, } impl<T> Val<T> { fn value(&self) -> &T { &self.val } } fn main() { let x = Val{ val: 3.0 }; let y = Val{ val: "hello".to_string()}; println!("{}, {}", x.value(), y.value()); }
- method (
mixup()
) of a generic struct (Point<T, U>
) receiving a generic parameter (Point<V, W>
) returning a generic struct (Point<T, W>
)struct Point<T, U> { x: T, y: U, } impl<T, U> Point<T, U> { // Implement mixup to make it work, DON'T modify other code. fn mixup<V, W>(self, z: Point<V, W>) -> Point<T, W> { Point { x: self.x, y: z.y } } } fn main() { let p1 = Point { x: 5, y: 10 }; let p2 = Point { x: "Hello", y: '中'}; let p3 = p1.mixup(p2); assert_eq!(p3.x, 5); assert_eq!(p3.y, '中'); println!("Success!"); }
- a
trait
is like a class for struct, but not exactly. Mainly used when you want some structs to share some functionalities. Notice howsay_hi()
is reimplemented forTeacher
// Fill in the two impl blocks to make the code work. // DON'T modify the code in `main`. trait Hello { fn say_hi(&self) -> String { String::from("hi") } fn say_something(&self) -> String; } struct Student {} impl Hello for Student { fn say_something(&self) -> String { String::from("I'm a good student") } } struct Teacher {} impl Hello for Teacher { fn say_hi(&self) -> String { String::from("Hi, I'm your new teacher") } fn say_something(&self) -> String { String::from("I'm not a bad teacher") } } fn main() { let s = Student {}; assert_eq!(s.say_hi(), "hi"); assert_eq!(s.say_something(), "I'm a good student"); let t = Teacher {}; assert_eq!(t.say_hi(), "Hi, I'm your new teacher"); assert_eq!(t.say_something(), "I'm not a bad teacher"); println!("Success!"); }
- Weird destructuring of struct value (
let &Inches(inches) = self;
),PartialEq
to allow equality comparison,PartialOrd
to allow greater / lesser comparison// `Centimeters`, a tuple struct that can be compared #[derive(PartialEq, PartialOrd)] struct Centimeters(f64); // `Inches`, a tuple struct that can be printed #[derive(Debug)] struct Inches(i32); impl Inches { fn to_centimeters(&self) -> Centimeters { let &Inches(inches) = self; Centimeters(inches as f64 * 2.54) } } // ADD some attributes to make the code work! // DON'T modify other code! #[derive(Debug, PartialEq, PartialOrd)] struct Seconds(i32); fn main() { let _one_second = Seconds(1); println!("One second looks like: {:?}", _one_second); let _this_is_true = (_one_second == _one_second); let _this_is_false = (_one_second > _one_second); let foot = Inches(12); println!("One foot equals {:?}", foot); let meter = Centimeters(100.0); let cmp = if foot.to_centimeters() < meter { "smaller" } else { "bigger" }; println!("One foot is {} than one meter.", cmp); }
- Operator overloading: overloading
*
operator to usea.mul(b)
from standard library, for any type that implementsMul
traituse std::ops; // Implement fn multiply to make the code work. // As mentioned above, `+` needs `T` to implement `std::ops::Add` Trait. // So, what about `*`? You can find the answer here: https://doc.rust-lang.org/core/ops/ fn multiply<T: std::ops::Mul<Output = T>>(a:T, b:T) -> T { a * b // a.mul(b) } fn main() { assert_eq!(6, multiply(2u8, 3u8)); assert_eq!(5.0, multiply(1.0, 5.0)); println!("Success!"); }
- Overloading
+
operator to funny-add 2 structs (Foo + Bar = FooBar
)// Fix the errors, DON'T modify the code in `main`. use std::ops; struct Foo; struct Bar; #[derive(PartialEq, Debug)] struct FooBar; #[derive(PartialEq, Debug)] struct BarFoo; // The `std::ops::Add` trait is used to specify the functionality of `+`. // Here, we make `Add<Bar>` - the trait for addition with a RHS of type `Bar`. // The following block implements the operation: Foo + Bar = FooBar impl ops::Add<Bar> for Foo { type Output = FooBar; fn add(self, _rhs: Bar) -> FooBar { FooBar } } impl ops::Sub<Bar> for Foo { type Output = BarFoo; fn sub(self, _rhs: Bar) -> BarFoo { BarFoo } } fn main() { // DON'T modify the code below. // You need to derive some trait for FooBar to make it comparable. assert_eq!(Foo + Bar, FooBar); assert_eq!(Foo - Bar, BarFoo); println!("Success!"); }
-
All functions of a trait must be implemented by the struct that uses it.
-
trait
as a fn parameter. Used to restrict parameter types to those that implement the trait. Note both implementations ofsummary()
// Implement `fn summary` to make the code work. // Fix the errors without removing any code line trait Summary { fn summarize(&self) -> String; } #[derive(Debug)] struct Post { title: String, author: String, content: String, } impl Summary for Post { fn summarize(&self) -> String { format!("The author of post {} is {}", self.title, self.author) } } #[derive(Debug)] struct Weibo { username: String, content: String, } impl Summary for Weibo { fn summarize(&self) -> String { format!("{} published a weibo {}", self.username, self.content) } } fn main() { let post = Post { title: "Popular Rust".to_string(), author: "Sunface".to_string(), content: "Rust is awesome!".to_string(), }; let weibo = Weibo { username: "sunface".to_string(), content: "Weibo seems to be worse than Tweet".to_string(), }; summary(&post); summary(&weibo); println!("{:?}", post); println!("{:?}", weibo); } // Implement `fn summary` below. fn summary(a: &impl Summary) { let output: String = a.summarize(); println!("{}", output); } //// alternate implementation //// fn summary<T: Summary>(a: &T) { let output: String = a.summarize(); println!("{}", output); }
-
General compiler rule: sizes of return types must be known at compile time.
-
Box
is smart pointer that allocates memory in heap. Used for types whose size is not known at compile time. It allocates and owns the memory in heap. Also deallocates memory when out of scope.&
on the other hand only points to existing memory.Box
can be passed across scopes, but&
has limited lifetime.Box
can be cloned, but&
cannot be.Box
can be used in pattern matching. -
For functions wanting to return different types that implement a given trait, we cannot use
impl Trait
directly as size of return type is unknown at compile time, so need to dynamic dispatch the return value, usingBox<dyn Trait>
struct Sheep {} struct Cow {} trait Animal { fn noise(&self) -> String; } impl Animal for Sheep { fn noise(&self) -> String { "baaaaah!".to_string() } } impl Animal for Cow { fn noise(&self) -> String { "moooooo!".to_string() } } // Returns some struct that implements Animal, but we don't know which one at compile time. // FIX the errors here, you can make a fake random, or you can use trait object. fn random_animal(random_number: f64) -> Box<dyn Animal> { if random_number < 0.5 { Box::new(Sheep {}) } else { Box::new(Cow {}) } } fn main() { let random_number = 0.234; let animal = random_animal(random_number); println!("You've randomly chosen an animal, and it says {}", animal.noise()); }
-
impl Trait
is a specific trait bound. More generally, when working with generics, the type parameters often use traits as bounds to stipulate what functionality a type implements, in various other ways. Below,T
is bound to any type that implementsstd::ops::Add
trait.fn main() { assert_eq!(sum(1, 2), 3); } // Implement `fn sum` with trait bound in two ways. fn sum<T: std::ops::Add<Output = T>>(x: T, y: T) -> T { x + y }
-
More trait bounds. Observe the alternate implementation of creating a struct. Also note
Self == Pair<T>
struct Pair<T> { x: T, y: T, } impl<T> Pair<T> { // Self == Pair<T> fn new(x: T, y: T) -> Self { Self { x, y, } } } impl<T: std::fmt::Debug + PartialOrd + PartialEq> Pair<T> { fn cmp_display(&self) { if self.x >= self.y { println!("The largest member is x = {:?}", self.x); } else { println!("The largest member is y = {:?}", self.y); } } } #[derive(Debug, PartialOrd, PartialEq)] struct Unit(i32); fn main() { let pair = Pair { x: Unit(1), y: Unit(3) }; // alternate version of creating a struct let pair = Pair::new(Unit(1), Unit(3)); pair.cmp_display(); }
-
hatch_a_bird()
returns aBox<dyn Bird>
, which is a trait object, meaning it can return any type that implements theBird
trait, without knowing the exact type at compile time (also note that return types for all arms inmatch
must be same)trait Bird { fn quack(&self) -> String; } struct Duck; impl Duck { fn swim(&self) { println!("Look, the duck is swimming") } } struct Swan; impl Swan { fn fly(&self) { println!("Look, the duck.. oh sorry, the swan is flying") } } impl Bird for Duck { fn quack(&self) -> String{ "duck duck".to_string() } } impl Bird for Swan { fn quack(&self) -> String{ "swan swan".to_string() } } fn main() { // FILL in the blank. let duck: Duck = Duck; duck.swim(); let bird = hatch_a_bird(2); // This bird has forgotten how to swim, so below line will cause an error. // bird.swim(); // But it can quak. assert_eq!(bird.quack(), "duck duck"); let bird = hatch_a_bird(1); // This bird has forgotten how to fly, so below line will cause an error. // bird.fly(); // But it can quak too. assert_eq!(bird.quack(), "swan swan"); println!("Success!"); } // IMPLEMENT this function. fn hatch_a_bird(species: u8) -> Box<dyn Bird> { match species { 1 => Box::new(Swan), 2 => Box::new(Duck), _ => panic!() } }
-
Array of trait objects. Note how every element of
birds
is a pointer with size ofusize
, coz we can't specifyDuck
orSwan
whose size is unknown at compile time. Also&dyn Bird
==Box<dyn Bird>
trait Bird { fn quack(&self); } struct Duck; impl Duck { fn fly(&self) { println!("Look, the duck is flying") } } struct Swan; impl Swan { fn fly(&self) { println!("Look, the duck.. oh sorry, the swan is flying") } } impl Bird for Duck { fn quack(&self) { println!("{}", "duck duck"); } } impl Bird for Swan { fn quack(&self) { println!("{}", "swan swan"); } } fn main() { // FILL in the blank to make the code work. let birds: [&dyn Bird; 2] = [&Swan, &Duck]; // usize for bird in birds { bird.quack(); // When duck and swan turn into Birds, they all forgot how to fly, only remember how to quack. // So, the code below will cause an error. // bird.fly(); } }
-
Creating a trait for a standard lib type
// FILL in the blanks. trait Draw { fn draw(&self) -> String; } impl Draw for u8 { fn draw(&self) -> String { format!("u8: {}", self) } } impl Draw for f64 { fn draw(&self) -> String { format!("f64: {}", self) } } fn main() { let x = 1.1f64; let y = 8u8; // Draw x. draw_with_box(Box::new(x)); // Draw y. draw_with_ref(&y); println!("Success!"); } fn draw_with_box(x: Box<dyn Draw>) { x.draw(); } fn draw_with_ref(x: &dyn Draw) { x.draw(); }
-
Note:
Box<dyn Foo>
doesn't work indynamic_dispatch(a: &dyn Foo)
trait Foo { fn method(&self) -> String; } impl Foo for u8 { fn method(&self) -> String { format!("u8: {}", *self) } } impl Foo for String { fn method(&self) -> String { format!("string: {}", *self) } } // IMPLEMENT below with generics. fn static_dispatch<T: Foo>(a: T) { a.method(); } // Implement below with trait objects. fn dynamic_dispatch(a: &dyn Foo) { a.method(); } fn main() { let x = 5u8; let y = "Hello".to_string(); static_dispatch(x); dynamic_dispatch(&y); println!("Success!"); }
-
A trait is object-safe when (1) its methods DO NOT return
Self
(2) there are NO generic parameters. Note return type off()
isBox<dyn MyTrait>
, so that its size is known at compile time.trait MyTrait { fn f(&self) -> Box<dyn MyTrait>; } impl MyTrait for u32 { fn f(&self) -> Box<dyn MyTrait> { Box::new(42) } } impl MyTrait for String { fn f(&self) -> Box<dyn MyTrait> { Box::new(self.clone()) } } fn my_function(x: Box<dyn MyTrait>) -> Box<dyn MyTrait> { x.f() } fn main() { my_function(Box::new(13_u32)); my_function(Box::new(String::from("abc"))); println!("Success!"); }
-
String
is a heap-allocated data structure that can be mutated.&str
is a view into a string.String::from()
,push_str()
take literals,push()
takes chars.fn main() { let mut s: String = String::from("hello, "); s.push_str("world"); s.push('!'); move_ownership(&s); assert_eq!(s, "hello, world!"); println!("Success!"); } fn move_ownership(s: &String) { println!("ownership of \"{}\" is moved here!", s) }
-
mutable reference to a string, slices by byte count
// FILL in the blanks fn main() { let mut s = String::from("hello, world"); let slice1: &str = s.as_str(); // other way: &s[..] assert_eq!(slice1, "hello, world"); let slice2 = &s[..=4]; assert_eq!(slice2, "hello"); let slice3: &mut String = &mut s; slice3.push('!'); assert_eq!(slice3, "hello, world!"); println!("Success!"); }
-
you cannot index into a String
-
utf8_slice
crate to slice a string by character countuse utf8_slice; fn main() { let s = "The 🚀 goes to the 🌑!"; let rocket = utf8_slice::slice(s, 4, 5); // Will equal "🚀" }
-
s.chars()
returns characters ins
one by one,s.chars().enumerate()
returns characters with their indexfn main() { let s = String::from("hello, 世界"); let slice1 = &s[..1]; //tips: `h` only takes 1 byte in UTF8 format assert_eq!(slice1, "h"); let slice2 = &s[7..10]; // Tips: `中` takes 3 bytes in UTF8 format assert_eq!(slice2, "世"); // Iterate through all chars in s for (i, c) in s.chars().enumerate() { if i == 7 { assert_eq!(c, '世') } } println!("Success!"); }
-
note
from_utf8(&v).unwrap())
use std::str::from_utf8; fn main() { let mut s = String::new(); s.push_str("hello"); // Some bytes, in a vector let v = vec![104, 101, 108, 108, 111]; // Turn a byte's vector into a String let s1 = from_utf8(&v).unwrap(); assert_eq!(s, s1); println!("Success!"); }
-
compiler increases string's capacity by 2 times, so better to do it ourself
// Output: // 25 // 25 // 25 // Here, there’s no need to allocate more memory inside the loop. fn main() { let mut s = String::with_capacity(25); println!("{}", s.capacity()); for _ in 0..2 { s.push_str("hello"); println!("{}", s.capacity()); } println!("Success!"); }
-
String is a vector that holds utf-8 encoded bytes.
-
vec![1, 2, 3]
==vec!(1, 2, 3)
,Vec::from(arr)
: array to vectorfn main() { let arr: [u8; 3] = [1, 2, 3]; let v = Vec::from(arr); is_vec(&v); let v = vec![1, 2, 3]; is_vec(&v); // vec!(..) and vec![..] are same macros, so let v = vec!(1, 2, 3); is_vec(&v); // In code below, v is Vec<[u8; 3]> , not Vec<u8> // USE Vec::new and `for` to rewrite the below code let mut v1 = Vec::new(); is_vec(&v1); for i in &v { v1.push(*i); } assert_eq!(v, v1); println!("Success!"); } fn is_vec(v: &Vec<u8>) {}
-
v2.extend(&v1)
: appends all elements ofv1
tov2
-
Vec::from(arr)
==arr.into()
==string_var.into()
==string_var.into_bytes()
==[0; 10].into_iter().collect()
(iter().collect()
also works for hashmaps) -
v.get(i)
returnsNone
ifi
is out of bounds without panickingfn main() { let mut v = Vec::from([1, 2, 3]); for i in 0..5 { println!("{:?}", v.get(i)) // get returns Option<&i32> i.e. Some, None } for i in 0..5 { match v.get(i) { Some(e) => v[i] = e + 1, None => v.push(i + 2) } } assert_eq!(v, vec![2, 3, 4, 5, 6]); println!("Success!"); }
-
when a
vector
andstring
are reallocated, their capacity is doubled. -
Hashmaps are an unordered collection of key-value pairs.
Hashmap:remove()
to delete a key-value pair. Allows insert, delete, and lookup in O(1) time (and O(n) in worst case) -
hashmap insertion convenience functions
use std::collections::HashMap; fn main() { // Type inference lets us omit an explicit type signature (which // would be `HashMap<&str, u8>` in this example). let mut player_stats = HashMap::new(); // Insert a key only if it doesn't already exist player_stats.entry("health").or_insert(100); assert_eq!(player_stats["health"], 100); // Insert a key using a function that provides a new value only if it // doesn't already exist player_stats.entry("health").or_insert_with(random_stat_buff); assert_eq!(player_stats["health"], 100); // Ensures a value is in the entry by inserting the default if empty, and returns // a mutable reference to the value in the entry. let health = player_stats.entry("health").or_insert(50); assert_eq!(health, &100); *health -= 50; assert_eq!(*health, 50); println!("Success!"); } fn random_stat_buff() -> u8 { // Could actually return some random value here - let's just return // some fixed value for now 42 }
-
only types that implement
Hash
andEq
can be used as keys in a hashmap. -
ways to approximately reduce the size of a hashmap. Note that the approximation here is due to some internal compiler stuff.
use std::collections::HashMap; fn main() { let mut map: HashMap<i32, i32> = HashMap::with_capacity(100); map.insert(1, 2); map.insert(3, 4); // Indeed ,the capacity of HashMap is not 100, so we can't compare the equality here. assert!(map.capacity() >= 100); // Shrinks the capacity of the map with a lower limit. It will drop // down no lower than the supplied limit while maintaining the internal rules // and possibly leaving some space in accordance with the resize policy. map.shrink_to(50); assert!(map.capacity() >= 50); // Shrinks the capacity of the map as much as possible. It will drop // down as much as possible while maintaining the internal rules // and possibly leaving some space in accordance with the resize policy. map.shrink_to_fit(); assert!(map.capacity() >= 2); println!("Success!"); }
-
hashmap takes ownersip of owned variables and copies static types during insertion, unless their reference is passed in
-
#[allow(overflowing_literals)]
to silence error in1000 as u8
which returns232
, and other anomalies#[allow(overflowing_literals)] fn main() { assert_eq!(1000 as u16, 1000); assert_eq!(1000 as u8, 232); // For positive numbers, this is the same as the modulus println!("1000 mod 256 is : {}", 1000 % 256); assert_eq!(-1_i8 as u8, 255); // Since Rust 1.45, the `as` keyword performs a *saturating cast* // when casting from float to int. If the floating point value exceeds // the upper bound or is less than the lower bound, the returned value // will be equal to the bound crossed. assert_eq!(300.1_f32 as u8, 255); assert_eq!(-100.1_f32 as u8, 0); // This behavior incurs a small runtime cost and can be avoided // with unsafe methods, however the results might overflow and // return **unsound values**. Use these methods wisely: unsafe { // 300.0 is 44 println!("300.0 is {}", 300.0_f32.to_int_unchecked::<u8>()); // -100.0 as u8 is 156 println!("-100.0 as u8 is {}", (-100.0_f32).to_int_unchecked::<u8>()); // nan as u8 is 0 println!("nan as u8 is {}", f32::NAN.to_int_unchecked::<u8>()); } }
-
implementing
from
trait auto-implementsinto()
method. Implementingfmt::Display
trait auto-implementsto_string()
method. -
String to i32:
"5".parse().unwrap()
==i32::from_str("5").unwrap()
-
221_u8 * 135_u8
panics due to overflow onu8
. -
v.get(1).unwrap()
returns a reference to the value, not the value itself. -
RUST_BACKTRACE=1 cargo run
to see backtrace. -
Result
is an enum that returnsOk(val)
orErr(msg)
. -
unwrap()
takesResult
as input and returns the value insideOk(val)
, or panics. -
?
is basicallyunwrap()
that returns error instead of panicking.
-
A package (project) can have multiple binary (executables) crates but only one library (reusable code) crate.
-
crate::module_name::function_name()
to call a function in a library,package_name::module_name::function_name()
to call a function in a binary. -
{}
: display notation,{:?}
: debug notation. Custom types need to implementfmt::Display
andfmt::Debug
traits to be used with{}
and{:?}
, inprintln!()
,format!()
, etc to be displayed. Static types have these implemented in std lib. -
beautify struct displays using
{:#?}
(with#[derive(Debug)]
ofc) -
manually modify existing
Debug
trait implementations for a struct. Making it print7
instead ofDeep(Structure(7))
use std::fmt; struct Structure(i32); struct Deep(Structure); // this snippet is from the docs impl fmt::Debug for Deep { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0.0) } } fn main() { // The problem with `derive` is there is no control over how // the results look. What if I want this to just show a `7`? /* Make it print: Now 7 will print! */ println!("Now {:?} will print!", Deep(Structure(7))); }
-
Dangling references: reference to a variable that has gone out of scope. Avoid it by ensuring that variables ALWAYS outlive their references, and not vice versa.
-
Functions accepting references as arguments, need to know their lifetimes to ensure that the reference is valid for the duration of the function call, otherwise the compiler will throw an error.
/* Make it work by adding proper lifetime annotation */ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } fn main() { let x = "long"; let y = "longer"; println!("{}", longest(x, y)); }
-
an example of when the reference does not outlive the function, hence does not compile.
// `'a` must live longer than the function. // Here, `&String::from("foo")` would create a `String`, followed by a // reference. Then the data is dropped upon exiting the scope, leaving // a reference to invalid data to be returned. /* Fix the error in three ways */ fn invalid_output<'a>() -> &'a String { &String::from("foo") } fn main() { let x = invalid_output(); println!("{}", x); }
-
Variable must outlive its reference, and the reference (of parameters, return type) must outlive the function. This won't compile coz the reference
y
does not outlivefailed_borrow()
(read comment). But compiles if'a
is removed from both places.// A function which takes no arguments, but has a lifetime parameter `'a`. fn failed_borrow<'a>() { let _x = 12; // ERROR: `_x` does not live long enough let y: &'a i32 = &_x; // Attempting to use the lifetime `'a` as an explicit type annotation // inside the function will fail because the lifetime of `&_x` is shorter // than `'a` . A short lifetime cannot be coerced into a longer one. }
-
Lifetime elision: automatic lifetime inference of references by compiler. Rules:
- each reference is assigned a lifetime
- if only 1 input reference, its lifetime is assigned to all output references (and if multiple input references, then need to manually assign output reference lifetime)
- if multiple references, but one of them is
&self
or&mut self
, the lifetime ofself
is assigned to all output references
/* The following is a minimum compilable example! */ fn input(x: &i32) { println!("`annotated_input`: {}", x); } fn pass(x: &i32) -> &i32 { x } fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'a str { x } struct Owner(i32); impl Owner { // Annotate lifetimes as in a standalone function. fn add_one(&mut self) { self.0 += 1; } fn print(&self) { println!("`print`: {}", self.0); } } struct Person<'a> { age: u8, name: &'a str, } enum Either { Num(i32), Ref(&'static i32), } fn main() {}
-
'static
lifetime: lifetime of the entire program.&'static
only indicates that the data can live forever, not the reference, which will be constrained by its scope.'static
can be coerced into a shorter lifetime, but not vice versa.// Make a constant with `'static` lifetime. static NUM: i32 = 18; // Returns a reference to `NUM` where its `'static` // lifetime is coerced to that of the input argument. fn coerce_static<'a>(_: &'a i32) -> &'a i32 { &NUM } fn main() { { // Make an integer to use for `coerce_static`: let lifetime_num = 9; // Coerce `NUM` to lifetime of `lifetime_num`: let coerced_static = coerce_static(&lifetime_num); println!("coerced_static: {}", coerced_static); } println!("NUM: {} stays accessible!", NUM); }
-
i
has'static
lifetime but its reference's lifetime is limited tomain()
. (const i
orstatic i
)'s reference is'static
though (while their lifetime is'static
ofc).use std::fmt::Debug; fn print_it<T: Debug + 'static>( input: T) { println!( "'static value passed in is: {:?}", input ); } // same as above fn print_it1( input: impl Debug + 'static ) { println!( "'static value passed in is: {:?}", input ); } fn print_it2<T: Debug + 'static>( input: &T) { println!( "'static value passed in is: {:?}", input ); } fn main() { // i is owned and contains no references, thus it's 'static: let i = 5; print_it(i); // oops, &i only has the lifetime defined by the scope of // main(), so it's not 'static: print_it(&i); print_it1(&i); // but this one WORKS ! print_it2(&i); }