佐々木屋

技術的なことから趣味まで色々書きます

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))

無意味なスコープ

変数スコープは出来るだけ短くするとバグの発生を抑えることができます。

課題①

変数iはメソッドの最初の方に宣言されていますが、実際はfor文でしか使用されていません。

int i;

//処理①

for (i = 0; i <= 10; ++i) {
    //処理②
}
Dim i As Integer

'処理①

For i = 0 To 10
    '処理②
Next


リファクタリング

特にIfやUsingなど構造化される場合は、構造化の内部で宣言すれば、スコープを構造化内部だけにすることが出来ます。これによって変数名を無駄にいくつもそろえる必要が無くなりますし、何と言っても意図しない値の衝突、バグの発生を抑えることができます。

//処理①

for (int i = 0; i <= 10; ++i) {
    //処理②
}
'処理①

For i As Integer = 0 To 10
    '処理②
Next



複数回同じ変数を違った意味で使いまわすことはバグの温床となります。

課題②

int m;

//変数mは分を保持する
m = DateTime.Now.Minute;

//変数hを使用した処理

//変数mは月を保持する
m = DateTime.Today.Month;

//変数mを使用した処理
Dim m As Integer

'変数mは分を保持する
m = DateTime.Now.Minute

'変数hを使用した処理

'変数mは月を保持する
m = DateTime.Today.Month

'変数mを使用した処理


リファクタリング

本来は別変数名を使用すればよいのですが、わざと構造化させて処理を分断して変数スコープを宣言する方法もあります。

{
    //変数mは分を保持する
    int m = DateTime.Now.Minute;

    //変数hを使用した処理
}

{
    //変数mは月を保持する
    int m = DateTime.Today.Month;

    //変数mを使用した処理
}
With Nothing
    '変数mは分を保持する
    Dim m As Integer = DateTime.Now.Minute

    '変数hを使用した処理
End With

WIth Nothing
    '変数mは月を保持する
    Dim m As Integer = DateTime.Today.Month

    '変数mを使用した処理
End With

但し、この場合mだけでなく他の変数スコープも狭くなりますので注意が必要です。

インデックス付きSelectメソッド

コレクションの要素を処理する際、ナンバリングが必要なことがたまにあります。
通常であれば、以下のようにfor文を使ってナンバリングするのでしょうが・・・。

string[] values = { "京都", "奈良", "大阪", "兵庫", "滋賀", "和歌山" };
var res = new List<string>(); 
for (int i= 0; i < values.Count(); ++i) {
    res.Add(i.ToString() + ":" + values[i]);
}
Console.WriteLine(String.Join(",", res));        
Dim values As String() = {"京都", "奈良", "大阪", "兵庫", "滋賀", "和歌山"}
Dim res = New List(Of String)()
For i As Integer = 0 To values.Count() - 1
    res.Add(i.ToString() & ":" & values(i))
Next
Console.WriteLine(String.Join(",", res))
0:京都,1:奈良,2:大阪,3:兵庫,4:滋賀,5:和歌山



LINQのSelectメソッドはインデックスを射影するオーバーロードがあります。

var res = values.Select((x, index) => index.ToString() + ":" + x).ToArray();
Console.WriteLine(String.Join(",", res));
Dim res = values.Select(Function(x, index) index.ToString() & ":" & x).ToArray()
Console.WriteLine(String.Join(",", res))



インデックスを利用して、Whereメソッドに応用することも可能です。

また、インデックスはユニークですので、これを利用してDictionaryクラスに変換することも可能です。

var dic = values1.Select((x, index) => new { index, x }).ToDictionary(x => x.index, x => x.x);
Dim dic = values1.Select(Function(x, index) New With {index, x}).ToDictionary(Function(x) x.index, Function(x) x.x)