Posted at

[C#]C#でDeferする

More than 1 year has passed since last update.

golangにはdeferという機能があります。

deferを使うと関数を抜けるときに処理を実行することができます。

以下にサンプルを示します。

package main

import (
"fmt"
)

func hoge() {
i := 1
fmt.Printf("A:%v\n", i)
i = i + 1
defer fmt.Printf("B:%v\n", i)
i = i + 1
fmt.Printf("C:%v\n", i)
i = i + 1
defer fmt.Printf("D:%v\n", i)
i = i + 1
fmt.Printf("E:%v\n", i)
}

func main() {
fmt.Printf("main start\n")
hoge()
fmt.Printf("main end\n")
}


実行結果

main start

A:1
C:3
E:5
D:4
B:2
main end

実行結果を見ると以下のことが分かります。



  • deferを書いた場所では関数が呼ばれていない

  • 関数の終了時にdeferを書いた順番とは逆順に実行されている

  • 変数ideferを書いた場所での値になっている

C++に慣れている方ならクラスのデストラクタをイメージしていただければよいです。

これをC#でも実現してみます。

(先に書いておきますが実用的ではありません。あくまで同じようなことを実現するだけです。)


実装

public class Deferrable

{
private List<Action> actions = new List<Action>();

public static void Execute(Action<Deferrable> action)
{
var d = new Deferrable();
try
{
action(d);
}
finally
{
d.ExecuteAll();
}
}

private Deferrable()
{
}

public void Defer(Action action)
{
this.actions.Add(action);
}

public void Defer<T>(Action<T> action, T arg)
{
this.actions.Add(() => action(arg));
}

private void ExecuteAll()
{
for (var i = this.actions.Count - 1; i >= 0; i--)
{
this.actions[i]();
}
}
}


使用例


サンプル

Deferrable.Execute(d =>

{
var index = 1;
Console.WriteLine($"A:{index++}");
d.Defer(_index => Console.WriteLine($"B:{_index}"), index++);
Console.WriteLine($"C:{index++}");
d.Defer(_index => Console.WriteLine($"D:{_index}"), index++);
Console.WriteLine($"E:{index}");
});


実行結果

A:1

C:3
E:5
D:4
B:2


解説

実装を見ればとてもシンプルなのであまり書くことはありませんがポイントはDefer<T>ExecuteAllでしょうか。

Defer<T>は引数にアクションだけではなく引数も受け取ります。

これによってDefer<T>を呼び出したときの値をコピーすることができます。

ExecuteAllではDeferを呼び出した順番と逆順になるようにアクションを実行しています。


まとめ

C#でもdeferっぽいものを作成することはできました。

DeferrableListなどといった一時オブジェクトが作成されてしまうためノーコストとは呼べません。

(golangdeferdeferを使用せずに書いた時と比べて完全にノーコストというわけではありませんがこの実装よりかは圧倒的に低いコストです。)