When you're writing code in Rust, you often need to pass data around between different parts of your program. For small pieces of data, like simple numbers or short text, this is usually straightforward. But what about small custom data types, called structs? Should you pass them around by making a copy, or by borrowing them?
This is a common question that pops up for many Rust developers. The answer might seem simple at first, but there are some important details to consider. Getting this right can make your code run a little faster and be easier to understand. Let's break down the best practices for handling small structs in Rust.
Understanding
Copy and Borrow in Rust
Before we talk about structs, let's quickly review what copy and borrow mean in Rust. When you *copy
- a value, you're essentially making a brand new, identical copy of that value. The original value and the new copy are completely separate. Think of it like photocopying a document; you get two independent versions.
Borrowing, on the other hand, is different. When you borrow a value, you're not making a copy. Instead, you're giving another part of your code temporary permission to look at or use the original value. The borrower doesn't own the data; they just have access to it for a limited time. This is like letting a friend read a book you own without giving them a copy.
Rust has strict rules about borrowing to prevent common programming errors like having multiple parts of your code try to change the same data at the same time. This is a big reason why Rust is known for being safe.
The Case for Copying Small Structs
So, when should you choose to copy a small struct? The general rule of thumb in Rust is that if a type is small and cheap to copy, it should probably implement the Copy trait. This trait tells Rust that it's okay to make copies of values of this type freely.
What counts as "small"? Usually, this means structs that contain only other types that are themselves Copy. Think of simple structs made up of integers, booleans, or characters. These types are very fast for the computer to duplicate.
When a struct is marked as Copy, passing it to a function or assigning it to a new variable automatically creates a copy. This can be very convenient. You don't have to worry about accidentally invalidating the original value because you're always working with a fresh copy.
Why Borrowing Might Seem
Like the Default
Many developers coming to Rust from other languages might instinctively think about borrowing everything. In languages like C++ or Java, passing objects by reference (which is similar to borrowing) is often the default way to avoid expensive copying. This is a good habit for many situations.
However, Rust's Copy trait is specifically designed for types that are inexpensive to duplicate. For these types, the cost of copying is so low that it's often less overhead than managing borrows. Managing borrows involves keeping track of who has access to the data and for how long, which can sometimes add a small amount of complexity.
If you have a struct that contains, for example, a String or a Vec (which are not Copy types), then you cannot simply make the struct Copy. In these cases, you would naturally fall back to borrowing.
The Performance Angle: Copy vs.
Borrow Overhead
Let's talk about speed. When you copy a small, Copy-able struct, the computer just duplicates its bits. This is usually a very fast operation, often just a few CPU instructions. The compiler is very good at optimizing these copies.
When you borrow, Rust needs to ensure that the borrowing rules are followed. This involves some checks, especially when you have mutable borrows (where you can change the data). While Rust's borrow checker is incredibly efficient, there's still a tiny bit of overhead involved in managing these checks and lifetimes.
For very small structs that are Copy, the overhead of copying is often less than the overhead of borrowing. This is a key insight that might surprise developers new to Rust.