IEnumerableによるフィルタ(C#)

Visual Studio 2008から導入された標準クエリ演算子を利用するスタイルでは、IEnumerableを返すメソッドから始まり、中間ではIEnumerableを受け取ってIEnumerableを返すメソッド、最後はIEnumerableを受け取って最終処理するメソッドを数珠(じゅず)繋ぎにします。IEnumerableを受け取るとは引数で受け取ることを考えてしまいますが、そうではなくIEnumerableのメソッドとして実装することでフィルタ処理を左から右に順にドットで結びつけて表現できるので、処理順序とプログラム記述順序が一致します。

各フィルタ内部ではyieldを使ってコンテキストスイッチを行い次のフィルタにデータを渡す処理を内部で繰り返しています。そのためフィルタを記述するのに繰り返しをその都度記述する必要はありません。

IEnumerableによるフィルタ
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
using System;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Text;

namespace ToUpper
{
static class EnumerableExtentions {
public static void Each<T>(this IEnumerable<T> source, Action<T> func)
{
foreach (var elm in source) {
func(elm);
}
}

public static IEnumerable<T> Map<T>(this IEnumerable<T> source, Func<T, T> func)
{
foreach (var elm in source) {
yield return func(elm);
}
yield break;
}
}

class Program
{
public static IEnumerable<string> FileReader()
{
string line;
while ((line = Console.ReadLine()) != null) {
yield return line;
}
yield break;
}

static void Main(string[] args)
{
FileReader().Map(line => line.ToUpper()).Each(line => Console.WriteLine(line));
}
}
}

NetFrameworkではSelect,Whereなどのメソッドが用意されていますがこれらはどのように作ればよいのでしょう。IEnumerableを利用するスタイルで使えるEachMapというメソッドを作ってみます。EachIEnumerableを受け取って、引数で指定したメソッドに各要素を順に渡すメソッド、Mapは、指定したメソッドに各要素を順に渡すだけでなくそのメソッドが何らかの変換を行って値を返します。IEnumerableはNetframeworkに用意されている型ですが、作り付けの型に新たなメソッドを追加しなければならないことになります。また、IEnumerableはクラスではなくインタフェースですから通常は実装を持たないはずです。なおさらメソッドを実装することが難しく思えます。2008からは拡張メソッドによってそれが可能になっています。

Each,Mapは共に拡張メソッドとして実装しました。Eachの第2引数funcは各要素を受け取って処理するメソッドを指定します。Actionは引数を一つ受け取り、値を返さないメソッドを表します。一方Mapでは値を返しますから、Func T>となっています。Funcとは一つの引数T1を受け取り、T2を返すメソッドを表します。ここでは両方Tですから引数と同じ型の値を返すのです。

使用例は、標準入力を順に読み取り、大文字にして標準出力に書き込むコンソールプログラムです。英文を入力してEnterを押せば次の行に大文字に変換された文字列が表示されます。終了させるにはcontrol-Zに続いてEnterを押します。

FileReaderは標準入力を1行ずつ読み取るメソッドです。FileReaderは次のMapに結果を渡します。Mapの引数には大文字変換を行うラムダ式が指定されています。MapはさらにEachに結果を渡します。Eachの引数には渡された文字列を表示するラムダ式が指定されています。

これを発展させれば多段階の処理が必要なフィルタを実現することもできます。

この例では頻繁にyieldが呼ばれます。yieldのパフォーマンスは下記ページを参考にしてください。

参照

ラムダ式によるフォーム生成(C#)

Visual StudioのIDEでWindowsフォームを作ると、System.Windows.Forms.Formを継承したクラスが作成されます。今回はIDEに頼らないでラムダ式を使ってコーディングしてみます。

ラムダ式によるフォーム生成
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
private void ShowForm2() {
var form = new Form();
var nextFormButton = new Button();
var closeButton = new Button();

form.Text = "form2";

nextFormButton.Location=new Point(30, 50);
nextFormButton.Text = "次へ";
nextFormButton.Click += (sender, e) => ShowForm3();

closeButton.Location = new Point(120, 50);
closeButton.Text = "閉じる";
closeButton.Click += (sender, e) => form.Close();

form.Controls.Add(nextFormButton);
form.Controls.Add(closeButton);
form.Show();
}

private void ShowForm3() {
var form = new Form();
var textbox1 = new TextBox();
var textbox2 = new TextBox();
var closeButton = new Button();

form.Text = "form3";
textbox1.Location = new Point(10, 10);
textbox1.Leave += (sender, e) => textbox2.Text = textbox1.Text;
//textbox1 = null;

textbox2.Location = new Point(10, 30);

closeButton.Location = new Point(30, 50);
closeButton.Text = "閉じる";
closeButton.Click += (sender, e) => form.Close();

form.Controls.Add(textbox1);
form.Controls.Add(textbox2);
form.Controls.Add(closeButton);
form.Show();
}

ShowForm2は「次へ」「閉じる」ボタンがついたフォームを作成・表示するメソッドです。次へのボタンでShowForm3を実行します。ShowForm3はテキストボックスが2つと「閉じる」ボタンがついたフォームを作成・表示するメソッドです。上側のテキストボックスに文字を入力してTab移動などを行うと、同じ内容が下のテキストボックスにもコピーされます。

このコードの特長は一つのメソッドでフォームの生成とイベントハンドラをすべて記述できていること、また、フォームの作成に伴って新たなクラスを全く必要としていないことです。アプリケーション全体を新たなクラスなしに作ることもできそうな気がします。しかし実際にそのようにすれば設計上いろいろな無理が出てきそうです。フォームを作成する場面ではIDEが使えないこの手法は採用されることはないと思いますが、Adapterなどその他の場面はラムダ式を使って簡潔に記述できる場面もあります。

このように2つのアプローチが存在することとを理解した上で、ラムダ式が向くところ、オブジェクト指向が向くところを適宜選択しながら設計を行うことがこれからの開発スタイルになりそうです。

補足

ラムダ式を使う上での注意点があります。ShowForm3textBox1LeaveイベントでtextBox2.Text=textBox1.Textが実行されます。

この場合のtextBox1textBox2などのラムダ式の外で宣言された変数を自由変数、sendereなどのラムダ式の引数となっている変数を束縛変数と呼びます。

これが実行されるのはShowForm3メソッドが完了してtextBox1からフォーカスが抜けたときですから、それまでに自由変数textBox1に別のものを代入すると不正な処理が実行されます。例えばコード中のtextbox1
= null
のコメントを外すとLeave時に例外が発生ます。つまり自由変数は使い回しすることができないのです。特にループ中のラムダ式ではその点に気を付ける必要があります。具体的にはループ内で自由変数を宣言して使います。

自由変数が実際に使用される時点では既に変数スコープを抜けてしまっていますがそれについては内部的に保持されているので問題ありません。

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

アプリケーション開発では通常NetFrameworkなどの既製のライブラリを使用します。既製のライブラリは汎用的に作ってありますのでこれを開発しようとするシステムに合わせてより使いやすい中間的なライブラリを開発することがしばしば必要になります。例えばデータベース応用システムをNetFramworkで開発する場合、BindingSourceDataTableにバインドすることが圧倒的に多いわけのですが、BindingSourceは汎用的に作ってあるため使いにくいところがあります。そこでDataTableしかバインドできないけれどもそれ専用に使いやすいメソッドを備えたクラスを新たに開発することもこれに該当します。提供される機能と必要とする機能が一致しないためその差を埋める必要があるわけです。

粒度の小さい例ではイベントハンドラの引数があります。イベントを発生させたオブジェクトがsenderに付加的な情報がeに渡されますが、これらを使用することは私の場合あまりなくてむしろ別の引数が欲しいことが多いのです。電卓のようなアプリケーションを考えると数字ボタンはたくさんありますが0から9まで行っている処理はそれぞれの数値入力を受け取ることです。sendereが不要で入力した数値が必要となります。

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
//IDEでイベントハンドラを作成
private void button1_Click(object sender, EventArgs e) {
Console.WriteLine('1');
}

//Adapterクラス
private class Adapter {
char _letter;

public Adapter(char letter) {
_letter = letter;
}

public void Handler(object sender, EventArgs e) {
Console.WriteLine(_letter);
}
}

//ラムダ式を使ったアダプタ
private EventHandler AdapterByLambda(char letter) {
return (sender, e) => Console.WriteLine(letter);
}

private void Form1_Load(object sender, EventArgs e) {
button2.Click += new Adapter('2').Handler; //Adapterクラスを使用
button3.Click += AdapterByLambda('3'); //ラムダ式を使ったアダプタを使用
button4.Click += (sender1, e1) => Console.WriteLine('4'); //その場でラムダ式を記述
}

右記サンプルコードを試すにはWindows Formを作成して、ボタンを4つ貼り付けてください。実行結果は出力ウィンドウで確認します。

IDEでは簡単にイベントハンドラを作れますので10個のイベントハンドラを作ることも一つの方法として考えられます。Button1がこの方法でイベントハンドラを設定しています。

別の方法はAdapterクラスを作ることです。数値をクラスのメンバー_letterに代入しておき、EventHandler形式のメソッドとしてイベントハンドラを取り出します。Button2がこの方法でイベントハンドラを設定しています。

ラムダ式を使うとアダプタを簡単に記述できます。Button3がこの方法でイベントハンドラを設定しています。リスト中のAdapterByLambdaを見てください。ラムダ式はメソッド(ポインタ)を返す式で=>の左辺が引数、右辺がメソッド本体を表します。各引数の型は型推論により指定不要です。メソッド本体が複文になるときは{}で囲みます。AdapterByLambdaを呼び出したときにラムダ式が返すメソッドが実行されるわけではありません。AdapterByLambdaの引数letterはラムダ式実行時にも保持されています。

アダプタがこれだけ単純になるのならと、Button4はアダプタを使わないで直接ラムダ式を記述しています。アダプタを他で再利用しないならこのような簡便な方法も使えます。

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