Understanding Rust disambiguating traits: Copy, Clone, and Dynamic

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 and f64
  • 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());
}

Run code in Rust Playground

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 and Clone allow you to create new values based on existing values
  • Implicit vs. explicit: The Copy trait is implicit, while the Clone trait requires an explicit call to the clone 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 the Clone 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 the Clone trait because it does not require the allocation of new memory, as using the Clone trait does
  • Restrictions: The Copy trait has some restrictions, such as not being able to implement Drop or having any interior mutability. The Clone 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.

LogRocket: Full visibility into web frontends for Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — .


Source link