try.directtry.direct

How to avoid multiple clone() in Rust

Here at TryDirect, our team is passionate about creating fast, efficient software. Sometime when I work on a project, I fall into the habit of using the clone() method to copy values. It's the easiest way to achieve results quickly, but I've learned it's a common mistake that needs to be avoided. How we overcame our cloning dependency and embraced more efficient practices?


The Project Begins

We were tasked with developing a new feature for our product, a real-time data processing module.


Lets take first and most common example from the real project

 let tag = self.tag.clone().unwrap_or("latest".to_string());

Here’s how you can refactor it to avoid cloning:

let t = self.tag.as_deref().unwrap_or("latest");

Explanation:

  1. as_deref() Method: The as_deref() method converts an Option<String> to an Option<&str>. It does this by converting the String inside the Option to a &str slice, thereby avoiding the need for cloning.
    If self.tag is Some(String), as_deref() will turn it into Some(&str).
    If self.tag is None, it stays None.
  2. unwrap_or("latest"): Now, with Option<&str>, you can use unwrap_or("latest") to provide a default string slice "latest" if the Option is None.

This way, the string "latest" is only created when necessary, and no cloning is done when the tag is present.


Discovering Borrowing

Next step towards efficiency could be embracing borrowing.

Why clone data when we can just borrow it? We can use references to access data without taking ownership. So here is a first example of how to avoid cloning.


fn process_data(data: &str) {
// Use data without taking ownership
}

let my_data = String::from("Hello, world!");

process_data(&my_data); // Pass a reference

// my_data can still be used here

Slicing Through Inefficiency

Next, we turned our attention to collections and strings. Instead of cloning entire vectors or strings, we used slices.


fn sum_slice(slice: &[i32]) -> i32 {
slice.iter().sum()
}

let numbers = vec![1, 2, 3, 4, 5];

let result = sum_slice(&numbers); // Pass a slice

By passing slices, we can reduce unnecessary data duplication, and functions become more versatile.


Embracing Smart Pointers

As our project grew, we needed a way to share data across multiple components. That's when we discovered Rc<T> (Reference Counted) smart pointers. These allowed us to share ownership without cloning.


use std::rc::Rc;

fn share_data(data: Rc<String>) {
println!("{}", data);
}

let shared_data = Rc::new(String::from("Hello, Rc!"));
share_data(shared_data.clone()); // Rc::clone() is cheap
share_data(Rc::clone(&shared_data)); // This is also a cheap operation

The Power of Cow

There is one another method that might help. Cow (Copy on Write). With Cow, you can work with borrowed data and only clone it if necessary.


Code example:


use std::borrow::Cow;

fn modify_string(input: Cow<str>) -> Cow<str> {
if input.contains("bad") {
let mut owned = input.into_owned();
owned.replace_range(.., "good");
Cow::Owned(owned)
} else {
input
}
}

let borrowed: Cow<str> = Cow::Borrowed("Hello");

let modified = modify_string(borrowed); // No clone here

let owned: Cow<str> = Cow::Owned(String::from("bad string"));

let modified_owned = modify_string(owned); // Clones only if modified

Cow provided the flexibility we needed without compromising efficiency.


Iterators: The Lazy Path to Performance

Iterators became our best friends for processing data lazily. We transformed and consumed elements without creating intermediate collections. Basic example:



fn sum_even_numbers<I>(iter: I) -> i32
where
I: Iterator<Item = i32>,
{
iter.filter(|&x| x % 2 == 0).sum()
}

let numbers = vec![1, 2, 3, 4, 5];

let result = sum_even_numbers(numbers.into_iter());

This approach not only improved performance but also made our code more readable.


Moving to Better Semantics

Sometimes, we realized we didn't need the original data after a certain point. Instead of cloning, we moved ownership.


fn take_ownership(s: String) {
println!("{}", s);
}

let my_string = String::from("Hello, world!");

take_ownership(my_string); // my_string is moved here

// my_string cannot be used here anymore

By moving data, we avoided unnecessary copies and kept our code efficient.


Interior Mutability with RefCell

For scenarios requiring mutable access to shared data, RefCell<T> provided a solution. It allowes us to mutate data even when it was behind an immutable reference.


use std::cell::RefCell;

fn mutate_data(data: &RefCell<i32>) {
*data.borrow_mut() += 1;
}

let data = RefCell::new(5);

mutate_data(&data);

println!("{}", data.borrow()); // Output: 6

This approach ensured safe runtime borrow checking without compromising performance.


Conclusion: A Cloning-Free Future

By avoiding unnecessary cloning and embracing efficient Rust practices, you can improve both performance and code quality. This journey taught us the value of understanding and leveraging Rust's powerful features to write efficient, idiomatic code.

And so, the story of TryDirect continues, now with a commitment to avoiding clone() whenever possible, always striving for efficiency and excellence in every project we undertake.


The Related discussion

https://users.rust-lang.org/t/can-i-avoid-cloning/56069
https://www.reddit.com/r/rust/comments/17luh6c/how_can_i_avoid_cloning_everywhere/