Functions
Write a Rust Function |
Write a Generic Function |
Return a Reference from a Function |
Work with Diverging Functions |
Work with Function Pointers |
Write a Rust Function
Functions are fundamental building blocks in Rust, used to encapsulate reusable blocks of code. Functions are defined with fn
, followed by the function name, optional parameters between parentheses (...)
, optional ->
followed by the return type, and curly braces { ... }
for the function body. Rust uses snake_case for function names by convention.
Here are examples of functions with or without parameters or return value:
/// Function with no parameters, no return type. fn say_hello() { println!("Hello, Rust!"); } /// Function with one parameter called `name` of type `&str` (string slice). fn greet(name: &str) { println!("Hello, {name}!"); } // Function with two parameters: `a` and `b`, both of type `i32`. fn add_and_display(a: i32, b: i32) { let sum = a + b; println!("{a} + {b} = {sum}"); } /// Function that takes two `i32` values and returns their sum as an `i32`. fn multiply(x: i32, y: i32) -> i32 { x * y // No semicolon: this expression's value is returned. } /// Function bodies follow the same rules than blocks: #[allow(clippy::let_and_return)] fn foo(x: i32) -> i32 { let y = { let z = 3; x + z // Blocks return the value of their last expression, if there is no semi-colon. }; y // Similarly, functions return the value of their last expression, if there is no semi-colon. } /// Function that implicitly returns the unit type `()`. /// This is equivalent to `fn print_coordinates(x: i32, y: i32) -> ()`. fn print_coordinates(x: i32, y: i32) { println!("Coordinates: ({x}, {y})"); // Note the semicolon. // Implicitly return `()`. } /// Function using an explicit `return` keyword (rarely needed). #[allow(clippy::manual_find)] fn find_first_even(numbers: &[i32]) -> Option<i32> { for &num in numbers { if num % 2 == 0 { return Some(num); // Return early, breaking out of the `for` loop. } } None // Return `None` if no even number is found (last expression). } fn main() { // Call the functions above. say_hello(); greet("Rustacean"); add_and_display(5, 3); println!("2 * 3 = {}", multiply(2, 3)); println!("Foo: {}", foo(1)); print_coordinates(1, 2); println!( "First even number: {:?}", find_first_even(vec![1, 2, 3].as_slice()) ); }
Write a Generic Function
Functions can be generic, meaning they can operate on parameters of various types without needing to be rewritten for each type:
// This is a generic function that can take any type. // The type parameter `T` is written between < and >. // It often can be inferred from the argument passed to the function. fn generic<T>(_: T) { println!("Argument type: {}", std::any::type_name::<T>()); } /// A generic function can take multiple type parameters. /// /// This function takes two arguments of potentially different types: fn take_two<T, U>(_a: T, _b: U) { println!( "Argument types: {} {}", std::any::type_name::<T>(), std::any::type_name::<U>() ); } fn main() { // The type parameter `T` is inferred to be `char` in this case. generic('a'); // The type parameter `T` is inferred to be `i32` in this case. generic(1); // We can also explicitly specify the type parameter `char` for `generic()`. // Note the use of the 'turbofish' notation: `::<>` generic::<char>('a'); take_two(Some(3), "4"); }
Type parameters can be constrained to implement specified traits:
use std::fmt::Debug; use std::fmt::Display; /// Generic functions can accept trait bounds. // Here we define a generic function `print_value` that takes one argument // `value` of any type `T`. The `T: std::fmt::Debug` part is a trait bound, // meaning `T` must implement the `Debug` trait, so that it can be printed using // the `{:?}` format specifier. fn print_value<T: Debug>(value: T) { println!("The value is: {value:?}"); // `{:?}` can be used, because `value` implements `Debug`. } /// Type constraints can be written in a separate `where` clause for clarity. /// You can also combine multiple trait bounds with `+`. fn print_value2<T>(value: T) where T: Debug + Display, { println!("Compare {value} and {value:?}"); } /// All type parameters have an implicit bound of `Sized`. /// The special syntax `?Sized` can be used to remove this bound if it is not /// appropriate. /// /// This function can work with types that may not have a known size at compile /// time. fn generic<T: ?Sized + Display>(t: &T) { println!("{t}"); } fn main() { // Call the generic function with an integer. print_value(10); // Call the generic function with a floating-point number. print_value(2.14); // Call the generic function with a string slice. print_value("hello"); // Call the generic function with a custom struct that implements `Debug`. #[allow(dead_code)] #[derive(Debug)] struct MyStruct { value: i32, } print_value(MyStruct { value: 42 }); let s = String::from("hello"); generic(&s[..]); generic(&s); }
Functions can also include one or more explicit lifetime parameters, which are also written with < and >. Generic type parameters, lifetime parameters and const
generic parameters can be intermixed, but with lifetime parameters first.
/// Function with an explicit lifetime parameter. /// /// `x` and `y` are string slices that live at least as long as the lifetime /// `'a`. The reference returned by the longest function will have the same /// lifetime as the lifetime `'a`. fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } fn main() { // The compiler infers the concrete lifetime for `'a` at the call site, // choosing the shorter of the two input lifetimes. let result = longest("abcd", "xyz"); println!("The longest string is {result}"); }
Work with Diverging Functions
Functions that are guaranteed never to return (e.g., they loop forever or exit the process) have the special return type !
(the "never" type).
/// This function diverges, meaning it never returns. /// It uses the `!` (Never) return type to indicate this. fn foo() -> ! { panic!("This call never returns."); } // This function never returns control. #[allow(dead_code)] fn exit_program() -> ! { println!("Exiting the process..."); std::process::exit(1); } // This function loops forever. #[allow(dead_code)] fn forever() -> ! { loop { println!("and ever..."); std::thread::sleep(std::time::Duration::from_secs(1)); } } fn main() { foo(); // This will panic. }
Work with Function Pointers
Functions are first-class citizens in Rust. This means you can treat them like any other value: assign them to variables, pass them as arguments to other functions, etc. The type of a function pointer looks like fn(i32) -> i32
.
/// Example function: add one to the input. fn add_one(x: i32) -> i32 { x + 1 } /// This function takes a function pointer in argument. fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 { f(arg) + f(arg) } fn main() { let func_ptr: fn(i32) -> i32 = add_one; // Assign a function pointer to a variable. println!("Calling via the pointer: {}", func_ptr(10)); // Output: 11. let answer = do_twice(add_one, 5); // Pass `add_one` as an argument. println!("The answer is: {answer}"); // Output: 12 ( (5+1) + (5+1) ) }
Return a Reference from a Function
You cannot directly return a reference to a local variable created within and owned by a function:
- Local variables reside on the stack. This memory is typically deallocated when the function finishes executing.
- Returning a reference to a local variable would result in a dangling reference. The reference would point to a memory location that is no longer valid or might be overwritten later, leading to undefined behavior.
In Rust, the borrow checker prevents returning references to local data, enforcing memory safety:
#![allow(unused)] fn main() { // Compile Error[E0515]: cannot return reference to temporary value. fn try_create<'a>() -> &'a String { let s = String::new(); &s // References are pointers to memory locations. Once the function is executed, the local variable is popped off the execution stack. } #[test] fn test() { let _ = try_create(); } }
Instead, you should typically:
- Return an owned value (
String
instead of&str
,Vec<T>
instead of&[T]
, etc.). - Pass a reference as an argument: If the goal is to modify a variable, pass a mutable reference to a variable that exists in the calling scope. The function can then directly modify the original variable.
- Use a literal, a static or a constant: Since a
static
variable lives as long as the process runs, its references will be pointing to the same memory location both inside and outside the function.
/// Return an owned type instead of a reference. fn return_string() -> String { String::new() } /// Pass a mutable reference, /// and modify in the function. fn use_mut_ref(v: &mut Vec<i32>) { *v = vec![0; 3]; } /// Variant: pass a mutable reference, /// return an immutable reference. fn use_mut_ref2(v: &mut Vec<i32>) -> &[i32] { *v = vec![0; 3]; &*v } /// The following is possible but should be rare: /// Use a literal. /// This applies broadly to any item that is composed solely of literals. fn use_literal() -> &'static str { let s: &'static str = "hello"; s } /// Use a static variable. fn use_static() -> &'static str { // Statics have by default a `'static` lifetime. static MY_STATIC: &str = "hello"; MY_STATIC } /// Use a constant. fn use_const() -> &'static str { // Constants have by default a `'static` lifetime. const MY_CONST: &str = "hello"; MY_CONST } // You might also use `Box::leak`. fn main() { let s = return_string(); assert!(s.is_empty()); let mut v = Vec::default(); use_mut_ref(&mut v); println!("{v:?}"); assert_eq!(v, vec![0; 3]); let m = &mut v; println!("{:?}", use_mut_ref2(m)); assert_eq!(v, vec![0; 3]); assert_eq!(use_literal(), "hello"); assert_eq!(use_static(), "hello"); assert_eq!(use_const(), "hello"); }
You can also use std::borrow::Cow
to generalize over owned data and unowned references. See the COW chapter.
Related Topics
- Closures.
- Rust Patterns.