再帰メソッドの戻り値をIEnumerable<T>
型にしたい場合ってありますよね。
この記事はC#で再帰メソッドの戻り値をIEnumerable<T>
型にする時にどう書くかを簡単に説明したものです。
普通の再帰メソッドを書いてみる
まずは、FizzBuzzプログラムの再帰版を書いてみましょう。まあ通常FizzBuzzで再帰メソッド定義することはないと思いますが、ちょうど良い再帰メソッドを思いつかなかったので許してください。
using System;
FizzBuzz(20);
void FizzBuzz(int n)
{
_FizzBuzz(1, n);
void _FizzBuzz(int i, int last)
{
if (i > last)
return;
string s = null;
if (i % 3 == 0)
s += "Fizz";
if (i % 5 == 0)
s += "Buzz";
Console.WriteLine(s ?? i.ToString());
_FizzBuzz(i + 1, last);
}
}
ローカル関数_FizzBuzzが再帰メソッドです。
このプログラムを実行すると、
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
(以下省略)
と表示されます。
モデルとビューを分離したい
では、FizzBuzzを求めるロジックと、求めた文字列を出力するロジックを分離したい場合はどうでしょうか。
これを解決する方法のひとつが、イベントやdelegateを使う方法です。
文字列が求まるたびに、イベントを発行すれば、FizzBuzzメソッドの利用者が FizzBuzzメソッドに手を入れることなく、自由に独自の処理を書くことができます。
ただ、この方法にも欠点があります。
例えば、GUIのプログラムで、次へボタンを押すたびに、文字列をひとつずつ表示することを考えて見ましょう。こういった要求には、イベント発行する方法は上手く対応することができません。
再帰メソッドの戻り値をIEnumerable<T>
にする
解決策のひとつが、FizzBuzzメソッドの戻り値をIEnumerable<T>
にする方法です。
全ての結果を List<string>
などに溜め込む方法でも、FizzBuzzメソッドの戻り値をIEnumerable<T>
に出来ますが、 全てを列挙し終わらないと、FizzBuzzメソッドから制御を戻せませんので、与える数値が大きい場合は、最初のひとつを取り出すのにも多くの時間を要してしまい好ましくありません。
そのため、yield return
使って反復子を定義すれば良いのですが、再帰メソッドの場合はちょっとした工夫が必要です。
以下にそのコードを示します。
using System;
using System.Collections.Generic;
foreach (var s in FizzBuzz(20))
{
Console.WriteLine(s);
}
IEnumerable<string> FizzBuzz(int n)
{
return _FizzBuzz(1, n);
IEnumerable<string> _FizzBuzz(int i, int n)
{
if (i > n)
yield break;
string s = null;
if (i % 3 == 0)
s += "Fizz";
if (i % 5 == 0)
s += "Buzz";
s = s ?? i.ToString();
yield return s;
var results = _FizzBuzz(i + 1, n); // ★
foreach (var item in results)
{
yield return item;
}
}
}
注目すべき点は、再帰メソッドを呼び出して返ってきた戻り値の扱いです(★印)。
return results;
とは書けません。foreachで一つ一つ要素を取り出し、yield return;
しています。こうすることで、再帰メソッドで、戻り値をIEnumerable<T>
にすることができます。
おまけ
GUIのプログラムで、次へボタンを押すたびに、文字列をひとつずつ表示することを考えて見ましょう。
もしかしたら、本当に戻り値をIEnumerable<T>
にすることでこの要求に応えられるのか疑問に思っている方がいるかもしれないので、以下のコードも載せておきます。
GUIのコードではないですが、これを理解できればGUIのコードにするのも難しくないはずです。
enterキーを押すたびに、FizzBuzzメソッドから得られる文字列をひとつずつ出力しています。
var ite = FizzBuzz(20).GetEnumerator();
while (true)
{
Console.ReadLine();
if (!ite.MoveNext())
break;
var s = ite.Current;
Console.WriteLine(s);
}