24
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

C#Advent Calendar 2019

Day 11

関数を返す関数。

Posted at

この記事は 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);
}

この書き方は、コメントに書きましたが、一つの関数の中で処理が順番に記述できるためわかりやすい、と私は感じています。

まとめ

以上、「関数を返す関数」の例を見てきました。「関数を返す関数」は、関数内で作成したオブジェクトを、その関数が終わってからも利用可能にします。単なる関数ではなく、内部に状態を持つ小さなクラスのようにふるまいます。通常の関数と違い呼び出し方に注意点が必要ですが、よろしければ利用を検討してみてください。

24
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?