8.3 Function Parameters and Data Passing
Rust functions can accept parameters in various forms, each affecting ownership, mutability, and borrowing. Within a function’s body, parameters behave like ordinary variables. This section describes the fundamental parameter types, when to use them, and how they compare to C function parameters.
We will illustrate parameter passing with the String
type, which is moved into the function when passed by value and can no longer be used at the call site. Note that primitive types implementing the Copy
trait will be copied by value instead of moved.
8.3.1 Passing by Value (T
)
When a parameter has type T
(and T
does not implement Copy
), the value is moved into the function. The function takes ownership, and the original variable in the caller’s scope becomes inaccessible.
// This function takes ownership of the String. fn process_string(s: String) { println!("Processing owned string: {}", s); // 's' goes out of scope here, and the memory is deallocated. } fn main() { let message = String::from("Owned data"); process_string(message); // Ownership of 'message' is transferred to process_string. // Trying to use 'message' here would cause a compile-time error: // println!("Original message: {}", message); // Error: value borrowed after move }
- Use Cases: Primarily when the function needs to consume the value (e.g., send it elsewhere, store it permanently) or take final ownership (ensuring the value is dropped or managed exclusively by the function). This pattern guarantees that the original variable cannot be used after the call. It’s also used when the function manages the lifecycle of a resource represented by
T
. While a functionfn transform(value: T) -> U
can exist, ifvalue
isn’t modified in place (which it can’t be ifT
isn’tmut
), often taking&T
might be more flexible if the original isn’t meant to be consumed. - Comparison to C: Similar to passing a struct by value, but Rust’s borrow checker prevents using the original variable after the move.
8.3.2 Passing by Mutable Value (mut T
)
You can declare a value parameter as mutable using mut T
. Ownership is still transferred (for non-Copy
types), but the function is allowed to modify the value it now owns.
// This function takes ownership and can modify the owned value. fn modify_string(mut s: String) { // 'mut s' allows modification inside the function s.push_str(" (modified)"); println!("Modified owned string: {}", s); // s is dropped here unless returned } // Example of modifying and returning ownership fn modify_and_return(mut s: String) -> String { s.push_str(" and returned"); s // Return ownership of the modified string } fn main() { // NOTE: 'message' does NOT need to be 'mut' here! let message = String::from("Mutable owned data"); // modify_string takes ownership, message cannot be used after modify_string(message); // println!("{}", message); // Error: use of moved value let message2 = String::from("Another message"); // modify_and_return takes ownership, but returns it let modified_message2 = modify_and_return(message2); // println!("{}", message2); // Error: use of moved value 'message2' println!("{}", modified_message2); // Ok: "Another message and returned" }
Note on Caller Variable Mutability: Notice in the examples that
message
andmessage2
were declared usinglet
, notlet mut
. When passing by value (mut T
), the function takes full ownership via a move. Themut
in the function signature (e.g.,mut s: String
) only grants the function permission to mutate the value it now exclusively owns. Since the caller loses ownership and cannot access the original variable after the move, whether the original variable was declaredmut
is irrelevant.This contrasts sharply with passing a mutable reference (
&mut T
), where the caller retains ownership and merely lends out mutable access. To grant this mutable borrow permission, the caller’s variable must be declared withlet mut
.
- Use Cases: When the function needs to take ownership and modify the value it now owns. This could be for internal computations, using the value as a mutable scratch space, or for patterns like functional builders/chaining. In such patterns, a configuration object or state might be passed through several functions, each taking ownership via
mut T
, modifying it in place, and then returning ownership (fn step(mut config: Config) -> Config
). This can be efficient as it may avoid allocations needed if new instances were created at each step. However, for simply modifying the caller’s original data without transferring ownership back and forth,&mut T
remains the more common choice. - Comparison to C: Similar to passing a struct by value regarding locality (changes don’t affect the caller), but distinct due to Rust’s move semantics. Modifications inside the function apply only to the specific instance whose ownership was transferred into the function via the move.
8.3.3 Passing by Shared Reference (&T
)
To allow a function to read data without taking ownership, pass a shared reference (&T
). This is known as borrowing. The caller retains ownership, and the data must remain valid while the reference exists.
// This function borrows the String immutably. fn calculate_length(s: &String) -> usize { s.len() // Can read from 's', but cannot modify it. } fn main() { let message = String::from("Immutable borrow"); let length = calculate_length(&message); // Pass a reference to 'message'. println!("The length of '{}' is {}", message, length); // 'message' is still valid and owned here. }
- Use Cases: Very common when a function only needs read-access to data. Avoids costly cloning or ownership transfer.
- Comparison to C: Similar to passing a pointer to
const
data (e.g.,const char*
orconst MyStruct*
). Rust guarantees at compile time that the referenced data cannot be mutated through this reference and that the data outlives the reference.
8.3.4 Passing by Mutable Reference (&mut T
)
To allow a function to modify data owned by the caller, pass a mutable reference (&mut T
). This is also borrowing, but exclusively – while the mutable reference exists, no other references (mutable or shared) to the data are allowed.
// This function borrows the String mutably. fn append_greeting(s: &mut String) { s.push_str(", World!"); // Can modify the borrowed String. } fn main() { // 'message' must be declared 'mut' to allow mutable borrowing. let mut message = String::from("Hello"); append_greeting(&mut message); // Pass a mutable reference. println!("Modified message: {}", message); // Output: Modified message: Hello, World! // 'message' is still owned here, but its content has been changed. }
- Use Cases: Very common when a function needs to modify data in place without taking ownership (e.g., modifying elements in a vector, updating fields in a struct).
- Comparison to C: Similar to passing a non-
const
pointer (e.g.,char*
orMyStruct*
) to allow modification. Rust’s borrow checker provides stronger safety guarantees by preventing simultaneous mutable access or mixing mutable and shared access, eliminating data races at compile time.
8.3.5 Summary Table: Choosing Parameter Types
Parameter Type | Ownership | Modification of Original | Caller Variable mut Required? | Typical Use Case | C Analogy (Approximate) |
---|---|---|---|---|---|
T (non-Copy ) | Transferred | No | No | Consuming data, final ownership transfer | Pass struct by value |
T (Copy type) | Copied | No | No | Passing small, cheap-to-copy data | Pass primitive by value |
mut T (non-Copy ) | Transferred | No (Local owned value) | No | Modifying owned value before consumption/return | Pass struct by value |
&T | Borrowed | No | No | Read-only access, avoiding copies | const T* |
&mut T | Borrowed | Yes | Yes | Modifying caller’s data in-place | T* (non-const ) |
(Self-correction: Minor tweak in table description for mut T
to be clearer)
Note on Shadowing Parameters: You can declare a new local variable with the same name as an immutable parameter, making it mutable within the function’s scope. This is called shadowing.
fn process_value(value: i32) { // 'value' parameter is immutable. // Shadow 'value' with a new mutable variable. let mut value = value; value += 10; println!("Processed value: {}", value); } fn main() { process_value(5); // Prints: Processed value: 15 }
Side Note on mut
with Reference Parameters:
In Rust, you might occasionally encounter function signatures like fn func(mut param: &T)
or fn func(mut param: &mut T)
. Adding mut
directly before the parameter name (mut param
) makes the binding param
mutable within the function’s scope. This means you could reassign param
to point to a different value of type &T
or &mut T
respectively.
- For
mut param: &T
, this does not allow modifying the data originally pointed to byparam
, because the type&T
represents a shared, immutable borrow. - For
mut param: &mut T
, the underlying data can be modified because the type&mut T
is a mutable borrow, regardless of whether the bindingparam
itself ismut
.
This pattern of making the reference binding itself mutable is relatively uncommon in idiomatic Rust compared to simply passing &T
or &mut T
.