rust lifetime

lifetime

  • Every reference in Rust has its own lifecycle
  • most of the time, Rust’s lifetime is implicit and can be inferred
  • There are two types of life cycle: input life cycle and output life cycle
  • ‘static is a special life cycle annotation

Example of lifetime out of scope

1
2
3
4
5
6
7
8
9
fn main() {
let mut x;
{
let y = String::from("hello");
// x = y; // this is allowed
x = &y; // not allowed. borrowed value (y) does not live long enough
}
println!("Str:{}", x);
}

lifetime checker

Rust compiler’s borrow checker to determine whether a borrow is legal

1
2
3
4
5
6
7
8
// If it returns a reference value, no matter how simple your function is written, it will always report an error `missing lifetime specifier.`
fn longest(x:&str, y:&str) -> &str { /// this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
if x.len() > y.len() {
x
}else{
y
}
}

let’s find out why it is such case

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
// variable to hold the result value
let long_str;

let x = "abc".to_string();
{
let y = "bbccd".to_string();
long_str = longest(x.as_str(), y.as_str());
}
// if x.len() > y.len() then it is OK,the long_str variable will hole x; if not, long_str supposed tohold y, however, y has a smaller scope than x, long_str will hold to a dropped value
println!("Longest str: {}", long_str);
}

Hence, we need lifetime annotation '

1
2
3
4
5
6
7
fn longest<'a>(x:&'a str, y:&'a str) -> &'a str {
if x.len() > y.len() {
x
}else{
y
}
}

deeper understanding

  • When returning a reference value from a function, the lifetime of the return type needs to match the lifetime of one of the parameters

Struct lifetime annotation

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let info = String::from("File not found.");
// 存放结果值的变量
let exc = ImportantExcepiton {
part: info.as_str()
};

println!("{:?}", exc);
}
#[derive(Debug)]
struct ImportantExcepiton<'a> {
part: &'a str,
}
  • lifetime of the field part must be longer than struct

Lifetime Elision

In order to make common patterns more ergonomic, Rust allows lifetimes to be elided in function signatures.
Elision rules are as follows:

  • Each elided lifetime in input position becomes a distinct lifetime parameter.
  • If there is exactly one input lifetime position (elided or not), that lifetime is assigned to all elided output lifetimes.
  • If there are multiple input lifetime positions, but one of them is &self or &mut self, the lifetime of self is assigned to all elided output lifetimes.
  • Otherwise, it is an error to elide an output lifetime.

struct lifetime annotation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

fn main() {
let info = String::from("File not found.");
let exc = ImportantExcepiton {
part: info.as_str()
};

println!("{:?}", exc);
}
#[derive(Debug)]
struct ImportantExcepiton <'a>{
part: &'a str,
}

// the first 'a is decraration, the second is usage
impl<'a> ImportantExcepiton <'a> {
// in return value, 'a is omitted according to Lifetime Elision rule
fn callname(&self ) -> &str{
self.part
}
}

‘static

‘static is a special lifetime that takes up the duration of the entire program, for example all string literals have a ‘static lifetime

rust ownership

ownership rule

  • Each value in Rust has an owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

variable scope

1
2
3
4
5
6
fn main() {
{ // s is not valid here, it’s not yet declared
let s = "hello"; // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no longer valid
}

Move

stack-only data: Copy trait

such as primitive type

1
2
3
4
fn main() {
let x = 5;
let y = x;
}

bind the value 5 to x; then make a copy of the value in x and bind it to y.

for heap variable

1
2
3
4
fn main() {
let s1 = String::from("hello");
let s2 = s1;
}

move-string
ptr, len, capacity is stored in stack, while string value is stored in heap
When we assign s1 to s2, the String data is copied, meaning we copy the pointer, the length, and the capacity that are on the stack. We do not copy the data on the heap
move-string-2
similar to shallow copy

Clone

1
2
3
4
5
6
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);
}

If we do want to deeply copy the heap data of the String, not just the stack data, we can use a common method called clone.

