Rust: Rust Functions and Scope
Last updated: 2026-08-26
Functions are first-class citizens in Rust—their parameter and return types serve as the compiler's first line of defense.
Functions are the basic building blocks of code. Rust's function syntax is similar to that of C-family languages, but it has a unique style when it comes to argument pattern matching and expression return values.
1. What You'll Learn
- Use
fnto define a function and specify the parameter and return types - Three Ways to Pass Parameters: Pass-by-Value, Pass-by-Reference, and Mutable Reference
- Function Return Values and Early Return
- Block Scope and Variable Shading
- Functions as the basic units of code organization
- An Introduction to Function Pointers and Higher-Order Functions
2. The Story of a Coffee Shop
(1) The struggle: There's no standardized process for making coffee
Lisa opened a coffee shop, and business has been getting better and better, but problems have also arisen:
- Every barista makes lattes in a different way
- Some add milk first, then coffee; others add coffee first, then milk.
- A customer complained, "The same latte tastes different every time."
- Lisa wants to write a standardized recipe—just like a function: input the amounts of milk and coffee, and it outputs a latte with a consistent flavor.
"It would be great if making coffee were as consistent as calling a function: the same input always produces the same output."
(2) Approach for Rust Functions
// How to Make a Latte"Function":Input Parameters,Brew Coffee
fn make_latte(milk_ml: i32, coffee_ml: i32) -> String {
let result = format!("Latte Complete:{}ml Milk + {}ml Coffee,Fragrant and Delicious!", milk_ml, coffee_ml);
result // No semicolons——Expression Return Value
}
fn main() {
let my_coffee = make_latte(200, 30);
println!("{}", my_coffee);
let another = make_latte(150, 45);
println!("{}", another);
}
Functions are like coffee recipes: the parameters are the ingredients (amount of milk, amount of coffee), and the return value is the finished product. The same input guarantees the same output—this is the determinism of functions.
3. Function Definitions
(1) Basic Syntax
fn function_name(param1: Type1, param2: Type2) -> ReturnType {
// Function Body
return_value // Expression returns(No semicolons)
}
graph TB
A[fn Function Name] --> B[(List of Parameters)]
A --> C[-> Return Type]
A --> D[{ Function Body }]
B --> E[Each parameter: name: Type]
D --> F[Final expression = Return Value]
D --> G[return Keywords = Return Early]
(2) Various Forms of Parameters and Return Values
| Form | Syntax | Example |
|---|---|---|
| No parameters, no return value | fn foo() |
fn greet() { println!("hi"); } |
| Takes parameters, returns nothing | fn foo(x: i32) |
fn show(n: i32) { println!("{}", n); } |
| With parameters and a return value | fn foo(x: i32) -> i32 |
fn double(x: i32) -> i32 { x * 2 } |
| Multiple return values (tuple) | fn foo() -> (i32, bool) |
fn stats() -> (i32, bool) { (42, true) } |
| Go Back | return value; |
if x < 0 { return 0; } |
Core Rule: The last expression in the function body is the return value (no semicolon). Use the
returnkeyword for an early return.
(3) Parameter Passing Methods
| Passing Method | Syntax | Change in Ownership | Applicable Scenarios |
|---|---|---|---|
| Pass-by-value | fn foo(s: String) |
Ownership is transferred to the function | The function needs to consume the value |
| Pass by immutable reference | fn foo(s: &String) |
Borrow, ownership remains unchanged | Read-only |
| Pass by reference | fn foo(s: &mut String) |
Borrow, ownership remains unchanged | Data needs to be modified |
(4) Comparison of Return Values
| Method | Syntax | Ownership Change | Example |
|---|---|---|---|
| Expression Returns | No Semicolon at the End of a Line | Transfer of Ownership | fn f() -> String { s } |
| Return | return value; |
Transfer Ownership | if err { return None; } |
| Multiple Tuple Returns | -> (T1, T2) |
Multiple Ownership Transfers | fn f() -> (i32, bool) |
| Output Parameter | &mut T |
No ownership transfer | fn fill(buf: &mut Vec<i32>) |
4. Scope and Shading
(1) Block Scope
Scope in Rust is defined by {}. Each block has its own scope:
fn main() {
let x = 10; // Outer Scope
{
let y = 20; // Inner Scope,y Available only here
println!("Inside: x={}, y={}", x, y); // ✅ Can access the outer layer x
}
// println!("{}", y); // ❌ Compilation Error:y Not in this scope
println!("External: x={}", x); // ✅
}
(2) Variable Shadowing
Outer variables can be shadowed by inner variables with the same name, and the inner code can only access the inner variables:
fn main() {
let x = 1; // Outer layer x = 1
{
let x = 2; // Inner layer x Covered the outer layer x
println!("Inside x = {}", x); // Output: Inside x = 2
}
println!("External x = {}", x); // Output: External x = 1
}
(3) Scope of Function Parameters
Function parameters are visible throughout the entire function body:
fn foo(x: i32) {
// x Available throughout the entire function body
let y = x + 1;
println!("x={}, y={}", x, y);
} // End of function, x and y are both destroyed
5. Function Examples
▶ Example 1: Various Function Signatures (Difficulty ⭐)
Output:
-------------------
5 the square of: <square(5)>
Area 10x20: <area(10, 20)>
min=<min>, max=<max>
// ============================================
// Functions with Different Combinations of Parameters and Return Types
// ============================================
// No parameters, no return value
fn print_separator() {
println!("-------------------");
}
// Single-parameter functions with return values
fn square(x: i32) -> i32 {
x * x // No semicolons,Expression returns
}
// Multi-parameter
fn area(width: u32, height: u32) -> u32 {
width * height
}
// Return multiple values(Tuple)
fn min_max(a: i32, b: i32) -> (i32, i32) {
if a < b {
(a, b) // Tuples as Return Values
} else {
(b, a)
}
}
fn main() {
print_separator();
println!("5 the square of: {}", square(5));
println!("Area 10x20: {}", area(10, 20));
let (min, max) = min_max(100, 50);
println!("min={}, max={}", min, max);
}
Output:
-------------------
5 the square of: 25
Area 10x20: 200
min=50, max=100
min_maxReturns a tuple; when called, it is destructured and assigned usinglet (min, max). This is the standard way to "return multiple values" in Rust.
▶ Example 2: Early Return and Guard Mode (Difficulty ⭐⭐)
Output:
Error:The divisor cannot be 0
10 / 3 = <safe_divide(10.0, 3.0)>
10 / 0 = <safe_divide(10.0, 0.0)>
Fractions 85: <get_grade(85)>
Fractions -5: <get_grade(-5)>
// ============================================
// Return Early:Inspection Criteria,If you're not satisfied, leave.
// ============================================
fn safe_divide(a: f64, b: f64) -> f64 {
if b == 0.0 {
println!("Error:The divisor cannot be 0");
return f64::NAN; // Return Early NaN(Not a Number)
}
a / b // Under normal circumstances,Expression returns
}
fn get_grade(score: i32) -> &'static str {
if score < 0 || score > 100 {
return "Invalid Fractions";
}
if score >= 90 { return "A"; }
if score >= 80 { return "B"; }
if score >= 70 { return "C"; }
if score >= 60 { return "D"; }
"F"
}
fn main() {
println!("10 / 3 = {}", safe_divide(10.0, 3.0));
println!("10 / 0 = {}", safe_divide(10.0, 0.0));
println!("Fractions 85: {}", get_grade(85));
println!("Fractions -5: {}", get_grade(-5));
}
Output:
10 / 3 = 3.3333333333333335
Error:The divisor cannot be 0
10 / 0 = NaN
Fractions 85: B
Fractions -5: Invalid Fractions
Early return (
return) is commonly used in the "check first, then execute" pattern. By handling error conditions and edge cases at the very beginning, the subsequent logic proceeds smoothly within a "safe zone."
▶ Example 3: Block Expressions and Scope Testing (Difficulty ⭐⭐)
Output:
Accessible internally: 42
After masking outer: 42
The value returned by the block: <inner_result>
External outer: 42
Nested variables
// ============================================
// Scope and Return Values of Block Expressions
// ============================================
fn main() {
let outer = "Outer-scope variables".to_string();
let inner_result = {
let inner = "Nested variables".to_string();
println!("Accessible internally: {}", outer); // ✅ The inner layer can access the outer layer
// Shading Variables with the Same Name
let outer = 42;
println!("After masking outer: {}", outer); // 42,Not a string
inner.len() // Block expression return values:inner length
}; // inner Destroyed here
println!("The value returned by the block: {}", inner_result);
println!("External outer: {}", outer); // ✅ Outer layer outer Still
// println!("{}", inner); // ❌ Compilation Error:inner Not in this scope
}
Output:
10 + 5 = <result1>
10 - 5 = <result2>
10 * 5 = <result3>
double(double(3)) = <result4>
increment(increment(5)) = <result5>
Alice After the bonus points are added: <bonus(alice_points)>
Block scope rules: Inner blocks can access outer variables, but outer blocks cannot access inner variables. Shading merely temporarily "masks" outer variables; once you exit the inner block, the outer variables return to normal.
▶ Example 4: Function Pointers and Higher-Order Functions (Difficulty ⭐⭐⭐)
Output:
10 + 5 = <result1>
10 - 5 = <result2>
10 * 5 = <result3>
double(double(3)) = <result4>
increment(increment(5)) = <result5>
Alice After the bonus points are added: <bonus(alice_points)>
// ============================================
// Passing Functions as Arguments——An Introduction to Higher-Order Functions
// ============================================
type MathOp = fn(i32, i32) -> i32;
fn add(a: i32, b: i32) -> i32 { a + b }
fn sub(a: i32, b: i32) -> i32 { a - b }
fn mul(a: i32, b: i32) -> i32 { a * b }
fn calculate(op: MathOp, a: i32, b: i32) -> i32 {
op(a, b)
}
fn apply_twice(f: fn(i32) -> i32, x: i32) -> i32 {
f(f(x))
}
fn main() {
let result1 = calculate(add, 10, 5);
let result2 = calculate(sub, 10, 5);
let result3 = calculate(mul, 10, 5);
println!("10 + 5 = {}", result1);
println!("10 - 5 = {}", result2);
println!("10 * 5 = {}", result3);
let double = |x: i32| x * 2;
let result4 = apply_twice(double, 3);
println!("double(double(3)) = {}", result4);
let increment = |x: i32| x + 1;
let result5 = apply_twice(increment, 5);
println!("increment(increment(5)) = {}", result5);
let alice_points = 80;
let bonus = |base: i32| -> i32 { base + 10 };
println!("Alice After the bonus points are added: {}", bonus(alice_points));
}
Output:
10 + 5 = 15
10 - 5 = 5
10 * 5 = 50
double(double(3)) = 12
increment(increment(5)) = 7
Alice After the bonus points are added: 90
Function pointers (of type
fn) and closures (of type|x| x * 2) can both be passed as arguments to higher-order functions. Function pointers point to code addresses determined at compile time, while closures can capture environment variables.
▶ Example 5: Comprehensive Exercise—Processing Student Grades (Difficulty ⭐⭐⭐)
Output:
Class Average Score: Outer layer avg
<name>: <score> pts, Level <grade>, <status>
Number of people who passed: <passing>/3
=== Report Card ===
--- Scope Demonstration ---
<result>
Outer-scope variables: Outer layer avg
// ============================================
// Comprehensive Example:Function,Combining Scope and Shading
// ============================================
fn average(scores: &[i32]) -> f64 {
if scores.is_empty() {
return 0.0;
}
let sum: i32 = scores.iter().sum();
sum as f64 / scores.len() as f64
}
fn classify(score: i32) -> &'static str {
match score {
90..=100 => "A",
80..=89 => "B",
70..=79 => "C",
60..=69 => "D",
_ => "F",
}
}
fn report(names: &[&str], scores: &[i32]) {
let avg = average(scores);
println!("Class Average Score: {:.1}", avg);
for (i, name) in names.iter().enumerate() {
let score = scores[i];
let grade = classify(score);
let status = if score >= 60 { "Through" } else { "Failed" };
println!("{}: {} pts, Level {}, {}", name, score, grade, status);
}
let passing = scores.iter().filter(|&&s| s >= 60).count();
println!("Number of people who passed: {}/{}", passing, scores.len());
}
fn main() {
let names = ["Alice", "Bob", "Charlie", "David"];
let scores = [95, 72, 58, 88];
println!("=== Report Card ===");
report(&names, &scores);
println!("\n--- Scope Demonstration ---");
let result = {
let scores = [100, 90, 80];
let avg = average(&scores);
format!("Average of the top three: {:.1}", avg)
};
println!("{}", result);
let avg = "Outer layer avg";
println!("Outer-scope variables: {}", avg);
}
Output:
=== Report Card ===
Class Average Score: 78.2
Alice: 95 pts, Level A, Through
Bob: 72 pts, Level C, Through
Charlie: 58 pts, Level F, Failed
David: 88 pts, Level B, Through
Number of people who passed: 3/4
--- Scope Demonstration ---
Average of the top three: 90.0
Outer-scope variables: Outer layer avg
This example combines the use of function definitions, slice parameters,
matchclassification, iterator methods, and block scope shadowing. Using the&[T]slice as a function parameter offers the greatest flexibility, and shadowing a variable with the same name within a block scope does not affect the outer scope.
❓ FAQ
return and an expression return?return is used for early returns, while an expression return is used at the end of a function.📖 Summary
- Functions are defined using the
fnkeyword, and parameters must be typed. - If the last expression in a function body does not end with a semicolon, it serves as the return value.
returnKeyword used for early return{}Blocks define scope; inner blocks can access outer variables, but not vice versa.- Variable shadowing allows inner scopes to declare variables with the same name, temporarily overriding variables in outer scopes.
- Function parameters are visible throughout the entire function body
📝 Exercises
- Difficulty ⭐: Write a function
fn is_even(n: i32) -> boolthat determines whether an integer is even, and then writemainto call it and print the parity of each number from 1 to 10. - Difficulty ⭐⭐: Write a function
fn max_of_three(a: i32, b: i32, c: i32) -> i32that returns the maximum of three numbers. (You may not use the standard library's.max(); write your own comparison logic.) - Difficulty ⭐⭐⭐: Write a function
fn calculator(op: char, a: f64, b: f64) -> f64that performs the corresponding operation based on the parametersop('+''-''*''/'), and returnsf64::NANwhen the divisor is 0. Test these four operations inmain.