Rust traits play a crucial role in making the language safe and performant. They provide abstractions that enforce safety constraints, promote modularity and code reuse, and enable the compiler to perform optimizations such as monomorphization, resulting in more robust and efficient code.
In essence, Rust traits provide an abstract definition of common behavior and allow for method sharing among different types, like interfaces in JavaScript or abstract classes in C++.
The truth is, when working with Rust, it’s important to understand the nuances of the three most commonly used traits: Copy
, Clone
, and the Dynamic
trait object (dyn
). In this article, we will delve into the specifics of each trait and explain their use cases so that you can effectively implement them and make informed decisions in your Rust projects. Let’s get started!
Jump ahead:
Understanding the Copy
trait
It’s no surprise that one of the fundamentals of Rust is the concept of ownership. Ownership determines who is responsible for managing the lifetime of a value. When a value is moved from one place to another, the ownership of the value is transferred to the new location. This is the default behavior in Rust because it helps ensure that values are used safely and predictably, avoiding common problems like null
or dangling pointer errors.
However, if you want to make an exception based on your requirements, Rust provides some functionalities that allow you to override this default behavior as long as you know what you are doing.
The Copy
trait is one such functionality. It is a marker trait that indicates that a type can be copied rather than moved. This means that a copy of the value will be created when the value is assigned to a new variable or is passed as an argument to a function.
For example, let’s create a Math
trait and implement an additional functionality of the trait for an Arithmetic
struct:
pub trait Math { fn add(&self) -> i32; } pub struct Arithmetic { pub num1: i32, pub num2: i32, } impl Math for Arithmetic { fn add(&self) -> i32 { self.num1 + self.num2 } } fn main() { let params: Arithmetic = Arithmetic { num1: 23, num2: 43 }; let parameters: Arithmetic = params; println!("Add: {}", parameters.add()); println!("Add again: {}", params.add()); }
Rust Playground
A browser interface to the Rust compiler to experiment with the language
You’ll get an error
from the Rust borrow checker when you run the code. It should look like this:
Fortunately, the Rust compiler leaves a clear and reasonable error
message that can help us solve the problem. From the image above, it seems that we are trying to copy a value that does not implement the Copy
trait:
let params: Arithmetic = Arithmetic { num1: 23, num2: 43 }; let parameters: Arithmetic = params; // moved params into parameters println!("Add: {}", parameters.add()); println!("Add again: {}", params.add());
Considering the code above, we moved params
value into the parameters
variable. However, we still tried to use params
after it had been moved. That is what triggered that error
.
In a language like Javascript, this isn’t a problem. For example, we can do this in Javascript:
let a = 5; let b =a; console.log("A: ", a); // A console.log("B: ", b); //Result A: 5 B: 5
This is where Copy
traits come in. At first thought, one might think that you can just Derive
the Copy
trait for the Arithmetic
struct by adding the #[Derive(Copy)]
attribute. However, it’s a little tricky because Clone
is a super trait of Copy
. So, to use Copy
, you will need to derive both the Copy
and Clone
traits as shown below:
#[Derive(Copy, Clone)] pub struct Arithmetic { pub num1: i32, pub num2: i32, }
But, not all types implement the Copy
trait. So, let’s look at some of the types that do and those that don’t.
When can my type be Copy
?
A type can implement Copy
if all of its components implement Copy
or it holds a shared reference &T
. We’ll take a look at an example in a bit, but before that, make sure you understand that the following types implement Copy
:
- All integer types: Such as
u8
,i16
,u32
,i64
, and more - The Boolean type:
bool
- All floating-point types: Such as
f32
andf64
- The character type:
char
So, let’s see an example of a struct that implements Copy
:
#[derive(Debug, Copy, Clone)] pub struct ImagePost { id: u32, image_url: &'static str, }
The example above explains the statement “ …if all of its components implement Copy
or it holds a shared reference &T
”. The item image_url
holds a shared reference to a string with a static
lifetime — which means the reference will be valid until the program ends.
In contrast, if we use String
instead of the shared static
string reference, we’ll get an error because String
types in Rust do not implement Copy
. This is because they have a dynamic size, meaning their contents cannot be safely copied and moved between variables without allocating new memory.
The Clone
trait is implemented for String
to allow creating a new String
value with the same contents as an existing one. The code below will throw an error:
#[derive(Debug, Copy, Clone)] pub struct ImagePost { id: u32, image_url: String, }
From our previous example, here is a complete sample code that uses Copy
appropriately:
pub trait Math { fn add(&self) -> i32; } #[derive(Copy, Clone)] pub struct Arithmetic { pub num1: i32, pub num2: i32, } impl Math for Arithmetic { fn add(&self) -> i32 { self.num1 + self.num2 } } fn main() { let params: Arithmetic = Arithmetic { num1: 23, num2: 43 }; let parameters: Arithmetic = params; println!("Add: {}", parameters.add()); println!("Add again: {}", params.add()); }
If you get confused, the Rust Copy
documentation is the best place to reference. In addition, it’s fair to mention that types with dynamically allocated resources, such as Vec<T>
, String
, Box<T>
, Rc<T>
, and Arc<T>
do not implement Copy
. We’ll see how to work with them next as we discuss the Clone
trait.
Looking at the Clone
trait
A type is clonable in Rust if it implements the Clone
trait. This means the type can be duplicated, creating a new value with the same information as the original. The new value is independent of the original value and can be modified without affecting the original value.
To make a type clonable, we simply need to Derive
it as we did the Copy
trait. But this time, we won’t need to Derive
the Copy
trait with the Clone
trait because Clone
does not depend on it.
Now, let’s build on our previous example for Copy
and modify the code to show how Clone
works:
pub trait Math { fn add(&self) -> i32; } #[derive(Clone)] pub struct Arithmetic { pub num1: i32, pub num2: i32, } impl Math for Arithmetic { fn add(&self) -> i32 { self.num1 + self.num2 } } fn main() { let params: Arithmetic = Arithmetic { num1: 23, num2: 43 }; let parameters: Arithmetic = params.clone(); let another_parameter: Arithmetic = parameters.clone(); println!("Add: {}", parameters.add()); println!("Add again: {}", params.add()); }
What changed, you asked?
So, we changed the Arithmetic
struct to derive Clone
alone because we don’t need Copy
in this example. We also called the Clone
method on the moved value parameters
and another_paramter
, as shown here:
let parameters: Arithmetic = params.clone(); let another_parameter: Arithmetic = parameters.clone();
Another interesting benefit of cloning is that you can use Clone
directly on primitive types, as shown below:
fn main(){ let compliment: String = "Smart".to_string(); let another_compliment = compliment.clone(); println!("You are {}", compliment.clone()); println!("You are {} again", another_compliment.clone()); }
This is unlike Copy
which you can only use when the copiable type is used in a struct
, enum
, or union
that derives Copy
and Clone
.
Similarities and differences between Copy
and Clone
In this section, we’ll highlight some common benefits of Clone
and Copy
traits and how they differ.
- Create new values: Both
Copy
andClone
allow you to create new values based on existing values - Implicit vs. explicit: The
Copy
trait is implicit, while theClone
trait requires an explicit call to theclone
method to create a new value - Deep vs. shallow copy: When a value is copied using the
Copy
trait, it creates a shallow copy, a new reference to the original value. When a value is cloned using theClone
trait, it creates a deep copy, which is a new, independent value with the same contents as the original - Performance: Copying using the
Copy
trait is generally more efficient than cloning using theClone
trait because it does not require the allocation of new memory, as using theClone
trait does - Restrictions: The
Copy
trait has some restrictions, such as not being able to implementDrop
or having any interior mutability. TheClone
trait has fewer restrictions and can be implemented for a broader range of types
The Dynamic
trait object
A Dynamic
trait object, also known as a dyn
, is a keyword in Rust used to handle values that can have different types at runtime. Essentially, it allows us to write code that can work with values of different types consistently without knowing exactly what type each value is beforehand. Using dyn
makes our code more flexible and easier to maintain because we don’t have to write separate code for each type of value.
For context, let me give you a real-life scenario. Imagine you have a pet store that sells dogs and cats. You want to write a program that makes all of your pets make a sound. To do this, you create two structs, Dog
and Cat
, that represent your pets:
struct Dog; struct Cat;
You also create a trait, Animal
, which specifies what methods all animals should have, like a method to make a sound:
trait Animal { fn make_sound(&self) -> &str; }
Next, you implement the Animal
trait for both Dog
and Cat
. This means you write code that tells the Dog
and Cat
structs how to make a sound:
impl Animal for Cat { fn make_sound(&self) -> &str { "Meow!" } }
Now, you want to store all of your pets in a single data structure, so you create a Vec<Box<dyn Animal>>
. This vector can hold values of any type that implements the Animal
trait, which includes both Dog
and Cat
:
let animals: Vec<Box<dyn Animal>> = vec![Box::new(Dog), Box::new(Cat)];
So, the scenario above is where the dyn
shines because it’s readable and maintainable. However, there is a caveat to using dyn
, which I’ll explain in the next section.
Lastly, you use a for loop
to make all of the pets in the vector make a sound. When you call the make_sound
method on each animal, the program will automatically call the correct implementation for each type of animal, whether it’s a Dog
or a Cat
. Here’s what that looks like:
for animal in animals { println!("{}", animal.make_sound()); }
So, at the end of the day, we’ve been able to create a program that allows our pets to make sounds following a sort of object-oriented design, and it can we can easily add as many pets and actions as we want. Here is the complete source code that was made possible by dyn
:
trait Animal { fn make_sound(&self) -> &str; } struct Dog; impl Animal for Dog { fn make_sound(&self) -> &str { "Woof!" } } struct Cat; impl Animal for Cat { fn make_sound(&self) -> &str { "Meow!" } } fn main() { let animals: Vec<Box<dyn Animal>> = vec![Box::new(Dog), Box::new(Cat)]; for animal in animals { println!("{}", animal.make_sound()); } }
Advantages and disadvantages of using dyn
Just like the Clone
and Copy
traits, there are some good reasons you might want to use dyn
. These include polymorphism, dynamic dispatch code reuse, and interoperability:
- Polymorphism: Enables you to write generic code that can work with values of different types in a consistent way
- Dynamic dispatch:
dyn
allows for dynamic dispatch, meaning that the type of a value can be determined at runtime - Code reuse: By using
dyn
, you can write code that can be reused for different types of values, making your code more flexible and easier to maintain - Interoperability: It is possible to use
dyn
to create interoperability between different libraries or modules, allowing you to use values of different types from different parts of your code
There are also some reasons not to use dyn
:
- Performance: Because
dyn
values have to be checked for their type at runtime, they may be slower than values with a known type, known as monomorphized values - Difficult to debug: Debugging code that uses
dyn
can be more difficult, as it can be hard to determine the exact type of a value at runtime
So, with our current knowledge, let’s refactor our application to use monomorphism — a situation where the types are known at compile time instead of runtime:
trait Animal { fn make_sound(&self) -> &str; } struct Dog; impl Animal for Dog { fn make_sound(&self) -> &str { "Woof!" } } struct Cat; impl Animal for Cat { fn make_sound(&self) -> &str { "Meow!" } } enum AnimalType { DogType(Dog), CatType(Cat), } impl Animal for AnimalType { fn make_sound(&self) -> &str { match self { AnimalType::DogType(dog) => dog.make_sound(), AnimalType::CatType(cat) => cat.make_sound(), } } } fn main() { let dog = AnimalType::DogType(Dog); let cat = AnimalType::CatType(Cat); let animals = vec![dog, cat]; for animal in animals.iter() { println!("{}", animal.make_sound()); } }
In the code above, we introduced an enum
to allow for a type-safe representation of multiple types of data in a single type. In our case, the enum
allowed for the creation of a single type that could store either a value of type Dog
, Cat
, etc. Then we created the instances of Dog
and Cat
, stored them in a vec
, iterated over them, and called the make sound
method on all of them.
That changed it completely from using the dyn
ensuring that it runs on compile time and reduces the complexity of debugging in case something goes wrong. It does not mean that the other implementation is wrong! This depends on your project needs, and you should decide what works best and is more efficient for your use case. Knowing there are several ways to solve the problem is good.
Conclusion
So far, so good. We’ve been able to disambiguate some of Rust’s most popular traits that seem a little confusing for beginners, the Clone
, Copy
, and Dynamic
traits. This knowledge will allow you to write better Rust programs that require using these traits.
The post Understanding Rust disambiguating traits: <code>Copy</code>, <code>Clone</code>, and <code>Dynamic</code> appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/Uy6IK7s
Gain $200 in a week
via Read more