Rustの関数とスコープ:定義、引数、戻り値、および変数のシャドウイング
Rustでは関数は第一級オブジェクトであり、その引数型と戻り値型がコンパイラの最初の防衛線として機能します。
関数は、コードを構成する基本的な要素です。Rustの関数構文はC系言語のそれと似ていますが、引数のパターンマッチングや式の戻り値に関しては、独自のスタイルを持っています。
1. 学習内容
fnを使用して関数を定義し、パラメータ型と戻り値型を指定します- パラメータの渡し方:値渡し、参照渡し、および可変参照の3つの方法
- 関数の戻り値と早期終了
- ブロックスコープと変数の色分け
- コード構成の基本単位として機能する
- 関数ポインタと高階関数の入門
2. あるコーヒーショップの物語
(1) 課題:コーヒーの淹れ方に決まった手順はない
リサはコーヒーショップを開店し、商売はますます好調になってきているが、一方で問題も生じてきた:
- バリスタによって、ラテの作り方はそれぞれ異なります
- まず牛乳を入れてからコーヒーを入れる人もいれば、まずコーヒーを入れてから牛乳を入れる人もいる。
- ある客が、「同じラテなのに、飲むたびに味が違う」と不満を述べた。
- リサは、関数のように標準化されたレシピを作りたいと考えています。つまり、牛乳とコーヒーの分量を入力すれば、常に同じ味わいのラテが出来上がるようなものです。
「コーヒーを淹れることが、関数を呼び出すのと同じくらい一貫性があれば素晴らしいだろう。つまり、同じ入力に対して常に同じ出力が得られるように。」
(2) Rustの関数に対するアプローチ
// 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);
}
関数はコーヒーのレシピのようなものです。引数は材料(ミルクの量、コーヒーの量)であり、戻り値は出来上がったコーヒーです。同じ入力であれば、必ず同じ出力が得られます。これが関数の決定性です。
3. 関数の定義
(1) 基本的な構文
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) パラメータと戻り値のさまざまな形式
| フォーム | 構文 | 例 |
|---|---|---|
| 引数なし、戻り値なし | fn foo() |
fn greet() { println!("hi"); } |
| 引数を受け取り、何も返さない | fn foo(x: i32) |
fn show(n: i32) { println!("{}", n); } |
| 引数と戻り値あり | fn foo(x: i32) -> i32 |
fn double(x: i32) -> i32 { x * 2 } |
| 複数の戻り値(タプル) | fn foo() -> (i32, bool) |
fn stats() -> (i32, bool) { (42, true) } |
| 戻る | return value; |
if x < 0 { return 0; } |
基本ルール:関数本体の最後の式が戻り値となります(セミコロンは不要)。早期リターンを行うには、
returnキーワードを使用してください。
(3) パラメータの渡し方
| 引数渡し方法 | 構文 | 所有権の変更 | 適用可能なシナリオ |
|---|---|---|---|
| 値渡し | fn foo(s: String) |
所有権は関数に移転される | 関数はその値を消費する必要がある |
| 不変参照による渡し | fn foo(s: &String) |
借用、所有権は変わらない | 読み取り専用 |
| 参照渡し | fn foo(s: &mut String) |
データを借用するが、所有権は変わらない | データを変更する必要がある |
(4) 戻り値の比較
| メソッド | 構文 | 所有権の変更 | 例 |
|---|---|---|---|
| 式の戻り値 | 行末にセミコロンがない | 所有権の移転 | fn f() -> String { s } |
| 戻る | return value; |
所有権の移転 | if err { return None; } |
| 複数のタプルの返却 | -> (T1, T2) |
複数の所有権移転 | fn f() -> (i32, bool) |
| 出力パラメータ | &mut T |
所有権の移転なし | fn fill(buf: &mut Vec<i32>) |
4. 範囲と陰影
(1) ブロックのスコープ
Rustにおけるスコープは{}によって定義されています。各ブロックには独自のスコープがあります:
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) 変数のシャドウイング
外部変数は、同じ名前の内部変数によってシャドウイングされる可能性があり、内部のコードからは内部変数にしかアクセスできません:
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) 関数パラメータの範囲
関数のパラメータは、関数本体全体で参照可能です:
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. 関数の例
(1) ▶ サンプル:さまざまな関数のシグネチャ (難易度 ⭐)
// ============================================
// 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);
}
出力:
-------------------
5 the square of: 25
Area 10x20: 200
min=50, max=100
min_maxはタプルを返します。呼び出されると、そのタプルは分解され、let (min, max)を使用して代入されます。これが Rust における「複数の値を返す」ための標準的な方法です。
(2) ▶ サンプル:早期帰還とガードモード(難易度 ⭐⭐)
// ============================================
// 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));
}
出力:
10 / 3 = 3.3333333333333335
Error:The divisor cannot be 0
10 / 0 = NaN
Fractions 85: B
Fractions -5: Invalid Fractions
早期リターン(
return)は、「まず確認してから実行する」というパターンでよく使われます。エラーやエッジケースを最初の段階で処理することで、その後のロジックは「セーフゾーン」内でスムーズに進行します。
(3) ▶ サンプル:ブロック式とスコープの検証(難易度 ⭐⭐)
// ============================================
// 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
}
出力:
Accessible internally: Outer-scope variables
After masking outer: 42
The value returned by the block: 8
External outer: Outer-scope variables
ブロックスコープのルール:内側のブロックからは外側の変数にアクセスできますが、外側のブロックからは内側の変数にアクセスできません。シェーディングは、外側の変数を一時的に「マスク」するだけです。内側のブロックを抜けると、外側の変数は元の状態に戻ります。
(4) ▶ サンプル:関数ポインタと高階関数(難易度 ⭐⭐⭐)
// ============================================
// 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));
}
出力:
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
関数ポインタ(型
fn)とクロージャ(型|x| x * 2)は、いずれも高階関数の引数として渡すことができます。関数ポインタはコンパイル時に決定されるコードのアドレスを指すのに対し、クロージャは環境変数をキャプチャすることができます。
(5) ▶ サンプル:総合演習—学生の成績処理(難易度 ⭐⭐⭐)
// ============================================
// 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);
}
出力:
=== 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
この例では、関数定義、スライスパラメータ、
matchの分類、イテレータメソッド、およびブロックスコープでの変数のシャドウイングを組み合わせています。&[T]スライスを関数パラメータとして使用することで最大の柔軟性が得られ、ブロックスコープ内で同名の変数をシャドウイングしても、外側のスコープには影響しません。
❓ よくある質問
returnと式による戻り値の違いは何ですか?returnは早期リターンに使用され、式による戻り値は関数の末尾で使用されます。📖 まとめ
- 関数は
fnキーワードを使用して定義され、パラメータには型を指定する必要があります。 - 関数本体内の最後の式がセミコロンで終わっていない場合、その式が戻り値となります。
return早期返却に使用されるキーワード{}ブロックはスコープを定義します。内側のブロックからは外側の変数にアクセスできますが、その逆はできません。- 変数のシャドウイング により、内側のスコープで同じ名前の変数を宣言することができ、外側のスコープにある変数が一時的に上書きされます。
- 関数のパラメータは、関数本体全体で参照可能です
📝 練習問題
- 難易度 ⭐:整数が偶数かどうかを判定する関数
fn is_even(n: i32) -> boolを記述し、次にmainを記述してその関数を呼び出し、1 から 10 までの各数の偶奇を出力してください。 - 難易度 ⭐⭐: 3つの数値のうち最大の値を返す関数
fn max_of_three(a: i32, b: i32, c: i32) -> i32を作成してください。(標準ライブラリの.max()は使用できません。独自の比較ロジックを実装してください。) - 難易度 ⭐⭐⭐: パラメータ
op('+''-''*''/')に基づいて対応する演算を実行し、除数が 0 の場合はf64::NANを返す関数fn calculator(op: char, a: f64, b: f64) -> f64を作成してください。これら 4 つの演算をmainでテストしてください。