ownership and function

  • Passing a value to a function will result in a move or copy of ownership
  • difference between “stack” and “heap” variables: stack variables will be copied, and heap variables will be moved. When a variable containing heap data leaves the scope, its value will be cleared by the drop function, unless the ownership of the data is moved to another variable
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    fn main() {
    let s = String::from("hello"); // s comes into scope
    takes_ownership(s); // s's value moves into the function...
    // ... and so is no longer valid here

    let x = 5; // x comes into scope

    makes_copy(x); // x would move into the function,
    // but i32 is Copy, so it's okay to still
    // use x afterward

    } // Here, x goes out of scope, then s. But because s's value was moved, nothing
    // special happens.

    fn takes_ownership(some_string: String) { // some_string comes into scope
    println!("{}", some_string);
    } // Here, some_string goes out of scope and `drop` is called. The backing
    // memory is freed.

    fn makes_copy(some_integer: i32) { // some_integer comes into scope
    println!("{}", some_integer);
    } // Here, some_integer goes out of scope. Nothing special happens.

return values and scope

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
fn main() {
let s1 = gives_ownership(); // gives_ownership moves its return
// value into s1

let s2 = String::from("hello"); // s2 comes into scope

let s3 = takes_and_gives_back(s2); // s2 is moved into
// takes_and_gives_back, which also
// moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
// happens. s1 goes out of scope and is dropped.

fn gives_ownership() -> String { // gives_ownership will move its
// return value into the function
// that calls it

let some_string = String::from("yours"); // some_string comes into scope

some_string // some_string is returned and
// moves out to the calling
// function
}

// This function takes a String and returns one
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
// scope

a_string // a_string is returned and moves out to the calling function
}

What if we want to let a function use a value but not take ownership?
that’s reference

reference & borrow

  • & means reference (borrow but not own), default immutable
  • &mut a mutable reference, only one mutable reference allowed in same scope (avoid data racing)
  • Multiple mutable references can be created non-simultaneously by creating a new scope
  • Cannot have mutable and immutable references at the same time
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    fn main() {
    let mut s = String::from("hello");
    {
    let s1 = &mut s;
    } // r1 goes out of scope here, so we can make a new reference with no problems.
    let s2 = &mut s;
    }

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

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{} and {}", r1, r2);
    // variables r1 and r2 will not be used after this point

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

    println!{"{}",r1} // got problem with above mutable borrow
    }

reference as function arguments

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let s1 = String::from("hello");

let len = calculate_length(&s1);

println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize { // s is a reference to a String
s.len()
} // Here, s goes out of scope. But because it does not have ownership of what
// it refers to, nothing happens.

we pass &s1 into calculate_length and, in its definition, we take &String rather than String. These ampersands represent references, and they allow you to refer to some value without taking ownership of it. Because it does not own it, the value it points to will not be dropped when the reference stops being used.
When functions have references as parameters instead of the actual values, we won’t need to return the values in order to give back ownership, because we never had ownership.
We call the action of creating a reference borrowing.

mutable references

1
2
3
4
5
6
7
8
9
fn main() {
let mut s = String::from("hello");

change(&mut s);
}

fn change(some_string: &mut String) {
some_string.push_str(", world");
}

dangling references

  • A pointer refers to an address in memory, but the memory may have been freed and allocated for use by someone else
  • rust,The compiler can guarantee that there will never be dangling references
    1
    2
    3
    4
    5
    6
    7
    8
    fn main() {
    let r = dangle();
    }
    fn dangle() -> &string { // dangle returns a reference to a String
    let s = String::from("hello"); // s is a new String
    &s // we return a reference to the String, s
    } // Here, s goes out of scope, and is dropped. Its memory goes away.
    // Danger
    The solution here is to return the String directly:
    1
    2
    3
    4
    5
    6
    7
    8
    fn main() {
    let string = no_dangle();
    }

    fn no_dangle() -> String {
    let s = String::from("hello");
    s
    }
    This works without any problems. Ownership is moved out, and nothing is deallocated.

