Rust learning notes
About
极客时间 rust第一课笔记
Foundation
at compile time, the variables that we do not know the exact size, or the size can change, we need to put it on
heap
instead ofstack
eg array that has a changing size
linked list
hash table
or the variable that needs to be shared between threads
primitive type is the basic one provided by language, eg int, float, tupe, array, closure, their sizes are fixed, so it can be stored on stack
composite type
structure type
tagged union
enumerate
a
pointer
that contains address, and extra info, eg length of string, isfat pointer
closure
stores function and its context together, such that it can use the variables in the contextinterface
separates the usage and its implementation, which reflects the abstraction of the systemtrait
in rustwe need a fat pointer to point to the data itself, and a virtual table
the
virtual table
defines the destructor, size, methods, etc
concurrency
means dealing with multiple tasks togetherparallelism
is one way to achieve concurrency
sync
means execute the tasks in order, one after the other,async
means we can change tasks, say we are running task a, which is waiting, then we can run task b, note that b should not have casuality wrt asmall rust tutorials, https://github.com/rust-lang/rustlings
starter
vscode rust plugin
rust analyzer
crates
better toml
rust test lens
tabnine
line number statistics
tokei
drawing tool
exaclidraw
function as parameter
fn apply(value: i32, f: fn(i32) -> i32) -> i32 {
f(value)
}
fn square(value: i32) -> i32 {
value * value
}
fn cube(value: i32) -> i32 {
value * value * value
}
fn main() {
println!("apply square: {}", apply(2, square));
println!("apply cube: {}", apply(2, cube));
}
we can use workspace to include multiple crates, otherwise it takes too long to build, in this way we only need to build the changed crates
through
trait
, we can doinversion of control
easilytraits can also help us realize
separation of concerns
, which reduces the complexity
ownership
if a variable type has copy trait, then rust will use it, otherwise rust will use move
mut ref, variable length data do not have copy trait
readonly ref and mut ref are mutually exclusive
this is wrong
fn main() {
let mut arr = vec![1, 2, 3];
let last = arr.last();
arr.push(4);
println!("last: {:?}", last);
}
need to use readonly ref first, then do mut ref
fn main() {
let mut arr = vec![1, 2, 3];
let last = arr.last();
println!("last: {:?}", last);
arr.push(4);
}
ref can be a pointer, eg
&Vec<T>
, or a fat pointer, eg&[u8]
Box is the smart pointer in rust, it can create any data on heap, and put a pointer on stack
for Box::leak(), the creation will not be controlled by stack, its life time can be static
Rc provides multiple ownership
can be used for DAG
use std::rc::Rc;
#[derive(Debug)]
struct Node {
id: usize,
downstream: Option<Rc<Node>>,
}
impl Node {
pub fn new(id: usize) -> Self {
Self {
id,
downstream: None,
}
}
pub fn update_downstream(&mut self, downstream: Rc<Node>) {
self.downstream = Some(downstream);
}
pub fn get_downstream(&self) -> Option<Rc<Node>> {
self.downstream.as_ref().map(|v| v.clone())
}
}
fn main() {
let mut node1 = Node::new(1);
let mut node2 = Node::new(2);
let mut node3 = Node::new(3);
let node4 = Node::new(4);
node3.update_downstream(Rc::new(node4));
node1.update_downstream(Rc::new(node3));
node2.update_downstream(node1.get_downstream().unwrap());
println!("node1: {:?}, node2: {:?}", node1, node2);
}
when we use let mut, or &mut, this is exterior mutability, RefCell uses interior mutability to change variables
exterior mutability is checked at compile time
interior mutability is checked at run time
use std::cell::RefCell;
fn main() {
let data = RefCell::new(1);
{
let mut v = data.borrow_mut();
*v += 1;
}
println!("data {:?}", data.borrow());
}
use Rc and RefCell, we can mut the DAG
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
id: usize,
downstream: Option<Rc<RefCell<Node>>>,
}
impl Node {
pub fn new(id: usize) -> Self {
Self {
id,
downstream: None,
}
}
pub fn update_downstream(&mut self, downstream: Rc<RefCell<Node>>) {
self.downstream = Some(downstream);
}
pub fn get_downstream(&self) -> Option<Rc<RefCell<Node>>> {
self.downstream.as_ref().map(|v| v.clone())
}
}
fn main() {
let mut node1 = Node::new(1);
let mut node2 = Node::new(2);
let mut node3 = Node::new(3);
let node4 = Node::new(4);
node3.update_downstream(Rc::new(RefCell::new(node4)));
node1.update_downstream(Rc::new(RefCell::new(node3)));
node2.update_downstream(node1.get_downstream().unwrap());
println!("node1: {:?}, node2: {:?}", node1, node2);
let node5 = Node::new(5);
let node3 = node1.get_downstream().unwrap();
node3.borrow_mut().downstream = Some(Rc::new(RefCell::new(node5)));
println!("node1: {:?}, node2: {:?}", node1, node2);
}
Rc is efficient but does not support concurrency, use Arc, Mutex, RwLock instead
change
Rc<RefCell<T>>
toArc<Mutex<T>>
lifetime
note return value of strtok has same lifetime with input &str, not the same with &mut &str, which is just a mut ref to &str
<'a>
means the lifetime para is a template param
pub fn strtok<'a>(s: &mut &'a str, delimiter: char) -> &'a str {
if let Some(i) = s.find(delimiter) {
let prefix = &s[..i];
let suffix = &s[(i + delimiter.len_utf8())..];
*s = suffix;
prefix
} else {
let prefix = *s;
*s = "";
prefix
}
}
fn main() {
let s = "hello world".to_owned();
let mut s1 = s.as_str();
let hello = strtok(&mut s1, ' ');
println!("input is {}, prefix: {}, suffix: {}", s, hello, s1);
}
variable length structure is stored by a fat pointer on the stack, that has the address of that variable on the heap, and its capacity and length
for the memory layout, refer to
cheat.rs
when using
Vec<T>
, can useshrink_to_fit
to reduce the capacity allocated to itownership model ensures a variable only has one owner, so when releasing the heap memory, only need to use the drop trait, this is specific to rust
in python, we need to close the files explicitly, but in rust, when we no longer use the file, no need to close it, since this file will not be used anywhere else, so all its resources are released automatically
python
with
can partially solve this issue
types
type safe: code is only allowed to access the memory based on permitted ways
C++ can implicity convert the type, so it is not type safe
HashSet
is an alias ofHashMap<K, ()>
sometimes rust cannot infer the type, we need to label it
original, has error
fn main() {
let numers = vec![1, 2, 3];
let even_num = numbers
.into_iter()
.filer(|n| n % 2 == 0)
.collect();
println!("{:?}", even_num);
}
we can label even_num
fn main() {
let numers = vec![1, 2, 3];
let even_num: Vec<_> = numbers
.into_iter()
.filer(|n| n % 2 == 0)
.collect();
println!("{:?}", even_num);
}
or we can let collect to return a type
fn main() {
let numers = vec![1, 2, 3];
let even_num: Vec<_> = numbers
.into_iter()
.filer(|n| n % 2 == 0)
.collect::<Vec<_>>();
println!("{:?}", even_num);
}
::<T>
is called turbo fishwe always need to specify type for const and static variables, because they are global and can be used in different contexts
clone on write (Cow) is an enum in rust, where it either returns a readonly ref to the data, or a owned data that is mutable
B’s lifetime is
'a
, so Cow’s generic parameter is'a
?Sized
means the condition can be relaxed,Sized
means length is fixed, so?Sized
means variable lengthToOwned
means clone a ref-ed data, that the clone can be modifiedB as ToOwned
is a type conversion, and then::Owned
means to access the owned type
pub enum Cow<'a, B: ?Sized + 'a> where B: ToOwned,
{
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned),
}
trait
Self
means the current type, eg File implements Write, then in Write, Self means Filewhen
self
is used as the first parameter, it meansself: Self
, so&self
meansself: &Self
in a trait, if it does not use self as parameter, then need to call it like
T::parse(str)
example
use std::ops::Add;
#[derive(Debug)]
struct Complex {
real: f64,
imagine: f64,
}
impl Complex {
pub fn new(real: f64, imagine: f64) -> Self {
Self { real, imagine }
}
}
impl Add for Complex {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
let real = self.real + rhs.real;
let imagine = self.imagine + rhs.imagine;
Self::new(real, imagine)
}
}
fn main() {
let c1 = Complex::new(1.0, 1f64);
let c2 = Complex::new(2 as f64, 3.0);
println!("{:?}", c1 + c2);
}
we can also implement for
&T
use std::ops::Add;
#[derive(Debug)]
struct Complex {
real: f64,
imagine: f64,
}
impl Complex {
pub fn new(real: f64, imagine: f64) -> Self {
Self { real, imagine }
}
}
impl Add for Complex {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
let real = self.real + rhs.real;
let imagine = self.imagine + rhs.imagine;
Self::new(real, imagine)
}
}
impl Add for &Complex {
type Output = Complex;
fn add(self, rhs: Self) -> Self::Output {
let real = self.real + rhs.real;
let imagine = self.imagine + rhs.imagine;
Complex::new(real, imagine)
}
}
fn main() {
let c1 = Complex::new(1.0, 1f64);
let c2 = Complex::new(2 as f64, 3.0);
println!("{:?}", &c1 + &c2);
println!("{:?}", c1 + c2);
}
sub trait can be used in parent trait
impl Animal
is a shorthand ofT: Animal
, sofn name(animal: impl Animal) -> &'static str
is equivalent tofn name<T: Animal>(animal: T) -> &'static str
struct Cat;
struct Dog;
trait Animal {
fn name(&self) -> &'static str;
}
impl Animal for Cat {
fn name(&self) -> &'static str {
"Cat"
}
}
impl Animal for Dog {
fn name(&self) -> &'static str {
"Dog"
}
}
fn name(animal: impl Animal) -> &'static str {
animal.name()
}
fn main() {
let cat = Cat;
println!("cat: {}", name(cat));
}
to tell the compiler we need a type that implements
Formatter
trait, we can use Trait Object, ie&dyn Trait
orBox<dyn Trait>
dyn is used to tell the difference between normal type and trait type
eg
formatters: Vec<&dyn Formatter>
, which is dynamic dispatchingTrait Object is a fat pointer, one pointer points to the data, another pointer points to vtable
orphan rule means for the trait and the type that implements the trait, at least one of them is defined in current trait
copy trait is defined as
pub trait Copy: Clone {}
this is a marker trait for trait bound check
if a type has copy trait, then the value will be copied, otherwise the ownership will be moved
&mut T does not have copy, since it will create multiple mutable ref
copy trait is shallow copy, it assumes there is no resource to be dropped, so a type that has copy trait cannot also has drop trait, so no
use after free
will happen
sized trait is used to mark variables with fixed length, use for generic types
when the length is fixed, we can pass it to functions
struct Data<T> {
inner: T,
}
fn process_data<T>(data: Data<T>) {
todo!();
}
is equivalent to
struct Data<T: Sized> {
inner: T,
}
fn process_data<T: Sized>(data: Data<T>) {
todo!();
}
in some case we need T to has changing length, we can use
?Sized
trait, so it can be[T]
orstr
, whose length are not fixedsend/sync are unsafe auto trait, auto means the compiler will automatically add them
send means ownership can transfer from one thread to another
sync means &T can be shared in threads
'static
means we own the variable, or the variable has static lifetimederef example
let mut x = 42;
let y = &mut x;
*y += 1;
deref for Rc
impl<T: ?Sized> Deref for Rc<T> {
type Target = T;
fn deref(&self) -> &T {
&self.inner().value
}
}
let a = Rc::new(1);
let b = a.clone();
println!("v = {}", *b);
*b
above equals to*&b.inner().value
,*b
is automatically expanded to*(b.deref())
when the first argument is
&mut self
, the compiler will automatically deref,example for default/display/debug
use std::fmt;
#[derive(Clone, Debug, Default)]
struct Developer {
name: String,
age: u8,
lang: Language,
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
enum Language {
Rust,
TypeScript,
Elixir,
Haskell,
}
impl Default for Language {
fn default() -> Self {
Language::Rust
}
}
impl Developer {
pub fn new(name: &str) -> Self {
Self {
name: name.to_owned(),
..Default::default()
}
}
}
impl fmt::Display for Developer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}({} years old): {:?} developer",
self.name,
self.age,
self.lang
)
}
}
fn main() {
let dev1 = Developer::default();
let dev2: Developer = Default::default();
let dev3 = Developer::new("Tim");
println!("dev1: {}\\ndev2: {}\\dev3: {:?}", dev1, dev2, dev3);
}
Vec<T>
cannot have copy trait, ref https://users.rust-lang.org/t/question-copy-vec-t/37524/2the vector owns the heap memory, so if the shallow copy of vector adds to heap and exceeds its capacity, then the heap will reallocate, so cannot copy
+------------+
| The vector |
+------------+
|
V
+--------+--------+--------+-------------------------+
| Item 1 | Item 2 | Item 3 | Additional capacity... |
+--------+--------+--------+-------------------------+
smart pointer
smart pointer is a fat pointer
eg String vs &str, String has a capacity field, &str does not, String has ownership for the heap, &str does not
as long as there are resources to be recyled, and implements deref, derefmut, drop, those are smart pointers
unique_ptr in C++ is similar to
Box<T>
Box::new()
is a function, so its parameter exist in the stack first, then move to heapString can be borrowed to
&String
or&str
, since it has multipleBorrow<T>
, so we need to declare the type explicity
fn main() {
let s = "hello world!".to_owned();
let r1: &String = s.borrow();
let r2: &str = s.borrow();
println!("r1: {:p}, r2: {:P}", r1, r2);
}
Owned(<B as ToOwned>::Owned)
meansB as ToOwned, transfer B to
ToOwned
typeToOwned has associated type
Owned
, so we can use ToOwned::Owned
design a smart pointer
in order to increase efficiency, we can store the string on stack when it is short, and only store it on heap when it is long
MyString below feels the same with &str
a third party
smartstring
implements this
use std::{fmt, ops::Deref, str};
const MINI_STRING_MAX_LEN: usize = 30;
struct MiniString {
len: u8,
data: [u8; MINI_STRING_MAX_LEN],
}
impl MiniString {
fn new (v: impl AsRef<str>) -> Self {
let bytes = v.as_ref().as_bytes();
let len = bytes.len();
let mut data = [0u8; MINI_STRING_MAX_LEN];
data[..len].copy_from_slice(bytes);
Self {
len: len as u8,
data,
}
}
}
impl Deref for MiniString {
type Target = str;
fn deref(&self) -> &Self::Target {
str::from_utf8(&self.data[..self.len as usize]).unwrap()
}
}
impl fmt::Debug for MiniString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.deref())
}
}
#[derive(Debug)]
enum MyString {
Inline(MiniString),
Standard(String),
}
impl Deref for MyString {
type Target = str;
fn deref(&self) -> &Self::Target {
match *self {
MyString::Inline(ref v) => v.deref(),
MyString::Standard(ref v) => v.deref(),
}
}
}
impl From<&str> for MyString {
fn from(s: &str) -> Self {
match s.len() > MINI_STRING_MAX_LEN {
true => Self::Standard(s.to_owned()),
_ => Self::Inline(MiniString::new(s)),
}
}
}
impl fmt::Display for MyString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.deref())
}
}
fn main() {
let len1 = std::mem::size_of::<MyString>();
let len2 = std::mem::size_of::<MiniString>();
println!("Len: MyString {}, MiniString {}", len1, len2);
let s1: MyString = "hello world".into();
let s2: MyString = "this is a super long string that has more than 30 bytes".into();
println!("s1: {:?}, s2: {:?}", s1, s2);
println!(
"s1: {}({} bytes, {} chars), s2: {}({} bytes, {} chars)",
s1,
s1.len(),
s1.chars().count(),
s2,
s2.len(),
s2.chars().count()
);
assert!(s1.ends_with("world"));
assert!(s2.starts_with("this"));
}
MyString only supports new from
&str
, to supportString
impl<T> From<T> for MyString
where
T: AsRef<str> + Into<String>,
{
fn from(s: T) -> Self {
match s.as_ref().len() > MINI_STRING_MAX_LEN {
true => Self::Standard(s.into()),
_ => Self::Inline(MiniString::new(s)),
}
}
}
container
summary from geekbang
vec to boxed vec
use std::ops::Deref;
fn main() {
let mut v1 = vec![1, 2, 3, 4];
v1.push(5);
println!("cap should be 8: {}", v1.capacity());
let b1 = v1.into_boxed_slice();
let mut b2 = b1.clone();
let v2 = b1.into_vec();
println!("cap should be exactly 5: {}", v2.capacity());
assert!(b2.deref() == v2);
b2[0] = 2;
println!("b2: {:?}", b2);
let b3 = Box::new([2, 2, 3, 4, 5]);
println!("b3: {:?}", b3);
assert!(b2 == b3);
}
hash map
definition
K, V means the types for key/value
S is the state of hash function, which is
RandomState
hashmap in rust uses hashmap from hashbrown
use hashbrown::hash_map as base;
#[drive(Clone)]
pub struct RandomState {
k0: u64,
k1: u64,
}
pub struct HashMap<K, V, S = RandomState> {
base: base::HashMap<K, V, S>,
}
usage
new hash map does not have memory allocation
hash map size will grow at 2^n - 1, min is 3
only when shrink_to_fit is called, will the hash map size shrink
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
explain("empty", &map);
map.insert('a', 1);
explain("added 1", &map);
map.insert('b', 2);
map.insert('c', 3);
explain("added 3", &map);
map.insert('d', 4);
explain("added 4", &map);
assert_eq!(map.get(&'a'), Some(&1));
assert_eq!(map.get_key_value(&'b'), Some((&'b', &2)));
map.remove(&'a');
assert_eq!(map.contains_key(&'a'), false);
explain("removed", &map);
map.shrink_to_fit();
explain("shrinked", &map);
}
fn explain<K, V>(name: &str, map: &HashMap<K, V>) {
println!("{}: len: {}, cap: {}", name, map.len(), map.capacity());
}
use std::collections::HashMap;
fn main() {
let map = HashMap::new();
let mut map = explain("empty", map);
map.insert('a', 1);
let mut map = explain("added 1", map);
map.insert('b', 2);
map.insert('c', 3);
let mut map = explain("added 3", map);
map.insert('d', 4);
let mut map = explain("added 4", map);
assert_eq!(map.get(&'a'), Some(&1));
assert_eq!(map.get_key_value(&'b'), Some((&'b', &2)));
map.remove(&'a');
assert_eq!(map.contains_key(&'a'), false);
let mut map = explain("removed", map);
map.shrink_to_fit();
explain("shrinked", map);
}
fn explain<K, V>(name: &str, map: HashMap<K, V>) -> HashMap<K, V> {
let arr: [usize; 6] = unsafe {
std::mem::transmute(map)
};
println!("{}: bucket mask 0x{:x}, ctrl 0x{:x}, growth left: {}, items: {}",
name, arr[2], arr[3], arr[4], arr[5]
);
unsafe { std::mem::transmute(arr) }
}
customized hash table
use hash to compute hash values
use partial eq/eq to check wehther two structures are equal
use std::{
collections::{hash_map::DefaultHasher, HashMap},
hash::{Hash, Hasher},
};
#[derive(Debug, Hash, PartialEq, Eq)]
struct Student<'a> {
name: &'a str,
age: u8,
}
impl<'a> Student<'a> {
pub fn new(name: &'a str, age: u8) -> Self {
Self { name, age }
}
}
fn main() {
let mut hasher = DefaultHasher::new();
let student = Student::new("Sean", 30);
student.hash(&mut hasher);
let mut map = HashMap::new();
map.insert(student, vec!["math", "writing"]);
println!("hash: 0x{:x}, map: {:?}", hasher.finish(), map);
}
hashset
defintion is
HashMap<K, ()>
unordered
use hashbrown::hash_set as base;
pub struct HashSet<T, S = RandomState> {
base: base::HashSet<T, S>,
}
pub struct HashSet<T, S = DefaultHashBuilder, A: Allocator + Clone = Global> {
pub(crate) map: HashMap<T, (), S, A>,
}
BTreeMap
BTreeMap is ordered
when print, BTreeMap will print elements in order
for customization, need to use PartialOrd and Ord
use std::collections::BTreeMap;
fn main() {
let map = BTreeMap::new();
let mut map = explain("empty", map);
for i in 0..16usize {
map.insert(format!("Sean {}", i), i);
}
let mut map = explain("added", map);
map.remove("Sean 1");
let map = explain("remove 1", map);
for item in map.iter() {
println!("{:?}", item);
}
}
fn explain<K, V>(name: &str, map: BTreeMap<K, V>) -> BTreeMap<K, V> {
let arr: [usize; 3] = unsafe { std::mem::transmute(map) };
println!("{}: height {}, root node 0x{:x}, len 0x{:x}",
name, arr[0], arr[1], arr[2]);
unsafe { std::mem::transmute(arr) }
}
in code below, output will always be the same, no matter the order of insertion
use std::collections::BTreeMap;
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
struct Name {
pub name: String,
pub flags: u32,
}
impl Name {
pub fn new(name: impl AsRef<str>, flags: u32) -> Self {
Self {
name: name.as_ref().to_string(),
flags,
}
}
}
fn main() {
let mut map = BTreeMap::new();
map.insert(Name::new("harrypotter", 0x1), 12);
map.insert(Name::new("ron", 0x0), 20);
for item in map.iter() {
println!("{:?}", item);
}
}
output
the hash is based on
name
field, egharrypotter
will always be in front ofRon
, butdobby
will be in front ofharrypotter
(Name { name: "harrypotter", flags: 1 }, 12)
(Name { name: "ron", flags: 0 }, 20)
error handling
we can handle errors by returning error values
eg in C, if fopen(filename) fails, it return NULL
exception is
separation of concerns
, exception can be propagated using stack unwindneed to have exception safety
Option is an enum
pub enum Option<T> {
None,
Some(T),
}
Result is also an enum
if must_use is not explicitly used, the compiler will warn
#[must_use = }this `Result` may be an `Err` variant, which should be handled"]
pub enum Result<T, E> {
Ok(T),
Err(E),
}
we can use ? to avoid handling the error immediately
use std::fs::File;
use std::io::Read;
fn read_file(name: &str) -> Result<String, std::io::Error> {
let mut f = File::open(name)?;
let mut contents = String::new();
f.read_to_string(&mut contents)?;
Ok(contents)
}
? is equivalent to
match result {
Ok(v) => v,
Err(e) => return Err(e.into())
}
you can use
railroad oriented programming
Railway oriented programming is a functional approach to the execution of functions sequentially
you can also use exception in rust for unrecoverable errors, and use catch_unwind to handle it
use std::panic;
fn main() {
let result = panic::catch_unwind(|| {
println!("hello!");
});
assert!(result.is_ok());
let result = panic::catch_unwind(||{
panic!("no!!!!");
});
assert!(result.is_err());
println!("panic captured: {:#?}", result);
}
closure
input for spawn is a cloure f
F: FnOnce() -> T
means F accepts 0 parameters, return type TF: Send + 'static
means F has static lifetime or has ownership, it also needs to be sent to another threaddue to the ownership requirement, we need to use
move
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
use std::thread;
fn main() {
let s = String::from("hello world");
let handle = thread::spawn(move || {
println!("moved {:?}", s);
});
handle.join().unwrap();
}
FnOnce has an associated type Output, which is the returned type
call_once has a parameter self, which means the onwership of self is moved to call_once
pub trait FnOnce<Args> {
type Output;
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}
FnMut can be called multiple times, and it can change the capture variable
since FnOnce is the super trait for FnMut, so it can be called when FnOnce is required, equivalent to
call_once()
FnMut is the super trait for Fn
closure in rust is very efficient
the captured variables are sotored in stack, not in heap
each closure has its own type, no need to use function pointer
faq
can use
excalidraw
to draw the diagram to better understand the codethe code below is correct, since the lifetime of returned chars will be the same with name, as name is a borrowed reference
or is it because chars is copied, so has ownership?
fn lifetime3(name: &str) -> Chars {
name.chars()
}
String / Vec cannot be borrowed and moved at the same time
code below is incorrect
fn main() {
let s = "hello".to_string();
let r1 = &s;
let s1 = *r1;
println!("{:?}", s1);
}