C#の再帰処理を初心者向けに解説|仕組み・書き方・無限ループを防ぐ実践例

はじめに

C#でプログラムを書いていると、「再帰処理」という言葉を目にすることがあります。再帰処理は、メソッドの中から同じメソッドを呼び出す処理のことです。最初は少し難しく感じるかもしれませんが、仕組みを理解すると、フォルダ検索やツリー構造の処理、階層データの読み取りなどでとても役立ちます。

この記事では、C#の再帰処理について、初心者にもわかりやすいように基本から解説します。再帰処理の書き方、終了条件の考え方、無限ループを防ぐ方法、実践的なコード例まで順番に紹介します。

1. C#の再帰処理とは?初心者向けに基本を解説

1-1. 再帰処理とは「自分自身を呼び出す処理」

再帰処理とは、メソッドの中で自分自身を呼び出す処理のことです。

たとえば、次のようなメソッドは再帰処理です。

C#
void Hello()
{
Console.WriteLine("こんにちは");
Hello();
}

このコードでは、Helloメソッドの中で再びHelloメソッドを呼び出しています。つまり、自分自身を呼び出しているため、再帰処理になります。

ただし、このコードには問題があります。終了する条件がないため、永遠にHelloメソッドが呼び出され続けてしまいます。再帰処理では、必ず「どこで処理を止めるか」を決める必要があります。

1-2. C#で再帰処理が使われる代表的な場面

C#の再帰処理は、同じような構造が繰り返し現れるデータを扱うときに便利です。

代表的な場面には、次のようなものがあります。

C#
// フォルダの中にあるファイルをすべて調べる
// 親フォルダの中に子フォルダがあり、さらにその中にもフォルダがある

フォルダ構造のように、「中に同じ種類のものが入っている」構造は、再帰処理と相性がよいです。

ほかにも、ツリー構造、メニュー構造、親子関係を持つデータ、JSONやXMLのような階層データでも再帰処理がよく使われます。

1-3. ループ処理との違い

C#で繰り返し処理を書く方法としては、for文やwhile文などのループ処理があります。

たとえば、1から5まで表示する場合は、ループで次のように書けます。

C#
for (int i = 1; i <= 5; i++)
{
Console.WriteLine(i);
}

一方、再帰処理で書くと次のようになります。

C#
void PrintNumber(int number)
{
if (number > 5)
{
return;
}

Console.WriteLine(number);
PrintNumber(number + 1);
}

PrintNumber(1);

ループ処理は「同じ処理を決まった回数繰り返す」ときに向いています。再帰処理は「同じ構造をたどる」ときに向いています。

1-4. 初心者が再帰処理でつまずきやすいポイント

初心者がC#の再帰処理でつまずきやすいポイントは、主に次の3つです。

1つ目は、終了条件を書き忘れることです。終了条件がないと、メソッドが無限に呼び出され続けます。

2つ目は、再帰呼び出しのたびに値が変化していないことです。値が変わらなければ、いつまでも終了条件に近づきません。

3つ目は、処理の流れが見えにくいことです。再帰処理では、メソッドが何度も呼び出されるため、どの順番で処理が進んでいるのかがわかりにくくなることがあります。

2. C#の再帰処理の仕組み

2-1. メソッドが自分自身を呼び出す流れ

C#の再帰処理では、メソッドの中で同じメソッドを呼び出します。

次の例を見てみましょう。

C#
void CountDown(int number)
{
if (number <= 0)
{
Console.WriteLine("終了");
return;
}

Console.WriteLine(number);
CountDown(number - 1);
}

CountDown(3);

このコードを実行すると、次のように表示されます。

C#
3
2
1
終了

処理の流れは次のようになります。

C#
CountDown(3)
CountDown(2)
CountDown(1)
CountDown(0)

numberの値が1ずつ小さくなり、最後に0になった時点で終了します。

2-2. 再帰処理に必要な「終了条件」

再帰処理で最も重要なのが終了条件です。終了条件とは、再帰呼び出しを止めるための条件です。

先ほどの例では、次の部分が終了条件です。

C#
if (number <= 0)
{
Console.WriteLine("終了");
return;
}

この条件があることで、numberが0以下になったときに処理が止まります。

終了条件がない場合、C#のメソッドは自分自身を何度も呼び出し続け、最終的にStackOverflowExceptionが発生する可能性があります。