slice

  • Slices let you reference a contiguous sequence of elements in a collection rather than the whole collection. A slice is a kind of reference, so it does not have ownership.

string slice

1
2
3
4
5
6
fn main() {
let mut s = String::from("Hello world");

let hello = &s[0..5];
let world = &s[6..11];
}

Rather than a reference to the entire String, hello is a reference to a portion of the String,
With Rust’s .. range syntax, if you want to start at index zero, you can drop the value before the two periods
By the same token, if your slice includes the last byte of the String, you can drop the trailing number.

Note: String slice range indices must occur at valid UTF-8 character boundaries. If you attempt to create a string slice in the middle of a multibyte character, your program will exit with an error.

1
2
3
4
5
6
7
8
9
fn first_word(s :&String) -> &str {
let bytes = s.as_bytes();
for(i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}

String Literals Are Slices

1
2
3
fn main() {
let s = "Hello, world!";
}

The type of s here is &str: it’s a slice pointing to that specific point of the binary.

String Slices as Parameters

  • Pass &str as a parameter, you can receive parameters of type &String and &str at the same time
    1
    fn first_word(s: &String) -> &str
    equivalent to
    1
    fn first_word(s: &str) -> &str

other slices

array slice

1
2
3
4
5
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
}

rust basics

frequently used cmd

1
2
3
4
5
rustc [filename].rs
cargo new [project_name]
cargo build [--release]
cargo run [--release]
cargo check # check whether compile success, no executible output

data type

integer

  • i8,i16,i32,i64,i128,isize,u8,u16,u32,u64,u128,usize,etc
  • isize, usize indicates that the type is determined by the architecture of the computer. For example, on a 32 bit target, this is 4 bytes and on a 64 bit target, this is 8 bytes.
  • 0x: hex,0o Octal,0b binary,starting with b: byte (u8 only)
Number Literals Example
Decimal 98_222
Hex 0xff
Octal 0o77
Binary 0b1111_0000
Byte(u8 only) b’A’

Tuple

  • The length of Tuple is fixed, and the length cannot be changed once declared
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fn main() {
    // tuple could be declared as mut
    let mut tuple_1 = ("Hello", 39, "Years");
    let tuple_2:(i32, &str ) = (1983, "since.");
    tuple_1.0 = "Hi";
    println!("{} {} {}", tuple_1.0, tuple_1.1, tuple_1.2);
    // destructure
    let (a,b) = tuple_2;
    println!("{} {}", a, b);
    }

array

  • arrays in Rust have a fixed length.
  • Vector is similar to an array, it is provided by the standard library, and its length can be changed
1
2
3
4
5
6
7
8
9
10
11
fn main() {

let arr_test:[u8; 3] = [1,2,3];
println!("Number is {},{},{}", arr_test[0],arr_test[1],arr_test[2]);

let arr_test = ["I","love","you"];
println!("You said : {} {} {}", arr_test[0],arr_test[1],arr_test[2]);

let arr_test = [1;3];
println!("Call Num : {}&{}&{}", arr_test[0],arr_test[1],arr_test[2]);
}

String

  • Basic data types are stored on the stack, but the String type is stored on the heap
    1
    let s = String::from("hello");
  • push_str(): append a str slice a string
  • push(): appends a single character to a String
    1
    2
    3
    4
    5
    fn main() { 
    let mut data = String::from("andy");
    data.push_str(" is stronger");
    data.push('!');
    }
  • + operator, chaining strings. the left side of the + operator is the ownership of the string, and the right side is the string slice
  • String is actually a wrapper for Vec, so the length can be measured by the len() method, but note that Len() is not length of character, but byte len
  • String iteration
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    fn main() { 
    let mut data = String::from("andy");
    data.push_str(" is stronger");
    data.push('!');

    for i in data.bytes() {
    ///
    }

    for i in data.chars() {
    ///
    }
    }

