デザインパターン
以下に一つでも当てはまる方は、プログラミング設計を少し勉強した方が良いかもしれません。
・最初は問題ないが、だんだん読みづらいコードにある
・スパゲティコードになってしまう
・オブジェクト指向の恩恵を生かしきれていない
・コメントが無いとさっぱり分からない
デザインパターン
「事例」みたいなものなのですが、オブジェクト指向型開発において、現場でよく使われる設計を先人たちがパターン化してくれたものが「デザインパターン」です。
発端はGoF(Gang of Four)と呼ばれるエーリヒ・ガンマ、リチャード・ヘルム、ラルフ・ジョンソン、ジョン・ブリシディースの4人のことで、1995年に発刊された「オブジェクト指向における再利用のためのデザインパターン」で一躍世に広まることになります。
C言語にクラス概念が追加されたC++と、Java、最後にC#と、これらオブジェクト指向の代表言語でよく使われる設計を網羅したものとなります。
デザインパターンを意識することで、プログラムがオブジェクト指向の概念に沿った、「再利用」・「拡張性」・「独立性」が自然と設計されるようになります。
GroupByメソッド③
GroupByメソッドはクラスコレクション以外、通常のarrayコレクションでも使用可能です。
List<string[]> lst = new List<string[]> { new string[]{ "1", "25", "5", "3000"}, new string[]{ "1", "31", "10", "4000"}, new string[]{ "2", "31", "98", "12800"}, new string[]{ "1", "25", "11", "250"} }; var res = lst.GroupBy(x =>Tuple.Create(x[0], x[1] ));
Dim lst As New List(Of String()) From { New String() {"1", "25", "5", "3000"}, New String() {"1", "31", "10", "4000"}, New String() {"2", "31", "98", "12800"}, New String() {"1", "25", "11", "250"}} Dim res = lst.GroupBy(Function(x) Tuple.Create(x(0), x(1)))
GroupByメソッドにより、集約したコレクションの結果を使って集計することが簡単に出来ます。
var res = lst.GroupBy((x) => Tuple.Create(x[0],x[1])) .Select((y) => new { KaisyaCD = y.Key.Item1, SyainCD = y.Key.Item2, SumValue = y.Sum((s) => int.Parse(s[2])) } ); foreach r In res Console.WriteLine(r.KaisyaCD + ":" + r.SyainCD + " " + r.SumValue); Next
Dim res = lst .GroupBy(Function(s) Tuple.Create(s(0), s(1))).Select( Function(y) New With { .KaisyaCD = y.Key.Item1, .SyainCD = y.Key.Item2, .SumValue = y.Sum(Function(s) Integer.Parse(s(2))) }) For Each r In res Console.WriteLine(r.KaisyaCD & ":" & r.SyainCD & " " & r.SumValue) Next
1:25 16 1:31 10 2:31 98
GroupByメソッド②
GroupByメソッドには複合キーを指定することも可能です。複合キーの指定の仕方は2通りあり、new(VB.NETはNew With)による匿名型で指定する方法とTuple.Createで指定する方法があります。
List<Uriage> lst = new List<Uriage>() { new Uriage{KaisyaCD="1",SyainCD = "25",UriageSu = 5, UriageKingaku=3000}, new Uriage{KaisyaCD="1",SyainCD = "31",UriageSu = 10, UriageKingaku=4000}, new Uriage{KaisyaCD="2",SyainCD = "31",UriageSu = 98, UriageKingaku=12800}, new Uriage{KaisyaCD="1",SyainCD = "25",UriageSu = 11, UriageKingaku=250} }; //new var res = lst.GroupBy(x => new { x.KaisyaCD, x.SyainCD }); //Tuple.Create var res = lst.GroupBy(x => Tuple.Create(x.KaisyaCD, x.SyainCD)); foreach (var r1 in res) { Console.WriteLine(r1.Key); foreach (var r2 in r1) { Console.WriteLine(r2.UriageSu); } }
VB.NETのNew Withの場合は、匿名型に対してKeyキーワードをつけないと集計されません。
Dim lst As New List(Of Uriage) From { New Uriage With {.KaisyaCD = "1", .SyainCD = "25", .UriageSu = 5, .UriageKingaku = 3000}, New Uriage With {.KaisyaCD = "1", .SyainCD = "31", .UriageSu = 10, .UriageKingaku = 4000}, New Uriage With {.KaisyaCD = "2", .SyainCD = "31", .UriageSu = 98, .UriageKingaku = 12800}, New Uriage With {.KaisyaCD = "1", .SyainCD = "25", .UriageSu = 11, .UriageKingaku = 250} } 'New With Dim res = lst.GroupBy(Function(x) New With {Key x.KaisyaCD, Key x.SyainCD}) 'Tuple.Create Dim res = lst.GroupBy(Function(x) Tuple.Create(x.KaisyaCD, x.SyainCD)) For Each r1 In res Console.WriteLine(r1.Key) For Each r2 In r1 Console.WriteLine(r2.UriageSu) Next Next
{ KaisyaCD = 1, SyainCD = 25 } 5 11 { KaisyaCD = 1, SyainCD = 31 } 10 { KaisyaCD = 2, SyainCD = 31 } 98
GroupByメソッド①
GroupByメソッドはLINQの中では少し特殊(私だけか?)で、単純なlistやarrayのようなコレクションでは利用しません。コレクションコレクションの中にコレクションが格納されているような場合の集計に非常に便利な機能となります。複雑な集計を簡単に書くことができるのです。
データテーブルのような階層構造を持ったコレクションの小計・合計を求めたりも簡単に出来ます。
以下のような状態を保持するようなクラスと、このクラスを要素にもつようなコレクションがあります。一種のテーブルのような構造です。
public class Uriage { public string KaisyaCD { get; set; } public string SyainCD { get; set; } public int UriageSu { get; set; } public int UriageKingaku { get; set; } }
Public Class Uriage Public Property KaisyaCD() As String Public Property SyainCD() As String Public Property UriageSu() As Integer Public Property UriageKingaku() As Integer End Class
社員番号(SyainCD)で集計します。GroupByメソッドは引数にラムダ式でグループ分けのKeyを指定します。
List<Uriage> lst = new List<Uriage>() { new Uriage{KaisyaCD="1",SyainCD = "25",Su = 5, Kingaku=3000}, new Uriage{KaisyaCD="1",SyainCD = "31",Su = 10, Kingaku=4000}, new Uriage{KaisyaCD="1",SyainCD = "31",Su = 98, Kingaku=12800}, new Uriage{KaisyaCD="1",SyainCD = "25",Su = 11, Kingaku=250}}; var res = lst.GroupBy(x => x.SyainCD); foreach(var r in res) { Console.WriteLine(r.Key); }
Dim lst As New List(Of Uriage) From { New Uriage With {.KaisyaCD = "1", .SyainCD = "25", .Su = 5, .Kingaku = 3000}, New Uriage With {.KaisyaCD = "1", .SyainCD = "31", .Su = 10, .Kingaku= 4000}, New Uriage With {.KaisyaCD = "1", .SyainCD = "31", .Su = 98, .Kingaku= 12800}, New Uriage With {.KaisyaCD = "1", .SyainCD = "25", .Su = 11, .Kingaku= 250}} Dim res = lst.GroupBy(Function(x) x.SyainCD) For Each r In res Console.WriteLine(r.Key) Next
25 31
このように集計後はIEnumerableで列挙できるような状態になります。
SelectManyメソッド
単純な配列に対して処理する場合は、SELECTメソッドで処理できますが、配列に配列を持つようなコレクションは少し面倒です。
例えば、
List<string[]> values = new List<string[]> {new string[] { "京都府", "京都市"}, new string[] { "奈良県", "奈良市"}, new string[] { "大阪府", "大阪市"}, new string[] { "兵庫県", "神戸市"}, new string[] { "和歌山", "和歌山市"}};
Dim values As New List(Of String()) From {New String() {"京都府", "京都市"}, New String() {"奈良県", "奈良市"}, New String() {"大阪府", "大阪市"}, New String() {"兵庫県", "神戸市"}, New String() {"和歌山", "和歌山市"}}
このようなコレクションです。
これを一つのコレクションにするには、SelectメソッドでString.Joinメソッドなどを複合的に利用して処理する必要があります。
var res = values.Select(x => String.Join(",", x)).ToArray(); Console.WriteLine(String.Join(",", res));
Dim res = values.Select(Function(x) String.Join(",", x)).ToArray() Console.WriteLine(String.Join(",", res))
京都府,京都市,奈良県,奈良市,大阪府,大阪市,兵庫県,神戸市,和歌山,和歌山市
これはSelectManyメソッドを使用すると簡単に一つのコレクションへ変形できます。この処理を「平坦化」と呼びます。
var res = values.SelectMany(x => x).ToArray();
Console.WriteLine(String.Join(",", res));
Dim res = values.SelectMany(Function(x) x).ToArray() Console.WriteLine(String.Join(",", res))
京都府,京都市,奈良県,奈良市,大阪府,大阪市,兵庫県,神戸市,和歌山,和歌山市
LINQによる遅延評価
イテレータによる遅延評価と同様で、LINQでも遅延評価を実装することが出来ます。
例えば、単純に1秒待つメソッド関数があるとします。
private int testFunction(int v) { Thread.Sleep(1000); return v * 2; }
Private Function testFunction(ByVal v As Integer) As Integer Thread.Sleep(1000) Return v * 2 End Function
LINQを利用してコレクション全ての要素をこの関数へ渡して結果を受け取るような処理を考えます。res1は先行評価、res2は遅延評価で、結果をそれぞれ2回ずつforeachでループ処理をします。違いが分かるように、Stopwatch クラスで時間を計ります。
Stopwatch sw = new Stopwatch(); int[] values = { 8, 4, 9, 5, 7 }; //先行評価 sw.Start(); var res1 = values.Select(x => testFunction(x)).ToArray(); foreach (var r in res1) {} foreach (var r in res1) {} sw.Stop(); Console.WriteLine("先行評価 " + sw.Elapsed); sw.Reset(); //遅延評価 sw.Start(); var res2 = values.Select(x => testFunction(x)); foreach (var r in res2) {} foreach (var r in res2) {} sw.Stop(); Console.WriteLine("遅延評価 " + sw.Elapsed);
Dim sw As New Stopwatch() Dim values As Integer() = {8, 4, 9, 5, 7} '先行評価 sw.Start() Dim res1 = values.Select(Function(x) testFunction(x)).ToArray() For Each r In res1 Next For Each r In res1 Next sw.Stop() Console.WriteLine("先行評価 " & sw.Elapsed) sw.Reset() '遅延評価 sw.Start() Dim res2 = values.Select(Function(x) testFunction(x)) For Each r In res2 Next For Each r In res2 Next sw.Stop() Console.WriteLine("遅延評価 " & sw.Elapsed)
先行評価と遅延評価はの違いは、LINQの結果をToArrayメソッドがあるかないかの違いだけです。
どちらが何秒になるでしょうか。
先行評価 00:00:05.0092785 遅延評価 00:00:10.0103753
遅延評価が10秒と先行評価の倍かかっているのが分かります。これは、res2はLINQの段階では評価されず、foreachで初めて関数に渡されて評価される為です。これはブレークポイントを設定してみるとより分かりやすいです。
よって、foreachが2回あるので2回ずつ合計10回testFunctionが実行されます。結果、先行評価に比べて倍の時間がかかるわけです。
それに比べて先行評価は、ToArrayメソッドが実行される瞬間でLINQが評価されますので、foreachでは評価されません。つまり、5回testFunctionが実行されるだけです。
では、今度はforeachの途中で条件分岐を入れてみます。testFunctionの結果が10未満の場合はforeachを打ち切るようにします。あと、foreach文は一つに減らしてみましょう。
Stopwatch sw = new Stopwatch(); int[] values = { 8, 4, 9, 5, 7 }; //先行評価 sw.Start(); var res1 = values.Select(x => testFunction(x)).ToArray(); foreach (var r in res1) { if (r < 10) break; } sw.Stop(); Console.WriteLine("先行評価 " + sw.Elapsed); sw.Reset(); //遅延評価 sw.Start(); var res2 = values.Select(x => testFunction(x)); foreach (var r in res2) { if (r < 10) break; } sw.Stop(); Console.WriteLine("遅延評価 " + sw.Elapsed);
Dim sw As New Stopwatch() Dim values As Integer() = {8, 4, 9, 5, 7} '先行評価 sw.Start() Dim res1 = values.Select(Function(x) testFunction(x)).ToArray() For Each r In res1 If r < 10 Then Exit For Next sw.Stop() Console.WriteLine("先行評価 " & sw.Elapsed) sw.Reset() '遅延評価 sw.Start() Dim res2 = values.Select(Function(x) testFunction(x)) For Each r In res2 If r < 10 Then Exit For Next sw.Stop() Console.WriteLine("遅延評価 " & sw.Elapsed)
さて、結果はどうなりましたか?
遅延評価のメリットとしては、
- メモリの節約
- 評価値を都度評価できる
という点があります。
但し、上記の例は少し極端ですが、遅延評価をよく理解しないでLINQやイテレータを使用すると、思いがけないところでパフォーマンスが極端に落としてしまったり、逆にLINQを利用するメリットを無視してしまう可能性がありますので、よく理解して使用するようにしましょう。
判定メソッド(Any、All、Contains)
コレクションに対して、ある条件を満たすような要素が存在するかどうかを判定するメソッドです。以下のコレクションに対して実行してみます。
int[] values = { 1, 9, 5, 6, 8, 6, 2, 5, 3 };
Dim values As Integer() = {1, 9, 5, 6, 8, 6, 2, 5, 3}
Anyメソッド
条件を満たす要素があるかどうかを判定します。
Console.WriteLine(values.Any(x => x != 8));
Console.WriteLine(values.Any(Function(x) x <> 8))
Allメソッド
全ての要素が条件を満たしているか判定します。
Console.WriteLine(values.All(x => x != 8));
Console.WriteLine(values.All(Function(x) x <> 8))
Allメソッドは検索対象が空の場合trueを返しますので注意して下さい。
Containメソッド
指定した要素が含まれているかどうかを判定します。
Console.WriteLine(values.Contains(3));
Console.WriteLine(values.Contains(3))