2-3. 呼び出しごとに処理が積み重なる仕組み

再帰処理では、メソッドが呼び出されるたびに、その処理の情報がメモリ上に積み重なります。

たとえば、次のような階乗の処理を考えます。

C#
int Factorial(int n)
{
if (n <= 1)
{
return 1;
}

return n * Factorial(n - 1);
}

Console.WriteLine(Factorial(4));

Factorial(4)を実行すると、内部では次のように呼び出されます。

C#
Factorial(4)
4 * Factorial(3)
4 * 3 * Factorial(2)
4 * 3 * 2 * Factorial(1)

Factorial(1)が1を返すと、そこから順番に計算結果が戻っていきます。

C#
Factorial(1) = 1
Factorial(2) = 2 * 1
Factorial(3) = 3 * 2
Factorial(4) = 4 * 6

最終的な結果は24です。

2-4. スタックとStackOverflowExceptionの関係

C#では、メソッドが呼び出されるたびに、呼び出し情報がスタックという領域に積まれます。

再帰処理ではメソッドが何度も呼び出されるため、再帰回数が多くなりすぎるとスタックの領域を使い切ってしまいます。その結果、StackOverflowExceptionが発生します。

たとえば、次のコードは危険です。

C#
void Infinite()
{
Infinite();
}

Infinite();

このコードには終了条件がありません。そのため、メソッドが無限に呼び出され、最終的にスタックがいっぱいになります。

再帰処理を書くときは、終了条件と再帰回数を必ず意識しましょう。

3. C#で再帰処理を書く基本構文

3-1. 再帰メソッドの基本形

C#の再帰メソッドは、基本的に次の形で書きます。

C#
戻り値の型 メソッド名(引数)
{
if (終了条件)
{
return 結果;
}

return メソッド名(変更した引数);
}

ポイントは、終了条件と再帰呼び出しの2つです。

終了条件では、処理を止める条件を書きます。再帰呼び出しでは、引数の値を変化させて、終了条件に近づけます。

3-2. 引数を使って処理を進める書き方

再帰処理では、引数を使って現在の状態を管理することが多いです。

C#
void PrintNumbers(int current, int max)
{
if (current > max)
{
return;
}

Console.WriteLine(current);
PrintNumbers(current + 1, max);
}

PrintNumbers(1, 5);

このコードでは、currentが現在の数値、maxが最大値です。

PrintNumbers(current + 1, max)のように、再帰呼び出しのたびにcurrentを1増やしています。これにより、いつかcurrent > maxとなり、処理が終了します。

3-3. 戻り値を使う再帰処理の書き方

戻り値を使う再帰処理では、再帰呼び出しの結果を使って計算します。

C#
int Sum(int n)
{
if (n <= 0)
{
return 0;
}

return n + Sum(n - 1);
}

Console.WriteLine(Sum(5));

このコードでは、1から5までの合計を求めています。

処理の中身は次のようになります。

C#
Sum(5) = 5 + Sum(4)
Sum(4) = 4 + Sum(3)
Sum(3) = 3 + Sum(2)
Sum(2) = 2 + Sum(1)
Sum(1) = 1 + Sum(0)
Sum(0) = 0

結果は15になります。

3-4. voidメソッドで再帰処理を書く場合

戻り値がないvoidメソッドでも再帰処理を書くことができます。

C#
void PrintList(List<string> names, int index)
{
if (index >= names.Count)
{
return;
}

Console.WriteLine(names[index]);
PrintList(names, index + 1);
}

var names = new List<string> { "田中", "佐藤", "鈴木" };
PrintList(names, 0);

この例では、リストの要素を先頭から順番に表示しています。

indexがリストの件数以上になったら終了します。

4. C#の再帰処理の基本例

4-1. カウントダウンを再帰処理で書く例

まずは、シンプルなカウントダウンの例です。

C#
void CountDown(int number)
{
if (number <= 0)
{
Console.WriteLine("スタート!");
return;
}

Console.WriteLine(number);
CountDown(number - 1);
}

CountDown(5);

実行結果は次のようになります。

C#
5
4
3
2
1
スタート!

numberを1ずつ減らし、0になったら終了します。

4-2. 階乗を再帰処理で計算する例

階乗とは、1から指定した数までをすべて掛け合わせた値です。

たとえば、5の階乗は次のようになります。

C#
5 * 4 * 3 * 2 * 1 = 120