Vector

  • Vector is like any other struct. When Vector leaves the scope, the variable value is cleaned up, and all its elements are also cleaned up.
    1
    2
    3
    4
    5
    6
    7
    fn main() {
    let vec: Vec<u16> = Vec::new();
    let vec2: Vec<i32> = vec![3,45] // create vector by macro
    for i in vec2 {
    println!("Vector value is : {}", i);
    }
    }

HashMap

  • HashMap is not preloaded, so it needs to be included use std::collections::HashMap
    1
    2
    3
    4
    5
    6
    7
    use std::collections::HashMap;
    fn main() {
    let keys = vec!["andy".to_string(), "cliff".to_string()] ;
    let ages = vec![38, 26];
    let map :HashMap<_,_> = keys.iter().zip(ages.iter()).collect();
    println!("{:?}", map); /// print {"andy": 38, "cliff": 26}
    }

HashMap ownership

  • For types that implement the Copy trait (such as i32), the value will be copied into the HashMap
  • For values with ownership, such as (String), the value will be moved and ownership will be given to HashMap
  • If a reference to a value is inserted into the HashMap, the value itself does not move

HashMap iteration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::collections::HashMap;

fn main() {
let name = "andy".to_string();
let age = 36;
let mut map = HashMap::new();
map.insert(name, age);
map.insert(String::from("cliff"), 26);
println!("{:?}", &map);
for (k, v) in map {
println!("{} age {}", k, v);
} /// cliff age 26
/// andy age 36
}

update

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use std::collections::HashMap;

fn main() {
let name = "andy".to_string();
let age = 36;
let mut map = HashMap::new();
map.insert(name, age);
map.insert(String::from("cliff"), 26);

let result = map.entry("bob".to_string());
println!("{:?}", result); /// Entry(VacantEntry("bob"))

let result = map.entry("andy".to_string());
println!("{:?}", result); /// Entry(OccupiedEntry { key: "andy", value: 36, .. })

map.entry("bob".to_string()).or_insert(28);
map.entry("cliff".to_string()).or_insert(0);
}

control flow

  • if
    1
    2
    3
    4
    5
    fn main() {
    let condition = 1;
    let x = if condition == 1 { "A" } else { "B" };
    println!("Result x = {}" , x) ;
    }
  • loop
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    fn main() {
    let mut condition = 0;

    let result = 'outer: loop { // 'outer is label
    'inner: loop {
    condition += 1;
    if 3 == condition {
    break 'outer 3 * condition; // break outer loop
    }
    }
    };
    println!("Loop result is : {}", result); /// Loop result is : 9
    }

  • rot
    1
    2
    3
    4
    5
    6
    fn main() {
    let arr = [3,2,3];
    for num in arr.iter() {
    println!("For value is {}", num);
    }
    }

Range iterator

  • Range
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fn main() {
    for number in (1..=3) {
    println!("Number A is {}", number ); /// 1,2,3
    }

    for number in (1..=3).rev() { /// rev means reverse,
    println!("Number B is {}", number ); /// 3,2,1
    }
    }

struct

  • If struct is declared mutable then all fields in the instance are mutable

tuple struct

1
2
struct Color(i32,i32,i32);
let black = Color(0,0,0);

Unit-Like struct

1
struct Man {};

struct method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

fn main() {
let rec = Rectangle {
width: 30,
height: 50,
};

let result = rec.area();
println!("rectangle:{:?},area is:{}", rec, result);
}


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

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

associative func(similar to static method)

  • You can define a function that does not take self as the first parameter in the impl block. This form is called an associated function, and the calling method is similar to String::from()
    1
    2
    3
    4
    5
    6
    7
    8
    impl Rectangle {
    fn create_square(width: u32) -> Rectangle {
    Rectangle {
    width,
    height: width,
    }
    }
    }

enum

1
2
3
4
5
6
7
8
9
enum Ip {
V4,
V6,
}

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

