ラムダ式、Decoratorへの応用(C#)

時間の掛かる計算処理が終わったときに終了メッセージを表示したり、例外処理やトランザクションの様に特定のコンテキストで、何らかの処理を行いたいことがあります。これらの事前処理や事後処理をラムダ式や匿名メソッドを使わないで実現することは面倒なものです。終了メッセージの例では計算処理本体が既にメソッドになっている場合に、計算処理の後に終了メッセージを表示するだけのメソッドを新たに書かなければなりません。例外処理の例では汎用的な例外処理で済む場合にもTry
Catchをその都度記述するか、汎用的な例外処理を行うメソッドを作ったにしても、Try Catchの内側をメソッドとして独立させなければなりません。

このような場合にラムダ式を使うとメソッドの数が減り、一続きの処理なのに記述箇所が散らばってしまうのを避けることできます。

Form1のコード
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private void HeavyOperation() {
Console.WriteLine("時間の掛かる処理");
System.Threading.Thread.Sleep(1000);
}

private void Form1_Load(object sender, EventArgs e) {
button1.Click += Lib.ToEventHandler(() => MessageBox.Show("button1.Click"));
button2.Click += Lib.ToEventHandler(() => {
HeavyOperation();
MessageBox.Show("時間のかかる処理を実行しました.");
});
button3.Click += Lib.ToEventHandler(
Lib.GetExceptionTrapAction(() => {
var a = 0;
var b = 0;
var c = a / b;
})
);
<span class=code-comment>/*
button4.Click += Lib.ToEventHandler(Lib.ShowForm2AndExecute(() => {
HeavyOperation();
MessageBox.Show("時間のかかる処理を実行しました.");
}));
*/</span>
}

サンプルコードはAdapterの例と同様にフォームのロード時に各ボタンのイベントハンドラを設定しています。

Button1はクリックしたらメッセージを表示するだけの単純な処理です。Lib.ToEventHandlerは引数・戻り値のないメソッドをEventHandlerに変換するライブラリのメソッドです。

Button2は時間の掛かる計算処理が終わったときに終了メッセージを表示する例です。HeavyOperationは処理本体です。HeavyOperationとメッセージ表示を順次実行する短い処理をラムダ式で表現しています。
Button3はゼロでの割り算により例外が発生する処理です。Lib.GetExceptionTrapActionは処理を汎用的な例外処理コンテキストの下で実行するメソッドです。この場合はTry
Catchの内側の処理をラムダ式で指定しています。

印刷処理などでは印刷前にタイトルを入力したり、データ抽出などの指定を行うために別フォームを表示することがあると思います。指定用のフォームをいろんな帳票に使い回すケースもよくあります。そんなとき起動したフォームから指定用のフォームの「実行」ボタンが押されたときの処理を渡す必要があります。これを簡潔に書くためにラムダ式を使います。

Visual Basic から C# に変換

普段Visual Basicを使っていると、C#が持っている匿名メソッドやyieldが使えたらと考えることもあり、Visual Studio 2008ではVisual Basicのラムダ式の制限が多く、さらに差を付けられているように感じます。そこでプロジェクトの一部をC#に変換してみることにしました。

小さなサンプルコードであればWeb上にオンライン変換の機能を提供してくれているサイトがあるのでそれを利用しますが、まとまったコード量だと正確に変換してくれないと手修正が大変になります。ツールの選定はいろいろな情報を探したなかでInstant C#(カナダからダウンロード販売)が良いと判断しました。

Visual BasicにあってC#にない機能や問題になりそうなところを事前に書き直しました。また実際に変換してみた結果問題があったところも含めて以下の項目を変更しておくと良いようです。

  • WithEventsHandles句をAddHandler, RemoveHandlerに書き換えておく
  • With文の後にコンストラクタや関数を記述しているものは、一時変数を定義してWithの直前で代入しておく、With文は残しておいても問題ない
  • 省略可能な引数を定義しているメソッドはオーバーロードに書き換えておく(Instant C#では自動変換できる)
  • メソッド内のスタティック変数をクラスレベルに出しておく
  • メソッド呼び出しで名前付き引数は通常の引数リストに書き換えておく

以上の事前準備を行ってから変換してみたのですが、コンパイルができる状態にはなりませんでした。Instant C#のバグもありましたし、言語仕様の違いから自動変換は不可能だろうと思えるところ(例えばインタフェースの明示的実装など)もありました。バグ対応はしてくれるようなので修正されたもので再度試すつもりです。

参照

Visual Studio 2008

Visual C# 2008 Expressを使って見ました。なかなか意欲的な新機能があるようです。単純な匿名メソッドをより単純に記述できるラムダ式・パイプライン対応のフィルタリング処理を実現してくれる標準クエリ演算子・フィルタリング処理をデータベースSQL構文に見せるシンタックスシュガーであるところの.NET統合言語クエリ(LINQ)・既存の型にメソッドを後付けする拡張メソッドに興味を持ちました。

Visual Studio 2005で追加されたyieldがフィルタリングをパイプライン化するのに利用され、ジェネリックが様々な型に標準クエリ演算子を適用するのに利用され、また新機能の拡張メソッドがNetFramework
2.0に手を加えずに既存クラスを標準クエリ演算子対応するために利用されています。このように緻密な計画に基づいて開発されたことに感心します。

LINQをSQL代替機能と見た場合、SELECT・INSERT・DELETE・UPDATEの内、SELECTに対応するものしか実現できません。そこで集合演算を一文でやってくれるものではなく、フィルタリングの標準的機能を提供してくれていると捉えています。

拡張メソッドは第一引数に指定されている型のメンバにそのメソッドが含まれているかのように振舞うものです。そのため既存の型にメソッドを後付けすることができます。Objectクラスの拡張メソッドInspectを書けばすべての型でInspectが使えるようになります。但し本来のメソッドと同名の拡張メソッドを作って機能を置き換えることはできません。拡張される型はクラスに限らずインタフェースも指定でき、Where・SelectなどもIEnumerable<T>の拡張メソッドとして実装されています。インタフェースを実装する型は拡張メソッドを実装する必要がなく、インタフェース内で完結する処理を拡張メソッドにすれば委譲コードを減らすことができます。

Visual Basicは2005に引き続きyieldと匿名メソッドが導入されませんでした。C#との色分けははっきりしたようです。業務アプリケーションでは標準クエリ演算子でパイプライン対応のフィルタリング処理ができればyieldは要らないでしょうし、匿名メソッドが欲しくなる局面はそれほどありません。しかしライブラリ開発ではyieldや匿名メソッドがないと遠回しな記述になったりすることがあります。これからはライブラリ開発をC#で行うことも考えています。

yieldのパフォーマンス(C#)

Linqは内部でyieldを使っていますがyieldはコンテキストスイッチを行いますので、そのパフォーマンスはどうなのか調べてみました。

yieldのパフォーマンス
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows.Forms;

namespace Project1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

//時間計測ルーチン
public TimeSpan MeasureTime(Action action)
{
var sw = new Stopwatch();
try {
sw.Start();
action();
} finally {
sw.Stop();
}
return sw.Elapsed;
}

//整数を一千万回発生
public IEnumerable<int> GetIntegerEnumerator()
{
for (var i = 0; i < 10000000; i++) {
yield return i;
}
}

//計測対象ルーチン、yield使用
void test1()
{
foreach (var i in GetIntegerEnumerator()) {
var x = i;
}
}

//クラスで実現
class Test2 : IEnumerator<int>
{
int _current = 0;

public int Current
{
get { return _current; }
}

public void Dispose()
{

}

object System.Collections.IEnumerator.Current
{
get { return _current; }
}

public bool MoveNext()
{
_current++;
return _current<10000000;
}

public void Reset()
{
_current = 0;
}
}

//計測対象ルーチン、クラス使用
void test2()
{
foreach (var i in new Test2().ToEnumerable()) {
var x = i;
}
}

private void button1_Click(object sender, EventArgs e)
{
for (var i=0; i<5; i++) {
Console.WriteLine(MeasureTime(test1)); //yield使用
}
}

private void button2_Click(object sender, EventArgs e)
{
for (var i = 0; i < 5; i++) {
Console.WriteLine(MeasureTime(test2)); //クラス使用
}
}
}

//ユティリティ
public static class EnumeratorExtensions
{
private class EnumerableConverter<T> : IEnumerable<T>
{
IEnumerator<T> _enumerator;

public EnumerableConverter(IEnumerator<T> enumerator)
{
_enumerator = enumerator;
}

public IEnumerator<T> GetEnumerator()
{
return _enumerator;
}

IEnumerator IEnumerable.GetEnumerator()
{
return _enumerator;
}
}

//IEnumerator<T>への変換メソッド
public static IEnumerable<T> ToEnumerable<T>(this IEnumerator<T> enumerator)
{
return new EnumerableConverter<T>(enumerator);
}
}

}
リストではGetIntegerEnumeratorで整数を一千万回発生させ受け側test1ではforeachでごく軽い処理を行っています。

その結果は約0.7秒でした。

  • 00:00:00.6699127
  • 00:00:00.7322575
  • 00:00:00.6883833
  • 00:00:00.6887978
  • 00:00:00.7158395

同等の処理をクラスで実現すると、0.5秒でした。

  • 00:00:00.5362418
  • 00:00:00.5300212
  • 00:00:00.5566201
  • 00:00:00.5479106
  • 00:00:00.5479031

マシンスペックはAMD Athlon(tm) X2 Dual Core Processor BE-2350 2.11GHz、2.00 GB RAM、OSはWin

XP SP3 32Bitです。Visual Studio 2008でデバッグビルドしました。


yieldはクラスに比べると若干遅いのですが、ソースファイルやHTMLファイルのフィルタリング処理を行う程度であれば極端にパフォーマンスを落とすこともなさそうです。

バッファリングによってコンテキストスイッチの回数を減らすテストプログラムを書こうとしたのですが、複雑でパフォーマンスが上がりそうもなかったので中止しました。

その後、Reflectorで生成されたコードを分析すると、yield returnはクラスによって状態を保持するコードに置き換えられることがわかりました。私はWinAPIのファイバーの様な仕組みを想像していましたが違いました。傾向としてyield returnの記述が多ければ生成されたコードのswitchの選択肢が増えますのでパフォーマンスは悪くなります。

初期化部と依存性注入(DI)

例えばVisual Studioのフォームデザイナの様なGUIを作ることを想定します。

フォームに貼り付けるコントロールはテキストボックスやチェックボックスなど様々な貼り付けられます。各コントロールはデザイナに貼り付けられたときに生成されるわけですが、このときデザイナからコントロールを生成するためには各コントロールの情報を持っていなければなりません。しかし、カスタムコントロールなどデザイナ設計時点では想定していないコントロールも扱えるようにしたい状況が生じます。そのためデザイナが生成すべき各コントロールの情報を持っているという設計には無理があるわけです。

Delphiなどいくつかの言語にはリンクしただけで初期化部が実行できるものがあります。この場合には初期化部でデザイナにコントロールの生成情報を登録する機会があります。それに対しC#、Visual
Basicでは初期化部を持ちませんので別の方法を考えないといけません。

そのような場面で依存性注入(Dependency Injection)が使えます。コントロールの生成情報(型名など)を設定ファイルに書き出し、デザイナ側で設定ファイルを読んでその情報を元にReflectionを使ってコントロールを生成します。

Visual Studioが依存性注入を使っているかどうかわかりませんが、フォームデザイナなどのプラグイン的な機能が必要なときには依存性注入を使えるというお話です。

アプリケーションの配付(ClickOnce)

以前に、高速通信が普及すると事務所内で使用するクライアント/サーバシステムでさえ、レンタルサーバを利用するソリューションを提案できると書きましたが、去年からそれを実現しています。

お客様にとってのメリットはサーバマシンの購入費用が不要、スペースが不要、メンテナンスが不要、バックアップが不要ということです(契約によって異なる)。また、最近では零細ソフトハウスの当方にいただく案件でさえ拠点間通信が必要であったりしますので、その点でもレンタルサーバはうってつけなのです。

システムとしてWebとクライアント/サーバの選択がありますが、現状では案件に応じてどちらも採用しています。Webのメリットは配付が楽であること、クライアント/サーバは使いやすいユーザインタフェースを提供できることがあげられます。それぞれのデメリットは全く逆になるわけですが、WebのユーザインタフェースについてはAjaxにより、クライアント/サーバのシステム配付についてはNetFramework限定ですがClickOnceにより、それぞれ欠点を克服する技術が現れてきました。

ClickOnceを先日お客様に導入したところ、初回導入はインストーラ実行などの手間があるものの、アップデートに関してはWebシステムの手間と全く変わらないものとなりました。エンドユーザ様との間に入っていただいているSI様の手間も大幅に軽減できるものと期待しています。

現在ではVPNとセットで 使用して効果を上げているSI様がおられます。

失敗談

通販向け販売システムでは、お客様の業務理解が不十分であったため最初にお見せしたプロトタイプがニーズにあっておらず、修正に時間が掛かりました。このシステムを手がけるまでは製造業の販売管理システムを手がけていたのですが、それを元に考えたため不整合が出てきました。

- 業務では販売に際して不良顧客でないか顧客情報を必ず確認してから納品書を発行する。プロトタイプではキータッチ数を減らすためにメニューからダイレクトに納品書入力画面に遷移していた
- 業務では顧客毎の売掛という扱いをしておらず納品書別の売掛を管理していた。プロトタイプでは顧客毎の売掛を合計して請求書発行する流れになっていた。また入金も顧客に対して入金するようになっていた

逆に重宝がられた機能もあります。

- 入金処理では売掛一覧を表示してその中から入金の選択をするようにしたので、業務が早くなった
- 売上・入金ともに伝票の取消・修正がいつでもできるようにした(月末締め戻し可)
- マウスを使わないでも操作できるようにした
- 処理スピードが速いため納品書を1割多く打てるとのこと(ハードウェアの進歩のおかげもあります)

この案件ではSI様のご都合によりメイキング前のエンドユーザ様との打ち合わせをまったく行っておらず、プロトタイプ段階でニーズにあったものを提示できませんでした。この案件以降ではエンドユーザ様との打ち合わせを重視しております。

標準クエリ演算子

表計算ソフトがExcel一色ではなかった頃、大変マイナーなソフトで三洋電機のGOALというものがありました。計算はセルに計算式を埋め込むのではなく、独自のマクロを使います。

1
D = A*B {C<1000}

上の式はC列が1000以下の行に対して、D列にA列とB列を掛けたものを代入します。余り複雑なことはできませんでしたが、一つの式で複数行の処理ができて単純明快でした。

同様の処理をVisual Basicなどのプログラミング言語で行うとループで回す処理が必要になります。Visual Studio 2008では繰り返し処理を簡潔に書けるように標準クエリ演算子とシンタックスシュガーである.NET統合言語クエリ(LINQ)が導入されました。モデルになっているのはデータベースに使われるSQLで複数行の計算を一文で書けます。SELECTはフィルタリング、INSERTは挿入、DELETEは削除、UPDATEは更新を行います。

1
UPDATE TableName SET D=A*B WHERE C<1000

このうちLINQではSELECTを処理してくれます。そのため上に書いた式で「A*B WHERE C<1000」の複数行の結果を作り出すことはできても素直な方法ではDへの代入はできないので、最終的にはループを回して代入する必要があります。

Visual Visual Studio 2005で比較しています。

現在Visual Basicを使っているのは、記述は冗長だが読みやすいという点を買っています。Modula-2,DelphiなどPascalライクな文法を持つ言語が好みということもあります。

VB vs C#(NET Framework)

Visual BasicとC#、変換ツールが存在するほど互換性の高い言語ですが、いざどちらを使うかとなると判断に困ることもあります。Visual
BasicとC#の比較については書籍が発行されていたり、他のWebサイト上でも同様の記述はたくさんありますが、私見を書いてみました。Visual
Studio 2015時点で比較しています。

項目 VB C# 内容 経過
機能有無 匿名メソッド × VBで同等のことをするには、メソッド内で使用する変数などをカプセル化するクラスが必要 VS2010でVBも同等機能が追加された
メソッドの引数省略 × C#ではオーバーロードで対応する。 VS2010でC#も同等機能が追加された
Handles句 × C#ではメンバ変数に代入するたびに-=,+=によりハンドラの除去、追加を行わなければならない。
yield return × yield returnで列挙に限ってコルーチンが実現できる。アーキテクチャパターンの一つであるPipe&Filterパターンを採用した場合に省メモリで見通しの良いコーディングができる VS2012でVBも対応
Option Strict Off × 通常Option Strict Onにしているが、Office VBAを呼び出すような状況において型チェックを外すとシンプルに書ける。また実行環境にOfficeがなくてもOffice呼出以外の機能は使える。 VS2010でdynamicが追加されC#でも問題解消
With文 × C#では一時変数で対応する。
多分岐 C#のswitchは使いにくい(breakが必要とか、範囲指定できないとか)
staticインポート × VBは別のクラスでモジュール名を指定しないでメンバーを呼び出せる。 VS2015でC#も対応
Like演算子 × C#は正規表現で代用
unchecked × 通信プログラムなど負の数を符号無し数値に変換するときに必要となる。これがないと一旦拡張変換してビット論理積をとるなどの対応が必要になる。
書きやすさ 制御構造 × C#は複文の時に中括弧を使う。C#はif,swithなどの制御構造を追加するときなどインデントが崩れると右括弧がどれに対応しているのかわからなくなる。
文末・行末 × C#はセミコロンが必要、タイピングが面倒。VBは逆に行継続文字が必要な場合がある。常にセミコロンがつきまとうC#が不便
ラムダ式 × C#の記述は簡潔
括弧 × VBは引数、型引数、配列のいずれも小括弧を使うので複雑に入り組んだ文で混乱しがち。C#はそれぞれ小括弧、山形括弧、かぎ括弧と分かれるので見やすい。但し、if,whileなどに制御構造に小括弧が必要
IDE支援 コード補完 × VBのコード補完は高速動作、End Subなど制御構造終端が補完された後の記述がしやすい。 VS2013でC#も終端括弧補完
リファクタリング × C#は名前の変更だけでなく、フィールドのカプセル化・パラメータの順序変更など多くのリファクタリングが利用できる。VBは名前の変更のみ VS2015でVBも対応
注意点 比較演算子 VBは等号でString.EmptyNothingを同一視する。あえて区別したいときはEqualsメソッドを使用するなどの対応が必要。便利な場面が多いがこの動作を理解していないとはまることがある。C#では上記2つを区別するので、同一視したいときはIsNullOrEmptyメソッドで対応。

現在C#を主に使っています。お客様のご要望に応じてVisual Basicも使います。私が使っている範囲ではそれぞれ一長一短があり甲乙付けがたいと感じています。

参照

BindingSource

NETFrameworkは汎用的なフレームワークなので、データバインディングについてもデータベース連結だけをターゲットに組まれているわけではありません。そのため、データベース応用システム開発に使用した場合、Accessなどのテータベース開発ツールと比べると少しずつ回りくどいというか使いにくい面があります。そのためデータバインディングやTableAdapterなどの枠組みを使わずもっと単純なクラスのみを使って開発するPONO(Plain Old Net Object)という考え方があるようです。もともとはPOJOから派生した言葉らしいです。Visual Basic 6においてもDataEnvironmentを使って開発する方は意外に少なかったと記憶しています。

私はNETFrameworkに用意されている枠組みを活かしながら、使いにくいところは別のクラスで置き換えたり、拡張したりしながら使っています。TableAdapterは基底型がObjectでありライブラリ化が難しいのでほとんど使わず、同等機能のクラスを新たに作成しました。BindingSourceはこれを継承してデータベース連結に特化したTableBindingSourceを作成して使用しています。またTextBoxなどUI部品はNullの扱いが苦手なので、Nullも自然に扱えるように拡張しています。このような拡張ができてしまうのもNETFrameworkの汎用性のおかげなのですが。

このような拡張の中で少し引っかかった点がありますのでご紹介します。BindingSourceにはバインドがサスペンドしている状態があります。DataTableに全くデータがない場合に暗黙的にサスペンド状態になります。明示的に状態を切り替えるにはSupendBindingとResumeBindingを使います。IsBindingSuspendedがその状態を取得するのに使われます。ところがIsBindingSuspendedが内部で保持しているサスペンドの状態を必ずしも表していないことがあるようです。データが全くなくなって暗黙的なサスペンド状態でAddNewを呼び出しても再開してくれない場合があったので、ResumeBindingを呼び出しました。しかしそれでも再開してくれないのです。

1
2
3
If IsBindingSuspended Then SuspendBinding() 'バグ対策、内部状態が一致しない場合がある
Dim newRow As Object = AddNew()
ResumeBinding() 'バグ対策、SuspendBindingに関連して必要

そこで対策としてIsBindingSuspendedの場合にSuspendBinding()を呼ぶことで本当のサスペンド状態?になってAddNew後のResumeBindingが働くようになりました。