C#の再帰処理で書くと、次のようになります。

C#
int Factorial(int n)
{
if (n <= 1)
{
return 1;
}

return n * Factorial(n - 1);
}

Console.WriteLine(Factorial(5));

実行結果は次のとおりです。

C#
120

Factorial(5)は、内部でFactorial(4)を呼び出し、その結果に5を掛けます。

4-3. フィボナッチ数列を再帰処理で求める例

フィボナッチ数列は、前の2つの数を足して次の数を作る数列です。

C#
0, 1, 1, 2, 3, 5, 8, 13 ...

C#の再帰処理で書くと、次のようになります。

C#
int Fibonacci(int n)
{
if (n == 0)
{
return 0;
}

if (n == 1)
{
return 1;
}

return Fibonacci(n - 1) + Fibonacci(n - 2);
}

Console.WriteLine(Fibonacci(6));

実行結果は次のとおりです。

C#
8

ただし、この書き方は同じ計算を何度も行うため、数値が大きくなると処理が遅くなります。実務では、メモ化やループ処理を使って改善することがあります。

4-4. 配列やリストの要素を再帰的に処理する例

配列の要素を再帰処理で合計する例です。

C#
int SumArray(int[] numbers, int index)
{
if (index >= numbers.Length)
{
return 0;
}

return numbers[index] + SumArray(numbers, index + 1);
}

int[] numbers = { 10, 20, 30, 40 };
Console.WriteLine(SumArray(numbers, 0));

実行結果は次のとおりです。

C#
100

indexを1ずつ増やしながら、配列の最後まで処理しています。

5. C#の再帰処理で無限ループを防ぐ方法

5-1. 終了条件を必ず設定する

C#の再帰処理で無限ループを防ぐためには、終了条件が必須です。

C#
void Print(int n)
{
if (n <= 0)
{
return;
}

Console.WriteLine(n);
Print(n - 1);
}

このコードでは、n <= 0が終了条件です。終了条件があることで、再帰呼び出しが止まります。

5-2. 再帰呼び出しごとに値を変化させる

終了条件を書いていても、引数の値が変化しなければ意味がありません。

次のコードは危険です。

C#
void Print(int n)
{
if (n <= 0)
{
return;
}

Console.WriteLine(n);
Print(n);
}

Print(n)としているため、nの値が変わりません。nが5なら、ずっと5のままです。

正しくは次のように書きます。

C#
void Print(int n)
{
if (n <= 0)
{
return;
}

Console.WriteLine(n);
Print(n - 1);
}

再帰呼び出しのたびにnを減らすことで、終了条件に近づきます。

5-3. 条件分岐の順番に注意する

再帰処理では、終了条件を先に書くのが基本です。

C#
void Process(int n)
{
if (n <= 0)
{
return;
}

Console.WriteLine(n);
Process(n - 1);
}

先に終了条件を確認することで、不要な処理や危険な処理を避けられます。

たとえば、配列やリストを扱う場合、終了条件より先に要素へアクセスすると、範囲外エラーになることがあります。

C#
void PrintArray(int[] numbers, int index)
{
if (index >= numbers.Length)
{
return;
}

Console.WriteLine(numbers[index]);
PrintArray(numbers, index + 1);
}

このように、配列にアクセスする前にindexの範囲を確認することが大切です。

5-4. 無限再帰が発生するNGコード例

次のコードは、終了条件がないため無限再帰になります。

C#
void BadMethod()
{
Console.WriteLine("処理中");
BadMethod();
}

次のコードも、引数が変化していないため危険です。

C#
void BadCount(int n)
{
if (n <= 0)
{
return;
}

Console.WriteLine(n);
BadCount(n);
}

終了条件があっても、nが変化しないため、n <= 0に近づきません。

修正するなら、次のようにします。

C#
void GoodCount(int n)
{
if (n <= 0)
{
return;
}

Console.WriteLine(n);
GoodCount(n - 1);
}

5-5. StackOverflowExceptionを防ぐ実践ポイント

StackOverflowExceptionを防ぐには、次の点を意識しましょう。

再帰処理には必ず終了条件を書くことが大切です。また、再帰呼び出しのたびに引数の値を変化させ、終了条件へ近づける必要があります。

さらに、処理対象の件数が非常に多い場合は、再帰処理ではなくループ処理を検討しましょう。特に、数万回以上の呼び出しが発生する可能性がある場合は注意が必要です。

