パイプライン処理の後始末をしよう

パイプライン対応、してますか?

軽めの記事をもう一つ。

関数内を begin - process - end の 3 つのブロックに区切って、パイプラインから配列を渡してやると、

  1. begin ブロックが 1 回実行される
  2. process ブロックが、パイプラインから渡した配列の要素数分、繰り返し実行される
  3. end ブロックが 1 回実行される

という流れになります。

なお、関数に begin - process - end を書かなかった場合、関数全体が暗黙に end ブロックになります。
そのため、パイプライン入力に対しては、最後の要素しか渡されない点に注意しましょう。

さて、process はいいとして、begin と end はどのように使えばいいでしょうか?

私見ですが、2 つのパターンがあると考えます。
勝手に命名すると、process 主体型と、end 主体型です。

この記事は process 主体型について取り上げるのがメインなので、先に簡単に end 主体型の説明をしてしまいます。

end 主体型

process ブロックで受け取ったデータを溜め込んで、end ブロックで主な処理をするタイプです。
例えば、Sort-Object、Get-Unique、Measure-Object などのように、配列全体を見ないと結果が出せないようなコマンドがこれにあたります。
その性質上、コマンドの出力をパイプラインでさらに次につなげても、一旦ここで処理が止まります。

このタイプでは、begin ブロックでは、集計等の準備をすることになるでしょう。

process 主体型

process ブロックで 1 つデータを受け取っては 1 つ処理し、(もしあれば)次のパイプラインに流していくタイプです。
このタイプですと、begin と end は必要ない場合も多く、そのため PowerShell には、簡易型の filter という機能があります。
filter については最後に簡単に解説します。

process 主体型で begin と end を必要とするのはどんな場合か? というのが、本稿の主題となります。

率直に言ってしまうと、begin では process での処理に必要な何らかの事前準備、それも、1 回だけ行えばよく、かつ、それなりに処理コストがかかるようなことをするということになるでしょうか。
そして、end ではその対極にあたる後始末を行うことになります。

例えば、Out-File コマンドのようなものを想像してみるとよいでしょう。
このコマンドは、

  1. begin でファイルを開き
  2. process でデータを 1 つずつ書きこんで
  3. end でファイルを閉じる

という動作をします。
ファイルを開いて閉じるのを process の中で毎回行っても動くには動きますが、パフォーマンス的に不利になるのは自明です。

さて、この手のリソースの確保と解放というパターンは、C# であれば using スコープで囲むのがお約束です。
using スコープを使えば、処理中に例外が発生したとしても(もちろん正常終了した場合もですが)、確実に解放してくれます。

では begin - process - end はどうかと言うと、using のような挙動を期待すると裏切られます。
process ブロックで例外が発生した場合、end ブロックは実行されません。
大事なことなのでもう一回言います。
process ブロックで例外が発生した場合、end ブロックは実行されません。

なんでそんな仕様になってるんだーと思いますが、考えてみると、end 主体型のコマンドの process ブロックで例外が発生した場合、end で適切な処理ができない場合もあるでしょう。
そのような場合にも常に end が呼ばれてしまう方が嫌な仕様かもしれません。
何と言っても、process 主体型で begin - end が必要なものは少数派なのですし。

やっと本題

process 主体型の処理では、例外発生時の後始末を自分でやらないといけません。
どうすればいいでしょうか。というわけでサンプル コードをドン。

はい。前回も出てきた関数内関数です。
関数内関数(CleanUp-Resource)は、外側の関数(Using-Resource)の外からはアクセスできませんが、begin - process - end のブロックをまたいでアクセスすることはできます。

注意すべきは、using(実体は try - finally)を意識しているからといって、スクリプトでもうっかり finally を使ってしまわないことと、catch ブロック内で例外を再 throw すること。
再 throw しないと、最後まで処理が走って、CleanUp-Resource がもう一回呼ばれてしまいます。

マネージ コードでは

C# のようなマネージ コードでコマンドを書く場合、1 つのコマンドは 1 つのクラスに対応し、begin - process - end の各ブロックはそれぞれがメソッドに対応します。
そのため、using スコープで後始末をするわけにはいきません。

このような場合は、コマンド クラスに IDisposable を実装することで、自動的に Dispose を呼んでくれますので、そこで後始末をします。
EndProcessing や StopProcessing から Dispose を自分で呼んでやる必要はありません。

なお、Dispose や StopProcessing では、実行できる処理が限られていますので注意しましょう(WriteDebug なども実行できません)。

filter

filter は簡易型の function です。
関数全体が暗黙に process ブロックになり、引数宣言をしなくても、パイプラインからの入力を $_ で受け取ることができます。

また、引数を明示的に宣言して渡すこともできるようですが、[Parameter] 属性を付けてしまうと、パイプライン入力を $_ で受け取ることができなくなってしまうようです。

実は、高度な関数の function キーワードを filter に変えても動作はします。
複雑なパラメーター属性や begin - process - end を書くことも function と同じように可能です。
が、そういう複雑なのを敢えて filter で書く必要はないので、素直に function にしておきましょう。

終わりに

これ、軽め?