8.3 Function Parameter Types in Rust
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 when passed by value.
8.3.1 Value Parameters
The parameter is passed as an immutable value. For types that do not implement Copy
, the instance is moved into the function:
fn consume(value: String) { println!("Consumed: {}", value); } fn main() { let s = String::from("Hello"); consume(s); // s is moved and cannot be used here. }
Note: The function takes ownership of the string but cannot modify it, as the parameter was not declared mut
.
Use Cases:
- When the function requires full ownership, such as for resource management or transformations.
- When returning the value after modification.
Comparison to C:
- Similar to passing structs by value in C, except Rust prevents access to
s
after it is moved.
8.3.2 Mutable Value Parameters
In this case, the parameter is passed as a mutable value. The function can mutate the parameter, and for types that do not implement Copy
, a move occurs:
fn consume(mut value: String) { value.push('!'); println!("Consumed: {}", value); } fn main() { let s = String::from("Hello"); consume(s); // s is moved and cannot be used here. }
Note: It is not required to declare s
as mut
in main()
.
Use Cases:
- Modifying a value without returning it (though this does not modify the original variable in the caller).
- Particularly useful with heap-allocated types (
String
,Vec<T>
) when the function wants ownership.
Comparison to C:
- Unlike passing a struct by value in C, Rust’s ownership model prevents accidental aliasing.
8.3.3 Reference Parameters
A function can borrow a value without taking ownership by using a shared reference (&
):
fn print_length(s: &String) { println!("Length: {}", s.len()); } fn main() { let s = String::from("Hello"); print_length(&s); // s is still accessible here. }
Use Cases:
- When only read access to data is required.
- Avoiding unnecessary copies for large data structures.
Comparison to C:
- Similar to passing a pointer (
const char*
) for read-only access in C.
8.3.4 Mutable Reference Parameters
A function can borrow a mutable reference (&mut
) to modify the caller’s value without taking ownership:
fn add_exclamation(s: &mut String) { s.push('!'); } fn main() { let mut text = String::from("Hello"); add_exclamation(&mut text); println!("Modified text: {}", text); // text is modified }
Note: The variable must be declared as mut
in main()
to pass it as a mutable reference.
Use Cases:
- When the function needs to modify data without transferring ownership.
- Avoiding unnecessary cloning or copying of data.
Comparison to C:
- Similar to passing a pointer (
char*
) for modification. - Rust enforces aliasing rules at compile time, preventing multiple mutable borrows.
8.3.5 Returning Values and Ownership
A function can take and return ownership of a value, often after modifications:
fn to_upper(mut s: String) -> String { s.make_ascii_uppercase(); s } fn main() { let s = String::from("hello"); let s = to_upper(s); println!("Uppercased: {}", s); }
Use Cases:
- When the function modifies and returns ownership rather than using a mutable reference.
- Useful for transformations without creating unnecessary clones.
Re-declaring Immutable Parameters as Mutable Locals
You can re-declare immutable parameters as mutable local variables. This allows calling the function with a constant argument but still having a mutable variable in the function body:
fn test(a: i32) { let mut a = a; // re-declare parameter a as a mutable variable a *= 2; println!("{a}"); } fn main() { test(2); }
8.3.6 Choosing the Right Parameter Type
Parameter Type | Ownership | Modification Allowed | Typical Use Case |
---|---|---|---|
Value (T ) | Transferred | No | When ownership is needed (e.g., consuming a String ) |
Reference (&T ) | Borrowed | No | When only reading data (e.g., measuring string length) |
Mutable Value (mut T ) | Transferred | Yes, but local only | Occasionally for short-lived modifications, but less common |
Mutable Reference (&mut T ) | Borrowed | Yes | When modifying the caller’s data (e.g., updating a Vec<T> ) |
Rust’s approach to parameter passing ensures memory safety while offering flexibility in choosing ownership and mutability. By selecting the proper parameter type, functions can operate efficiently on data without unnecessary copies, fully respecting Rust’s ownership principles.
Side note: In Rust, you can also write function signatures like
fn f(mut s: &String)
orfn f(mut s: &mut String)
. However, addingmut
before a reference parameter only rebinds the reference itself, not the underlying data (unless it is also&mut
). This is uncommon in typical Rust code.