match

  • match must exhaust all possibilities
  • If there are too many matchings, you can also use ““ for wildcarding, but note that ““ must be placed at the end
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    enum Color {
    Red,
    Yellow,
    Blue,
    }
    enum ColorWithVal {
    Red(u8,u8,u8),
    Yellow(u8,u8,u8),
    Blue(u8,u8,u8),
    }
    fn main(){
    let colour = Color::Blue;
    match colour {
    Color::Red => {
    println!("Red colour.");
    },
    _ => {
    println!("Other colour.");
    }
    }

    let colour = ColorWithVal::Red(222,111,22);
    match colour {
    ColorWithVal::Red(r,g,b) => {
    println!("Red colour. {},{},{}", r,g,b);
    },
    _ => {
    println!("Other colour.");
    }
    }
    }

if let

1
2
3
4
5
6
7
8
9
fn main(){
let colour = Color::Red(Some(222),Some(222),Some(222));

if let Color::Red(r,g,b) = colour {
println!("Red colour. {:?},{:?},{:?}", r,g,b);
} else {
println!("Other colour.");
}
}

Result<T,E>

  • Recoverable err via Result<T,E>, non-recoverable via panic!
  • upon panic!, the program will expand an error message, unwind, clean up the call stack (Stack) and finally exit the program
  • You can set panic = ‘abort’ in Cargo.toml to terminate the cleaning of the call stack
    1
    2
    [profile.release]
    panic='abort'
  • RUST_BACKTRACE = 1 prints detailed error messages in the stack
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    use std::fs::File;
    fn main() {
    let fp = File::open("hello.txt");
    let file = match fp {
    Ok(file)=> {
    file
    },
    Err(error) => panic!("file not found {:?} ", error),
    };
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::{fs::File, io::ErrorKind};
fn main() {
let fp = File::open("hello.txt");
let file = match fp {
Ok(file)=> {
file
},
Err(error) => {
match error.kind() {
ErrorKind::NotFound => {
match File::create("hello.txt") {
Ok(file) => {
file
},
Err(err) => {
panic!("file create error:{:?}", &err);
},
}
},
oe => panic!("other error {:?}", oe),
}
} ,
};
}
1
2
3
4
5
6
7
8
9
10
11
12
use std::{fs::File, io::ErrorKind};
fn main() {
let file = File::open("hello.txt").unwrap_or_else(|err| {
if err.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|err|{
panic!("error:{:?}", err);
})
}else{
panic!("other error:{:?}", err);
}
});
}

unwrap && expect

  • If do not want to deal with Err, can use unwarp() method. If result is Ok(val), return val. If Err, then call the panic! macro.
  • expect can specify what the error message is, which is easier to debug

The question mark operator, ?

When writing code that calls many functions that return the Result type, the error handling can be tedious. The question mark operator, ?, hides some of the boilerplate of propagating errors up the call stack.

1
let mut file = File::create("my_best_friends.txt")?;

generic

1
2
3
4
5
6
7
8
9
10
#[derive(Debug)]
struct Point<T, U> {
x : T,
y : U,
}
impl <T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point{x: self.x , y: other.y, }
}
}

trait

definition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pub trait Summary {
fn summarize(&self) -> String {
"... more".to_string() /// default
}
}
pub struct Tweet {
user_name :String,
replay_count :u32,
like_count :u32,
}
impl Tweet {
fn like(&mut self) {
self.like_count += 1;
}
}

impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{} like count :{} , replay count :{}", &self.user_name, &self.replay_count, &self.like_count)
}
}

trait as arguments

1
2
3
4
5
6
7
8
9
10
11
12
13
fn notify_msg <T:Summary> (info: T) {
println!("summary : {}", info.summarize() );
}
fn notify_msg (info: impl Summary + Display) {
println!("summary : {}", info.summarize() );
}
fn notify_msg <T> (info: T)
where
T: Summary + Display,
{
println!("summary : {}", info.summarize() );
println!("display implement info : {}", info);
}

trait as return

  • impl Trait can only return the same type, if it returns a different type, even if the Trait is implemented, an error will be reported

references