6. C#の再帰処理を実践で使う例

6-1. フォルダ内のファイルを再帰的に検索する

再帰処理が実務でよく使われる例のひとつが、フォルダ内のファイル検索です。

C#
using System;
using System.IO;

void SearchFiles(string folderPath)
{
foreach (string file in Directory.GetFiles(folderPath))
{
Console.WriteLine(file);
}

foreach (string directory in Directory.GetDirectories(folderPath))
{
SearchFiles(directory);
}
}

SearchFiles(@"C:\Sample");

このコードでは、指定したフォルダ内のファイルを表示したあと、子フォルダに対して同じ処理を行います。

フォルダの中にフォルダがあり、その中にもさらにフォルダがある場合でも、再帰処理を使えば同じメソッドで処理できます。

実際には、アクセス権限がないフォルダで例外が発生する可能性があるため、必要に応じてtry-catchを使います。

C#
using System;
using System.IO;

void SearchFiles(string folderPath)
{
try
{
foreach (string file in Directory.GetFiles(folderPath))
{
Console.WriteLine(file);
}

foreach (string directory in Directory.GetDirectories(folderPath))
{
SearchFiles(directory);
}
}
catch (UnauthorizedAccessException)
{
Console.WriteLine($"アクセスできません: {folderPath}");
}
}

6-2. ツリー構造のデータを再帰的に処理する

ツリー構造とは、親と子の関係を持つデータ構造です。メニュー、カテゴリ、組織図などでよく使われます。

C#
class Node
{
public string Name { get; set; }
public List<Node> Children { get; set; } = new List<Node>();
}

このようなツリー構造を再帰処理で表示してみます。

C#
void PrintNode(Node node, int level)
{
Console.WriteLine(new string(' ', level * 2) + node.Name);

foreach (Node child in node.Children)
{
PrintNode(child, level + 1);
}
}

使い方は次のとおりです。

C#
var root = new Node { Name = "親" };
root.Children.Add(new Node { Name = "子1" });
root.Children.Add(new Node { Name = "子2" });

PrintNode(root, 0);

子要素に対して同じPrintNodeメソッドを呼び出すことで、階層構造を順番にたどれます。

6-3. 親子関係を持つデータをたどる

データベースや業務システムでは、親IDを持つデータを扱うことがあります。

C#
class Category
{
public int Id { get; set; }
public int? ParentId { get; set; }
public string Name { get; set; }
}

親子関係を持つカテゴリを再帰的に表示する例です。

C#
void PrintCategories(List<Category> categories, int? parentId, int level)
{
var children = categories.Where(c => c.ParentId == parentId);

foreach (var child in children)
{
Console.WriteLine(new string(' ', level * 2) + child.Name);
PrintCategories(categories, child.Id, level + 1);
}
}

使い方は次のようになります。

C#
var categories = new List<Category>
{
new Category { Id = 1, ParentId = null, Name = "商品" },
new Category { Id = 2, ParentId = 1, Name = "パソコン" },
new Category { Id = 3, ParentId = 1, Name = "スマートフォン" },
new Category { Id = 4, ParentId = 2, Name = "ノートPC" }
};

PrintCategories(categories, null, 0);

親カテゴリから子カテゴリへ、さらにその子カテゴリへと再帰的に処理できます。

6-4. JSONやXMLの階層データを処理する

JSONやXMLのような階層データも、再帰処理と相性がよいです。

たとえば、JSONにはオブジェクトの中にオブジェクトが入っていることがあります。

JSON
{
"name": "root",
"child": {
"name": "child1",
"child": {
"name": "child2"
}
}
}

このような階層構造では、「現在の要素を処理して、子要素があれば同じ処理を行う」という考え方が使えます。

C#では、JSONライブラリを使って要素をたどるときに再帰処理を利用することがあります。

C#
using System.Text.Json;

void PrintJson(JsonElement element, int level)
{
if (element.ValueKind == JsonValueKind.Object)
{
foreach (JsonProperty property in element.EnumerateObject())
{
Console.WriteLine(new string(' ', level * 2) + property.Name);
PrintJson(property.Value, level + 1);
}
}
else
{
Console.WriteLine(new string(' ', level * 2) + element.ToString());
}
}

階層の深さが事前にわからないデータでは、再帰処理が便利です。

7. 再帰処理とループ処理はどちらを使うべきか

