-
Notifications
You must be signed in to change notification settings - Fork 0
Ownership and Borrowing Rules
Thanks to the Rust for not supporting garbage collection rules and concetps however it handles memory safety issues in completely a different way by injecting a nice pain in all devs' ass so they can experience a new WTF coding tools!
Every type has an ownership, a value and a lifetime which its lifetime comes to end when when it goes out of its scope then suddenly lights go off and gets dropped out of the ram, this happens when we move the value into or from functions or send it into other scopes using a channel sender (probably a nice and sexy thread), Rust does this automatically for heap data only to free the allocated space by them and make the ram neat and clean by removing extra spaces.
By moving the type the owner's lifetime will be dropped means we don't have access it after moving but the value gets a new ownership thus a new location inside the ram when it's inside the new scope finally Rust updates all the pointers of the very first owner after moving, if there are any of course, to avoid getting invalidated pointers at runtime however we can't use any of those pointer after the value gets moved cause the value has already a new owner! that's why all Rustaceans are wily cause they don't move heap data types when they're behind pointers they try to pass them by reference or clone them if they want to remove the pain from their ass! note that the reference you want to pass it must lives long enough and have a valid lifetime so it can be moved into the newly scope, after all lifetime belongs to baby pointers.
There are some scenarios that the new changes to pointers' value won't be reflected which need to understand the followings:
- since Rust don't have GC it uses the concepts of borrowing and ownership rules in such a way that passing types in their stack form or borrowed form require a valid lifetime to be used cause it tells Rust how long the pointer would be valid during the execution of the app so if the compiler comes across dropping the underlying type out of the ram Rust knows how to update the pointer to point to the right location of the type or even destroy it to avoid having dangling and invalidated pointers at runtime, there is
drop()
method which will be called on heap data right before passing them into a new scope like function and once they go in there they get owned by the function body as well as new ownership therefore new address inside the ram. - in Go we are allowed to return a pointer from a method, thanks to the GC of course, which validate the lifetime of a type by its references which has been taken during the app execution, once the counter reaches the zero it drops the type from the heap, in Rust however we can't return pointer from method unless we explicitly specify the lifetime of the pointer (
'v
,&self
or'static
), be careful about choosing lifetime cause if you want to move a pointer into a new scope with a lifetime shorter than the scope and use it after moving you'll feel a nice pain in your tiny ass coming constantly from the compiler of you're not allowed to do this error cause the pointer doesn't live long enough because some goofy scopes are using it or its scope will come to end as soos as some tough functions gets exeucted and you're passing it into a longer scope! reason for this is because Rust ownership and borrowing rules enforces the compiler to drop a type and accordingly its lifetime once its scope gets ended and in our case as soon as the function gets executed all the types owned by the function will be dropped out of the ram while their scope is getting finished by poping them out of the function stack, results in ending their lifetime too, to address this issue and simulate what Go does we can usePin
smart pointer in Rust to pin a value into the ram to tell Rust this value got pinned you can't move it around and change its address and its location so any pointer to this value is valid which allows us to return it from a method! since the location of a type has stuck into the ram thus any pointer to that address won't get changed, by default Rust moves heap data around the ram and give them new location by transferring their ownership into a new one like once we pass aString
into a function without borrowing or cloning it, although Rust updates any pointer of the moved type after moving to make them point to the right and new location of the value to avoid dangling issues in the future but can't use them since the type is moved. - in Rust pointers are divided into immutable and mutable ones imutbale pointers can only be used to borrow the type instead of moving them mutbale pointers however besides having the feature of immutable pointers they can be used to mutate the state of their underlying data.
- mutable pointer can be used to pass to multiple function in a same thread which their execution are not async to mutate the underlying data but in order to change it in multiple threads we should wrap the type around Mutex.
- pass by reference by borrowing the type using
&
or smart wrappers or if you wann pass by value the value's ownership will be transferred into a new one inside the new scope to prevent this clone the heap data types to avoid from moving. -
Rc
,Arc
,RefCell
,Mutex
andBox
are smart wrappers and pointers around the type which store the type on the heap with their own valid lifetime so the new type has all the functionalities of the wrapped type. - share the ownership of the type among other scopes and threads, break the cycle in
self-referential
types (moreover later!) like graphs using pointers likeRc
,Arc
,Box
and&mut _
, get the owned version of the type by cloning or dereferencing it using*
, in essence a reference counter gets created when usingRc
andArc
to count the taken references of a type besides sharing its ownership, once theRc
-ed or andArc
-ed type referecnes reached zero it can gets dropped from the ram. - storing trait as object must be like
Box<dyn Trait>
, it allows us to call the trait method on any instance that itsstruct
type impelemts the Trait, this is called dynamic dispatching and as you can see we usedyn
keyword to store our dynamic sizedTrait
on the heap, this tells Rust that this is going to be a dynamic call since we don't know the exact type of the implementor, the only thing we know is that we're sure it implements theTrait
take note that in order to get this work the trait object must be an object safe trait. - just trust me when I say don't move the type if it's behind a pointer or its ownership is being used by other scopes, you won't have access the pointer after moving Rust can't let you use it to avoid having invalidated or dangled pointer although it has updated the the content of the pointer to point to the new ownership location of the moved value, if you don't want this to happen just pass data by reference or its clone to method call and other scopes.
- returning pointer from function is only allowed if we tell rust that the storage of local variable will be valid after your execution such that if the pointer is pointing to the self the self is valid as long as the instance is valid and if an static pointer the type is valid for the entire execution of app other than these both scenarios returning a pointer requires a valid lifetime to make the pointer live long enough even after function execution but the point is that the type must be an in place allocation such that it allocates nothing inside the function body it only does this on the caller space.
- normally we can't return reference from a method unless we do so by adding a valid lifetime for it like
-> &'static str
,-> &'valid str
or-> &self
which in the last case it uses the lifetime of the&self
itself or the instance cause its lifetime is valid as long as the object is alive, the reason Rust doesn't allow to do so is because the pointer gets invalidated as soon as the method gets executed which causes all the types inside of it get dropped and moved out of the ram and the one we're returning it gets new ownership where the method is being called (the caller itself is its new ownership). - make sure the pointer we're trying to return it from the method lives long enough after the function gets executed, we should make sure that the lifetime of actual type will be valid, one of the solution is to tell Rust hey the actual type is immovable let us return that goddam pointer, we've pinned the type into the ram!
- note that we can't move pointer which doesn't live long enough into
tokio:::spawn()
unless it has a valid lifetime like if it's static is ok to move otherwise by moving it, pointer gets destroyed and we don't have access it aftertokio::spawn()
scope, if we need a type aftertokio::spawn()
scope we shouldclone()
it and pass the cloned version. - unknown sized types or those ones have been bounded to
?Sized
are the ones that must be behind pointer or boxed into the heap like trait objects like closures and slices ([]
) andstr
. - trait objects are dynamically sized and they are stored on the heap, due to the fact that
?Sized
types must be behind a pointer, it's better to keep them on the heap by boxing using Box which is an smart pointer and has a valid lifetime, so putting a future object on the heap is like:Box<dyn Future<Output=String>>
or&'validlifetime dyn Future<Output=String>
. - generally traits are not sized cause their size depends on the impelemnter type at runtime therefore to move them as an object between scopes it's ideal to keep them inside the Box like
Box<dyn Error>
that if it's used to be the error part of the Result in a return type it means that the actuall type that causes the error will be specified at runtime and we don't know what is it right now. - a tree or graph can be built in Rust using
Rc
,Arc
,RefCell
orMutex
in whichArc
the safe reference counting andMutex
are used to build a multithreaded based graph contrastinglyRc
the none safe reference counting andRefCell
are used to build a single threaded graph. - traits can be used as object: returning them from a method like:
... -> impl Trait
, as a method param like:param: impl Trait
, bounding generic to traits likeG: Send + Sync + Trait
, having them as a Box type in struct fields. - in Rust lifetime belongs to pointers which indicates how long the pointer must live or how long a value is borrowed for.
-
'static
lifetime lives long enough for the entire of the app therefore the type won't get dropped out of ram by moving it around scopes. - cases that Rust moves type around the ram are:
- 0 - heap data types move by default to avoid allocating extra spaces in the ram
- 1 - returning a value from a method: by returning the value from method the owner gets dropped out of the ram and is no longer accessible, the value however goes into a new location and gets a new ownership where the method return type is being stored
- 2 - Passing a value to a function: When a value is passed to a function, it may be moved to a different memory address if the function takes ownership of the value.
- 3 - Boxing a value and putting it on the heap: When a value is boxed using Box::new, it is moved to the heap, and the pointer to the boxed value is stored on the stack.
- 4 - Growing a Vec beyond its capacity: When a Vec outgrows its capacity and needs to reallocate memory, the elements may be moved to a new, larger buffer in memory.
- 5 - In each of these cases, the Rust compiler ensures that the ownership and borrowing rules are followed, and it updates references and pointers to the moved values to maintain memory safety.
- Rust allows us to have multiple immutable pointers and one mutable pointer in a scope but not both of them at the same time (you see the
mpsc
rule in here!) - a mutable pointer can access the underlying data of the type so by mutating the pointer the value inside the type will be mutated too nicely.
- Rust's ownership and borrowing rules are designed to ensure memory safety and prevent data races. When a value is moved, the ownership of that value is transferred, and the original owner can no longer access it. However, the mutable reference (pointer) to the value remains valid, and Rust allows the pointer to be updated to point to the right location after the value has been moved.
- almost every type in Rust is safe to be moved by the Rust compiler cause they are implementing
Unpin
which means they shouldn't be forced to be pinned into the ram, types like future objects are not safe to be moved cause they're kinda a self-refrential type that need to be pinned into the ram to not allow them to be moved at all cause they don't implementUnpin
or they implement!Unpin
. it's better not to move the heap data types which doesn't implementCopy
trait if they're behind pointer cause the pointer gets invalidated after moving which will be updated with owner location by the Rust compiler. - Rust by default moves heap data types around the ram like if we want to pass them into a method or return them from a method in such a way that by moving the very first owner of the value gets dropped out of the ram and the value goes into a new location inside the newly scope and gets owned by a new owner then Rust updates all of the very first owner's pointers to point to the right location of the new owner after moving however Rust doesn't allow us to use pointers after it gets moved to avoid having dangling pointers.
- struct methods that have
self
in their first param takes the ownership of the object when we call them on the object likeunrawp()
method, we should return the whole instance again as the return type of these methods so we can have a new instance again or use&self
which call the method on the borrow form of the instance. -
self-refrential, raw pointers and future objects are those types that are not safe to be moved around the ram by Rust compiler cause once they get moved any pointers of them won't get updated bringing us undefined behaviour after like swapping hence pointers get invalidated eventually the move process get broken, some how we must tell Rust that don't move them at all and kinda pin them into the ram to avoid this from happening, the solution to this would be pin the value of them into the heap so their pointers won't get invalidated even their value get swapped or moved, by pinning their pointer using
Box::pin
into the heap we can achive this, cause after swapping due to the fact that pinning pins the value to the ram to get fixed at its memory location and accordingly its address thus the whole pinned type get swapped and correspondingly its pointer which contains the right address of it. - Rust drops heap data values to clean ram and therefore updates all pointers to each value if there is any, however can't use the pointer after moving, what it means if the ownership of a type is being shared by other scopes it's better not to move the type and pass it by reference to other scopes cause by moving all pointers will be invalidated but based on borrowing and ownership Rust updates these pointers to point to the right location of the new ownership of the moved value to avoid having invalidated and dangled pointers.
- thanks to
Pin
, hopefully we can move future objects around other scopes and threads but first we must pin them into the heap so they can get solved later although.await
do this behind the scene but if we want toawait
on for example a mutable pointer of the future definitely we should pin it first. - note that
async move{}
is a future obejct and future objects are not safe to be moved since they're!Unpin
so we pin them into the ram (on the heap) usingBox::pin(async move{});
which enables us to move them around without updating their location so we can.await
on them or their mutable pointer later to poll them. - for some reason Rust can't update the pointers of self-refrential types, raw pointers and future objects as soon as their value get moved thus the pointers get invalidated, pinnning is about sticking the value into the ram to make the type safe to be moved, (safe to be moved means once the type contains the value gets moved all pointers to that type won't get invalidated cause the value is still exists and pinned to the ram), by doing this we tell Rust that any reference to the type must be valid after dropping the type cause we've already pinned the value into the ram so there is no worries cause all pointers point to the very first location of the value hence our move will keep the reference and don't get a new locaiton in the ram as it stays in its very first location, the sub-recaps:
- Pinning and Moving: Pinning in Rust is about ensuring that a value remains at a fixed memory location, even when it's moved. This is important for types that contain self-references or have to remain at a specific memory location for other reasons. When a value is pinned, it means that the value will not be moved to a different location in memory, even if the type is moved. This ensures that any references to the value remain valid, even after the type is moved, cause when Rust moves value around the ram pointers to that value can't ramain valid and get invalidated and dangled but thanks to the ownership rule in Rust those pointers get updated to point to the new ownership of the value after the value get moved.
- Self-Referential Types: Self-referential types are types where a part of the data within the type refers to another part of the same data. This can lead to issues with memory safety and ownership in Rust, especially when the type is moved or dropped. Pinning is often used to handle self-referential types, ensuring that the references within the type remain valid even after the type is moved.
- pinning is the process of make the value of a type stick into the ram at an stable memory address (choosen by the compiler) so it can't be able to be moved to get a new owner and location at all, it doesn't mean that the owner can't be moved it means that the value and any reference to it get pinned to the ram which enables us to move the pinned value (the pinned pointer) around other scopes which guarantees that the value won't be moved into a new location when we're using the pinned pointer (cause Rust understood that he must not transfer the ownership to a new one after once he moved the value hence he won't drop it so references remain valid), in the context of swapping two instances of an struct which has either a self-refrential field or a raw pointer field we must swap the pinned instances cause Rust won't update the raw pointers to reflect new location of the moved types, this is because raw pointers are
C
like pointers and are not safe to be used cause swapping two types swap their contents but not their pointer value and may get undefined behaviours after swapping like the contents are ok but the pointer of the first instance is still pointing to the previous content even after swapping, we can fix this by pinning the two instances into the ram and make them immovable so if would like to move or swap them it ensures that the poniters values are getting swapped too completely cause moving in Rust consists three parts, ensures that the: value gets a new ownership, very first owner is getting dropped out of ram, changes to pointers' content of the versy first owner are being reflected with new ownership so the pointer points to the right location of the new ownership hence accessing the moved value inside the updated pointer with newly address.
we can either pin the actual type or its references (immutable or mutable) into the ram, pinning the actual type moves its ownership into the pin pointer, after pinning we can use it instead of the actual type and move the pinned pointer around different scopes cause we're sure it has a fixed memory address every time we move it around.
let's imagine val
is a heap data type cause stack data implement Copy
trait, Rust treat them nicely!
let val = 1; // location of val itself is at 0xA1
let p = &val; // location of p itself is at 0xA2 which points to the 0xA1
______________________
| |
_↓___________ _______|______
| val = 1 | | p = 0xA1 |
|-------------| |--------------|
| 0xA1 | | 0xA2 |
------------- --------------
once we move the val
into a new scope, Rust takes care of the pointer and update the pointer (p
) of val
so it can points to the right location of val
value, as we know this is not true about types that implements !Unpin
like self-referential types, future objects and raw pointers.
____________________________________________________
| |
_↓_____________________________ __________________|______
| | | val = 1 | p = 0xA1 |
|-------------------------------| |-------------------------|
| 0xA1 | 0xA2 | | 0xB1 | 0xB2 |
------------------------------- -------------------------
as we can see val
is now at location 0xB1
after moving, got a new ownership but the pointer is still pointing to the very first owner location which was 0xA1
, Rust will update this to 0xB1
every time a heap data type wants to be moved so the pointer don't get dangled, about !Unpin
data which are not safe to be mvoed we should pin them into the ram (usually on the heap) in the first place so the pointer don't get invalidated if the type wants to be moved or swapped at any time, like in the swapping process both pointer and the value get swapped or in self-refrential types by pinning each instance of the type itself (a sexy struct for example) we tell Rust hey we've just pinned the value of this type into the ram and made it immovable so don't get panicked if you see any pointer of this type we know you can't update its pointer to point to the right location but we're telling you it's ok this won't get moved and neither will the pointer cause we're using the pinned owner value not the actual owner one, then Rust says ok then but the rules about moving is still exists for the pinned types too I'll drop their very first owner as soon as you try to move them let me show you:
let name = String::from("");
let pinned_name = Box::pin(name);
fn get_pinned(pinned_name: std::pin::Pin<Box<String>>){}
get_pinned(pinned_name);
println!("{:#?}", pinned_name);
see you can't use the pinned_name
after passing it into the get_pinned
method.
shit enough talking! I'm getting crazy.
// showing that pinned pointer of a type has a fixed memory address
// when we try to move it around different scopes
// **************************************************************
// ************************* SCENARIO 1 *************************
// **************************************************************
let mut name = String::from("");
let mut pinned_name = Box::pin(&mut name); // box and pin are pointers so we can print their address
**pinned_name = String::from("wildonion");
// passing name to this method, moves it from the ram
// transfer its ownership into this function scope with
// new address, any pointers (&, Box, Pin) will be dangled
// after moving and can't be used
// SOLUTION : pass name by ref so we can use any pointers of the name after moving
// CONCLUSION: don't move a type if it's behind a pointer
fn try_to_move(name: String){
println!("name has new ownership and address: {:p}", &name);
}
try_to_move(name);
// then what's the point of pinning it into the ram if we can't
// access the pinned pointer in here after moving the name?
// we can't access the pinned pointer in here cause name has moved
// println!("address of pinned_name {:p}", pinned_name);
// **************************************************************
// ************************* SCENARIO 2 *************************
// **************************************************************
// i've used immutable version of the name to print the
// addresses cause Rust doesn't allow to have immutable
// and mutable pointer at the same time.
let mut name = String::from("");
println!("name address itself: {:p}", &name);
let mut pinned_name = Box::pin(&name); // box and pin are pointers so we can print their address
println!("[MAIN] pinned type has fixed at this location: {:p}", pinned_name);
// box is an smart pointer handles dynamic allocation and lifetime on the heap
// passing the pinned pointer of the name into the function so it contains the
// pinned address that the name has stuck into same as outside of the function
// scope, the stable address of name value is inside of the pinned_name type
// that's why is the same before and after function, acting as a valid pointer
fn move_me(name: std::pin::Pin<Box<&String>>){
println!("name content: {:?}", name);
println!("[FUNCTION] pinned type has fixed at this location: {:p}", name);
println!("pinned pointer address itself: {:p}", &name);
}
// when we pass a heap data into function Rust calls drop() on the type
// which drop the type out of the ram and moves its ownership into a new one
// inside the function scopes, the ownership however blogns to the function
// scope hence returning pointer to the type owned by the function is impossible.
move_me(pinned_name); // pinned_name is moved
println!("accessing name in here {:?}", name);
// **************************************************************
// ************************* SCENARIO 3 *************************
// **************************************************************
// the address is change because a pinned pointer contains
// an stable address in the ram for the pinned value, second
// it’s the stable address of the new data cause pin pointer
// contains the stable address of the pinned value
let name = String::from("");
let mut pinned_name = Box::pin(&name);
println!("pinned at an stable address {:p}", pinned_name);
let mut mutp_to_pinned_name = &mut pinned_name;
let new_val = String::from("wildonion");
// here mutating the underlying data of mutp_to_pinned_name pointer
// mutates the data inside pinned_name name pointer cause mutp_to_pinned_name
// is a mutable pointer to the pinned_name pointer, so putting a new value
// in place of the old one inside the pin pointer will change the address
// of the pinned pointer to another stable address inside the ram cause
// Box::pin(&new_val) is a new value with new address which causes its
// pinned pointer address to be changed
*mutp_to_pinned_name = Box::pin(&new_val);
println!("pinned_name at an stable address {:p}", pinned_name);
println!("pinned_name content {:?}", pinned_name);
//===================================================================================================
//===================================================================================================
//===================================================================================================
// can't have self ref types directly they should be behind some kinda pointer to be stored on the heap like:
// we should insert some indirection (e.g., a `Box`, `Rc`, `Arc`, or `&`) to break the cycle
// also as you know Rust moves heap data (traits, vec, string, structure with these fields, ?Sized types) to clean the ram
// so put them inside Box, Rc, Arc send them on the heap to avoid lifetime, invalidate pointer and overflow issue
// also Arc and Rc allow the type to be clonned
type Fut<'s> = std::pin::Pin<Box<dyn futures::Future<Output=SelfRef<'s>> + Send + Sync + 'static>>;
struct SelfRef<'s>{
pub instance_arc: std::sync::Arc<SelfRef<'s>>, // borrow and is safe to be shared between threads
pub instance_rc: std::rc::Rc<SelfRef<'s>>, // borrow only in single thread
pub instance_box: Box<SelfRef<'s>>, // put it on the heap to make a larger space behind box pointer
pub instance_ref: &'s SelfRef<'s>, // put it behind a valid pointer it's like taking a reference to the struct to break the cycle
pub fut_: Fut<'s> // future objects as separate type must be pinned in the first place
}
the reason that it's better not to move the type if it's behind a pointer the type can be moved however, the pointer can't be used after moving also based on owership and borrowing rules of Rust the pointer gets updated to point to the right location of the new owner after value gets moved cause the ownership of the value will be moved to a new owner after moving.
/*
Rust's ownership and borrowing rules are designed to ensure memory safety and prevent data
races. In this case, the function execute takes ownership of the String name and then returns
it. Since ownership is transferred back to the caller, there are no violations of Rust's
ownership rules.
The fact that pname is created as a mutable reference to name does not affect the ability to
return name from the function. The ownership of name is not affected by the creation of the
mutable reference pname.
Rust's ownership system allows for the transfer of ownership back to the caller when a value
is returned from a function. This is a fundamental aspect of Rust's memory management and
ownership model.
In summary, the provided code is allowed in Rust because it follows Rust's ownership and borrowing
rules, and the ownership of name is transferred back to the caller when it is returned from the function.
*/
fn execute() -> String{
let mut name = String::from("");
let pname = &mut name;
name
}
/*
When name is moved into the async block, it is moved into a separate asynchronous context.
This means that the ownership of name is transferred to the async block, and it is no longer
accessible in the outer scope.
However, the mutable reference pname is still valid in the outer scope, even though the value
it refers to has been moved into the async block. This is because the reference itself is not
invalidated by the move.
In Rust, the borrow checker ensures that references are used safely, but it does not track the
movement of values across asynchronous boundaries. As a result, the mutable reference pname
remains valid in the outer scope, even though the value it refers to has been moved into the
async block.
It's important to note that while this code does not result in a compile-time error, it can lead
to runtime issues if the async block attempts to use the moved value name through the mutable
reference pname after the move.
In summary, Rust's ownership and borrowing rules do not prevent the movement of values across
asynchronous boundaries, and this can lead to potential issues if not carefully managed.
*/
fn execute_() {
let mut name = String::from("");
let pname = &mut name;
tokio::spawn(async move {
println!("{:?}", name);
});
// ERROR:
// println!("can't use the pointer in here after moving name: {:?}", pname);
}
// don't move pointer with short lifetime into tokio::spawn() scope
// since the borrow must live long enough to be valid after moving
// like if it has static lifetime we can move it
let name = String::from("");
let pname = &name; // ERROR: borrow doesn't live long enough cause it has moved into tokio::spawn()
tokio::spawn(async move{
let name = pname;
});
The differences between &mut Type
, Box<Type>
, and Box<&mut Type>
in Rust relate to ownership, borrowing, and memory management. Here's a breakdown of each type and their characteristics:
-
Mutable Reference:
-
&mut Type
represents a mutable reference to a value of typeType
. - It allows mutable access to the referenced value but enforces Rust's borrowing rules, ensuring that there is only one mutable reference to the value at a time.
-
-
Borrowing:
- The reference is borrowed and does not own the value it points to.
- The lifetime of the reference is tied to the scope in which it is borrowed, and it cannot outlive the value it references.
-
Heap Allocation:
-
Box<Type>
is a smart pointer that owns a value of typeType
allocated on the heap. - It provides a way to store values with a known size that can be dynamically allocated and deallocated.
-
-
Ownership Transfer:
-
Box<Type>
transfers ownership of the boxed value to the box itself. - It allows moving the box between scopes and passing it to functions without worrying about lifetimes.
-
-
Boxed Mutable Reference:
-
Box<&mut Type>
is a box containing a mutable reference to a value of typeType
. - It allows for mutable access to the value, similar to
&mut Type
, but with the value stored on the heap.
-
-
Indirection and Ownership:
- The box owns the mutable reference and manages its lifetime on the heap.
- This can be useful when you need to store a mutable reference with a dynamic lifetime or when you want to transfer ownership of the reference.
use box to store on the heap to break the cycle of self ref types and manage the lifetime dynamically on the heap of the type
-
&mut Type
: Represents a mutable reference with borrowing semantics and strict lifetime rules. -
Box<Type>
: Represents ownership of a value on the heap with the ability to move it between scopes. -
Box<&mut Type>
: Represents ownership of a mutable reference stored on the heap, providing flexibility in managing mutable references with dynamic lifetimes.
-
&mut Type
: Use when you need mutable access to a value within a limited scope and want to enforce borrowing rules. -
Box<Type>
: Use when you need to store a value on the heap and transfer ownership between scopes. -
Box<&mut Type>
: Use when you need to store a mutable reference with dynamic lifetime requirements or when you want to manage mutable references on the heap.
Each type has its own use cases based on ownership, borrowing, and memory management requirements in Rust. Understanding the differences between them helps in choosing the appropriate type for your specific needs.
The impl Trait
syntax and Box<dyn Trait>
are both used in Rust for handling trait objects, but they have different implications and usage scenarios. Here are the key differences between impl Trait
in the return type of a method and Box<dyn Trait>
:
-
Static Dispatch:
- When using
impl Trait
in the return type of a method, the actual concrete type returned by the function is known at compile time. - This enables static dispatch, where the compiler can optimize the code based on the specific type returned by the function.
- When using
-
Inferred Type:
- The concrete type returned by the function is inferred by the compiler based on the implementation.
- This allows for more concise code without explicitly specifying the concrete type in the function signature.
-
Single Type:
- With
impl Trait
, the function can only return a single concrete type that implements the specified trait. - The actual type returned by the function is hidden from the caller, providing encapsulation.
- With
-
Dynamic Dispatch:
- When using
Box<dyn Trait>
, the trait object is stored on the heap and accessed through a pointer, enabling dynamic dispatch. - Dynamic dispatch allows for runtime polymorphism, where the actual type can be determined at runtime.
- When using
-
Trait Object:
-
Box<dyn Trait>
represents a trait object, which can hold any type that implements the specified trait. - This is useful when you need to work with different concrete types that implement the same trait without knowing the specific type at compile time.
-
-
Runtime Overhead:
- Using
Box<dyn Trait>
incurs a runtime overhead due to heap allocation and dynamic dispatch. - This can impact performance compared to static dispatch with
impl Trait
.
- Using
-
impl Trait
: Useimpl Trait
when you have a single concrete type to return from a function and want to leverage static dispatch for performance optimization. -
Box<dyn Trait>
: UseBox<dyn Trait>
when you need to work with multiple types that implement a trait dynamically at runtime or when dealing with trait objects in a more flexible and polymorphic way.
In summary, impl Trait
is used for static dispatch with a single concrete type known at compile time, while Box<dyn Trait>
is used for dynamic dispatch with trait objects that can hold different types implementing the same trait at runtime. The choice between them depends on the specific requirements of your code in terms of performance, flexibility, and polymorphism.
Don't try to understand these recaps if you can't, just code fearlessly and allocate heap data as much as you can without returning a reference from methods unless you have to do so! Rust takes care of these for you! it cleans the heap under the hood once a function gest executed or a type moves into a new scope without cloning or passing it by ref, the lifetime of each type has a curcial role in deallocating heap spaces.