この記事は C# Advent Calendar 2019 の11日目です。
C#で「関数を返す関数」を作成するには、ActionやFuncなどのデリゲートを返す関数を作成します。「関数を返す関数」を作成することにより、一部の処理をシンプルに記述することができます。どのように使うのでしょうか。例を見てみましょう。
Actionを返す関数
1秒間隔でコンソールに現在時刻を表示するプログラムを作成します。まずは通常?の書き方です。
using System.Timers;
class Foo
{
private Timer MyTimer;
public void TimerStart()
{
MyTimer = new Timer(1000);
MyTimer.Elapsed += (s, e) => Console.WriteLine(DateTime.Now.ToString());
MyTimer.Start();
}
public void TimerStop()
{
MyTimer.Stop();
}
}
TimerStart()で開始し、TimerStop()で停止します。呼び出すのは以下のようになります。
var foo = new Foo();
foo.TimerStart();
Console.ReadKey();
foo.TimerStop();
これを「Actionを返す関数」で書きかえてみましょう。TimerStart()は、myTimerを停止させるためのActionを返す、という考え方です。
using System.Timers;
class Foo
{
public Action TimerStart()
{
var myTimer = new Timer(1000);
myTimer.Elapsed += (s, e) => Console.WriteLine(DateTime.Now.ToString());
myTimer.Start();
// myTimerを停止するActionを返す。
return () => myTimer.Stop();
}
}
はじめの方法からの変更点としては
- タイマーを停止するTimerStop()がなくなった
- MyTimerがローカル変数になった。
- TimerStart()しない限り、TimerStop()を呼び出せなくなった。
と、少しシンプルに記述することができるようになりました。その代り、呼び出し側に少し注意が必要です。これを呼び出す側は以下のように変化します。
var foo = new Foo();
var stop = foo.TimerStart();
Console.ReadKey();
stop();
myTimerはローカル変数だから、TimerStart()の終了後は利用できないのでは?と思われるかもしれません。しかし問題はありません。詳しい説明は省きますが、myTimerは戻り値のAction内で利用されているため、TimerStart()が終了しても開放されないのです。
この方法は初めの方法に比べパフォーマンスが若干劣ります。しかしゲームなどのパフォーマンスが追及される場合でない限り、問題ないと考えます。
Funcを返す関数
先ほどの例は、関数が関数を返しました。これを進めると、関数の数珠つなぎにすることができます。別の例を示します。テキストファイルでログを作成するプログラムです。
public Func<string, Action> LogStart(string path)
{
var writer = new System.IO.StreamWriter(path);
return text => // Funcを返す
{
writer.WriteLine(text);
return () => writer.Dispose(); // Actionを返す
};
}
LogStart()はかなり複雑そうに見えますが、大丈夫です。よく見ると、LogStart()は「ある関数」を返し、ある関数はまた「別の関数」を返しています。詳しく見てみましょう。
LogStart()は、ログファイルを新規作成し、「ログに文字を書き込む関数」を返します。「ログに文字を書き込む関数」はさらに、「ログをクローズする関数」を返します。
これを利用する方法は以下の通りです。
var log = LogStart(@"C:\Debug\test.log");
log("Hello"); // LogにHelloを書き込む。
log("World")(); // LogにWorldを書き込んで、閉じる。
log("World")()の部分、こんな書き方してもいいんでしょうか?いいんです。でも少しわかりづらいですね。さすがにここまでくると、LogStartはActionを二つ返したほうがわかりやすいでしょう。
class LogControl
{
public Action<string> Write { get; set; }
public Action Close { get; set; }
}
public LogControl LogStart(string path)
{
var writer = new System.IO.StreamWriter(path);
return new LogControl()
{
Write = text => writer.WriteLine(text),
Close = () => writer.Dispose()
};
}
var log = LogStart(@"C:\Debug\test.log");
log.Write("Hello");
log.Write("World");
log.Close();
またはタプルで返すという方法もあります。
public (Action<string> Write, Action Close) LogStart(string path)
{
var writer = new System.IO.StreamWriter(path); // ファイルを作成し、
Action<string> write = text => writer.WriteLine(text); // テキストを出力して、
Action close = () => writer.Dispose(); // 閉じる。
return (write, close);
}
この書き方は、コメントに書きましたが、一つの関数の中で処理が順番に記述できるためわかりやすい、と私は感じています。
まとめ
以上、「関数を返す関数」の例を見てきました。「関数を返す関数」は、関数内で作成したオブジェクトを、その関数が終わってからも利用可能にします。単なる関数ではなく、内部に状態を持つ小さなクラスのようにふるまいます。通常の関数と違い呼び出し方に注意点が必要ですが、よろしければ利用を検討してみてください。