7-1. 再帰処理が向いているケース

再帰処理が向いているのは、データ自体が再帰的な構造になっている場合です。

たとえば、フォルダ構造、ツリー構造、メニュー構造、親子関係、JSON、XMLなどです。

これらのデータは、「親の中に子があり、その子の中にもさらに子がある」という形になっています。このような構造では、再帰処理を使うと自然で読みやすいコードになることがあります。

7-2. ループ処理が向いているケース

ループ処理が向いているのは、単純に同じ処理を繰り返す場合です。

たとえば、1から100までの数値を表示する、配列の要素を順番に処理する、指定回数だけ処理を繰り返すといった場合です。

C#
for (int i = 1; i <= 100; i++)
{
Console.WriteLine(i);
}

このような単純な繰り返しでは、再帰処理よりもループ処理の方がわかりやすいです。

7-3. 可読性と処理速度の違い

再帰処理は、構造によってはコードを短く書けます。しかし、メソッド呼び出しが何度も発生するため、単純なループ処理よりも負荷が大きくなる場合があります。

一方で、ループ処理は処理の流れがわかりやすく、パフォーマンス面でも有利なことが多いです。

ただし、ツリー構造や階層データをループだけで処理しようとすると、コードが複雑になることがあります。そのような場合は、再帰処理を使った方が読みやすくなることもあります。

7-4. 初心者が判断するときの基準

初心者が再帰処理とループ処理で迷ったときは、まず次のように考えるとよいです。

処理対象が単純な繰り返しなら、ループ処理を使います。処理対象が階層構造や親子構造なら、再帰処理を検討します。

また、再帰処理を書くときは、終了条件を簡単に説明できるかを確認しましょう。終了条件がはっきりしない場合は、無限再帰になる危険があります。

8. C#の再帰処理でよくあるエラーと対処法

8-1. 終了条件を書き忘れる

再帰処理で最も多いミスは、終了条件を書き忘れることです。

C#
void Test()
{
Console.WriteLine("実行中");
Test();
}

このコードは止まりません。

対処法は、必ず終了条件を書くことです。

C#
void Test(int count)
{
if (count <= 0)
{
return;
}

Console.WriteLine("実行中");
Test(count - 1);
}

8-2. 引数の値が変化していない

終了条件を書いていても、引数が変化していなければ再帰処理は終わりません。

C#
void Test(int count)
{
if (count <= 0)
{
return;
}

Console.WriteLine(count);
Test(count);
}

このコードでは、countが変わっていません。

正しくは次のようにします。

C#
void Test(int count)
{
if (count <= 0)
{
return;
}

Console.WriteLine(count);
Test(count - 1);
}

8-3. 想定より再帰回数が多くなる

再帰処理では、想定よりも呼び出し回数が多くなることがあります。

特に、フィボナッチ数列のように、1回のメソッド内で複数回の再帰呼び出しを行う場合は注意が必要です。

C#
int Fibonacci(int n)
{
if (n <= 1)
{
return n;
}

return Fibonacci(n - 1) + Fibonacci(n - 2);
}

このコードはシンプルですが、nが大きくなると呼び出し回数が急激に増えます。

対策として、計算結果を保存するメモ化を使う方法があります。

C#
int Fibonacci(int n, Dictionary<int, int> memo)
{
if (memo.ContainsKey(n))
{
return memo[n];
}

if (n <= 1)
{
return n;
}

int result = Fibonacci(n - 1, memo) + Fibonacci(n - 2, memo);
memo[n] = result;

return result;
}

var memo = new Dictionary<int, int>();
Console.WriteLine(Fibonacci(10, memo));

8-4. デバッグで再帰処理の流れを確認する方法

再帰処理の流れがわからないときは、Console.WriteLineで現在の引数を表示すると理解しやすくなります。

C#
int Sum(int n)
{
Console.WriteLine($"Sum({n}) が呼ばれました");

if (n <= 0)
{
Console.WriteLine("終了条件に到達しました");
return 0;
}

int result = n + Sum(n - 1);
Console.WriteLine($"Sum({n}) の結果: {result}");

return result;
}

Console.WriteLine(Sum(3));

実行すると、どの順番でメソッドが呼び出され、どの順番で結果が戻ってくるかを確認できます。

Visual Studioを使っている場合は、ブレークポイントを設定して、ステップ実行で確認するのもおすすめです。

