メソッドが自分自身を呼び出す書き方を再帰といいます。再帰を理解するには、呼び出しのたびに情報が積まれるコールスタックも合わせて見る必要があります。
再帰は、メソッドが自分自身を呼び出すことです。
書式:再帰呼び出し
1
2
3
4
5
6
7
8
9
10
戻り値の型 メソッド名(型 引数名)
{
if (終了条件)
{
return;
}
処理
メソッド名(次の値);
}
| 要素 | 説明 |
|---|---|
終了条件 |
再帰を止める条件 |
処理 |
その段階で実行したい内容 |
次の値 |
次の呼び出しに渡す値 |
再帰では、必ず終了条件が必要です。終了条件がないと、自分自身を呼び出し続けて止まりません。
終了条件がない再帰は、呼び出しを無限に続けます。するとコールスタックがあふれ、最終的に StackOverflowException になります。
このため、再帰を書くときは最初に「どこで止めるか」を決めます。終了条件は先頭で確認するのが基本です。
メソッドを呼び出すたびに、コールスタックにスタックフレームが積まれます。スタックフレームには、戻り先やローカル変数など、その呼び出しに必要な情報が入ります。
return すると、そのフレームが解放される再帰では同じメソッド名でも、各呼び出しは別々のフレームとして管理されます。
1
2
3
4
5
6
7
8
9
10
11
12
class A
{
public void M(int n)
{
if (n <= 0) { Console.WriteLine("A.M: done"); return; }
Console.WriteLine($"A.M: n={n}");
M(n - 1);
}
}
var a = new A();
a.M(3);
1
2
3
4
A.M: n=3
A.M: n=2
A.M: n=1
A.M: done
この例では、n を 1 ずつ減らしながら自分自身を呼び出しています。n <= 0 になった時点で終了します。
1
2
3
4
5
6
M(3) 実行中
└─ M(2) 実行中
└─ M(1) 実行中
└─ M(0): return → フレーム解放
return → フレーム解放
return → フレーム解放
M(3) が終わる前に M(2)、その前に M(1)、さらに M(0) が呼ばれます。M(0) が return すると、積まれた順とは逆順にフレームが解放されます。
同じ処理は、再帰ではなくループでも書けます。
1
2
3
4
5
for (int n = 3; n > 0; n--)
{
Console.WriteLine($"A.M: n={n}");
}
Console.WriteLine("A.M: done");
1
2
3
4
A.M: n=3
A.M: n=2
A.M: n=1
A.M: done
再帰とループは、どちらでも書ける場合があります。重要なのは「終了条件があること」と「どの順番で処理が進むか」を正しく追えることです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A
{
// ❌ NG: 止まる条件がないので無限再帰になる
public void M(int n)
{
Console.WriteLine($"A.M: n={n}");
M(n - 1);
}
// ✅ OK: 先頭で終了条件を確認する
public void N(int n)
{
if (n <= 0) { Console.WriteLine("A.N: done"); return; }
Console.WriteLine($"A.N: n={n}");
N(n - 1);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A
{
// ❌ NG: 先に再帰呼び出ししてから条件を確認すると流れを追いにくい
public void M(int n)
{
Console.WriteLine($"A.M: n={n}");
if (n <= 0) { return; }
M(n - 1);
}
// ✅ OK: 最初に終了条件を確認する
public void N(int n)
{
if (n <= 0) { Console.WriteLine("A.N: done"); return; }
Console.WriteLine($"A.N: n={n}");
N(n - 1);
}
}
StackOverflowException になるreturn するとフレームが解放される以下の問いに答えられるか確認しましょう。
次のコードの出力結果は何になりますか?
1
2
3
4
5
6
7
8
9
10
11
12
class A
{
public void M(int n)
{
if (n <= 1) { Console.WriteLine("A.M: done"); return; }
Console.WriteLine($"A.M: n={n}");
M(n - 1);
}
}
var a = new A();
a.M(3);
A.M(int n) が n を表示しながら 0 まで減らし、最後に "A.M: done" を表示する再帰メソッドを書いてください。1
2
3
A.M: n=3
A.M: n=2
A.M: done
1
2
3
4
5
6
7
8
9
class A
{
public void M(int n)
{
if (n <= 0) { Console.WriteLine("A.M: done"); return; }
Console.WriteLine($"A.M: n={n}");
M(n - 1);
}
}
static メンバーと static クラス では、インスタンスに属するメンバーとクラスに属するメンバーの違いを学びます。