9. C#の再帰処理を理解するための練習問題

9-1. 数値を合計する再帰メソッド

1から指定した数値までを合計する再帰メソッドを作ってみましょう。

C#
int Sum(int n)
{
if (n <= 0)
{
return 0;
}

return n + Sum(n - 1);
}

Console.WriteLine(Sum(5));

実行結果は次のとおりです。

C#
15

Sum(5)は、5 + 4 + 3 + 2 + 1を計算しています。

9-2. 文字列を逆順にする再帰メソッド

文字列を再帰処理で逆順にする例です。

C#
string Reverse(string text)
{
if (text.Length <= 1)
{
return text;
}

return Reverse(text.Substring(1)) + text[0];
}

Console.WriteLine(Reverse("CSharp"));

実行結果は次のようになります。

C#
prahSC

先頭の文字を後ろに回しながら、残りの文字列に対して同じ処理を行っています。

9-3. リスト内の最大値を探す再帰メソッド

リストの中から最大値を探す再帰メソッドです。

C#
int FindMax(List<int> numbers, int index)
{
if (index == numbers.Count - 1)
{
return numbers[index];
}

int maxInRest = FindMax(numbers, index + 1);
return numbers[index] > maxInRest ? numbers[index] : maxInRest;
}

var numbers = new List<int> { 3, 8, 2, 10, 5 };
Console.WriteLine(FindMax(numbers, 0));

実行結果は次のとおりです。

C#
10

現在の値と、残りの要素の最大値を比較して、大きい方を返しています。

9-4. 練習問題を解くときの考え方

再帰処理の練習問題を解くときは、次の順番で考えるとわかりやすいです。

まず、処理をいつ止めるかを決めます。これが終了条件です。

次に、1回の処理で何をするかを決めます。たとえば、現在の値を表示する、現在の値を足す、現在の要素を比較するなどです。

最後に、次の再帰呼び出しで値をどう変化させるかを決めます。数値なら増やすか減らす、リストならインデックスを進める、ツリーなら子要素に移動する、という考え方です。

10. C#の再帰処理に関するよくある質問

10-1. 再帰処理は初心者でも使うべき?

初心者でも、再帰処理の基本は理解しておくべきです。

ただし、すべての繰り返し処理を再帰で書く必要はありません。単純な繰り返しは、for文やwhile文を使った方がわかりやすいことが多いです。

再帰処理は、フォルダ構造やツリー構造など、階層をたどる処理で使うと効果的です。

10-2. 再帰処理は処理速度が遅い?

再帰処理は、メソッド呼び出しを繰り返すため、単純なループ処理よりも遅くなる場合があります。

特に、フィボナッチ数列のように同じ計算を何度も行う再帰処理は、処理速度が遅くなりやすいです。

ただし、再帰処理の方がコードを理解しやすい場合もあります。処理速度が重要な場面では、ループ処理やメモ化を検討しましょう。

10-3. 再帰処理の深さに制限はある?

C#の再帰処理には、実質的な深さの制限があります。

再帰呼び出しが深くなりすぎると、スタック領域を使い切り、StackOverflowExceptionが発生します。

そのため、再帰回数が非常に多くなる可能性がある処理では、ループ処理に置き換えられないか検討することが大切です。

10-4. 実務で再帰処理は使われる?

実務でも再帰処理は使われます。

特に、フォルダ検索、カテゴリ管理、組織図、メニュー構造、ツリー構造、JSONやXMLの解析などで使われることがあります。

一方で、単純な繰り返し処理ではループ処理を使うことが多いです。再帰処理は、使うべき場面を選んで使うことが大切です。

まとめ

C#の再帰処理とは、メソッドの中で自分自身を呼び出す処理のことです。再帰処理を使うと、フォルダ構造やツリー構造、親子関係を持つデータ、JSONやXMLのような階層データを自然に処理できます。

再帰処理で最も重要なのは、終了条件を必ず書くことです。また、再帰呼び出しのたびに引数の値を変化させ、終了条件へ近づける必要があります。これを忘れると、無限再帰になり、StackOverflowExceptionが発生する可能性があります。

単純な繰り返し処理にはループ処理、階層構造をたどる処理には再帰処理が向いています。C#で再帰処理を使いこなすためには、まず小さなコードで動きを確認し、終了条件、引数の変化、戻り値の流れを意識して練習することが大切です。