2015年11月19日木曜日

.NET(VB) LINQをVBで使用する際に注意すること

LINQはC#でしか書いたことがないので、VBのLINQの書き方がわかりません。
来年からしばらくはVBでの開発になるので、VBでのLINQの書き方を調べてみました。

VBでLINQを書く際の注意点です。
他にもあるかもしれませんが、とりあえず以下の2点。
  • FunctionとSubを書き分ける必要がある。
  • 匿名クラスを集計やグループ化のキーにする場合は、Keyキーワードを使用する必要がある。

以下のようなテストデータで説明します。
【C#】
private class Fruit
{
    public string Name { get; set; }
    public string Rank { get; set; }
    public decimal Price { get; set; }
}

var fruits = new List<Fruit>()
                {
                    new Fruit(){Name = "りんご", Rank = "A" , Price = 1000 },
                    new Fruit(){Name = "みかん", Rank = "A" , Price = 600 },
                    new Fruit(){Name = "ぶどう", Rank = "B" , Price = 1200 },
                    new Fruit(){Name = "りんご", Rank = "B" , Price = 800 },
                    new Fruit(){Name = "みかん", Rank = "A" , Price = 500 }

                };
【VB】
Private Class Fruit
    Public Property Name As String
    Public Property Rank As String
    Public Property Price As Decimal
End Class

Dim fruits = New List(Of Fruit)() From
            {
                New Fruit() With {.Name = "りんご", .Rank = "A", .Price = 1000},
                New Fruit() With {.Name = "みかん", .Rank = "A", .Price = 600},
                New Fruit() With {.Name = "ぶどう", .Rank = "B", .Price = 1200},
                New Fruit() With {.Name = "りんご", .Rank = "B", .Price = 800},
                New Fruit() With {.Name = "みかん", .Rank = "A", .Price = 500}
            }

FunctionとSubを書き分ける必要がある


C#では以下のように何も考えずに 「 => 」と書けばよかったのですが
var nameRankList = fruits.Select(itm => new { itm.Name, itm.Rank });

ameRankList.ToList().ForEach(itm =>
            { Console.WriteLine(string.Format("{0} {1}", itm.Name, itm.Rank)); });
VBでは以下のように値を返すものは Function で、値を返さないものは Sub で書き分ける必要があります。
'値を返すものはFunctionで
Dim nameRankList = fruits.Select(Function(itm) New With {itm.Name, itm.Rank})

'値を返さないものはSub
nameRankList.ToList().ForEach(Sub(itm) _
        Console.WriteLine(String.Format("{0} {1}", itm.Name, itm.Rank)))

Subと書くところを間違えてFunctionと書いてもエラーにならないようです。
ソース元はコチラ → 「C#.NET vs VB.NET 」VB.NET Action デリゲート型に Function ラムダ式を代入
こわいですねぇ
ちょと試してみましょうw

Form上にあるコントロールをすべて無効にするコードです。
Me.Controls.Cast(Of System.Windows.Forms.Control)().ToList().ForEach(Sub(itm) itm.Enabled = False)
Functionに変えてもエラーは出ません。もちろんコントロールも無効になりません。
Me.Controls.Cast(Of System.Windows.Forms.Control)().ToList().ForEach(Function(itm) itm.Enabled = False)

匿名クラスを集計やグループ化のキーにする場合は、Keyキーワードを使用する必要がある


まずC#で、果物名とランクでGroupByしてみます。
var nameList = fruits.GroupBy(itm => new { itm.Name, itm.Rank });
//--出力
//りんご A
//みかん A
//ぶどう B
//りんご B
nameList.ToList().ForEach(grp => { Console.WriteLine("{0} {1}",grp.Key.Name, grp.Key.Rank); });
次にVBのコードです。(NGパターン)
C#と同じ感覚で書くと、以下のようなコードになるかと思います。
出力結果からわかるように全然グループ化されていません。
Dim nameList = fruits.GroupBy(Function(itm) New With {itm.Name, itm.Rank})
'--出力
'りんご A
'みかん A
'ぶどう B
'りんご B
'みかん A
nameList.ToList().ForEach(Sub(grp) Console.WriteLine("{0} {1}", grp.Key.Name, grp.Key.Rank))
VBで匿名クラスをグループ化のキーに使用する場合、
キーに使用するプロパティの前に「Key」キーワードをつける必要があります。


匿名クラスのNameプロパティの前だけにKeyキーワードをつけてみます。
結果は名称だけでグループ化されます。
Dim nameList = fruits.GroupBy(Function(itm) New With {Key itm.Name, itm.Rank})
'--出力
'りんご A
'みかん A
'ぶどう B
nameList.ToList().ForEach(Sub(grp) Console.WriteLine("{0} {1}", grp.Key.Name, grp.Key.Rank))
匿名クラスのNameプロパティ、Rankプロパティの前にKeyキーワードをつけると、名称とランクでグループ化されます。
Dim nameList = fruits.GroupBy(Function(itm) New With {Key itm.Name, Key itm.Rank})
'--出力
'りんご A
'みかん A
'ぶどう B
'りんご B
nameList.ToList().ForEach(Sub(grp) Console.WriteLine("{0} {1}", grp.Key.Name, grp.Key.Rank))
うぅ~ん・・・ナンダカナァ

以下のコードはSelectで果物リストから名称とランクを抽出してます。
結果も予想通りです。
Dim nameList = fruits.Select(Function(itm) New With {itm.Name, itm.Rank})
'--出力
'りんご A
'みかん A
'ぶどう B
'りんご B
'みかん A
nameList.ToList().ForEach(Sub(itm) Console.WriteLine("{0} {1}", itm.Name, itm.Rank))
で重複を除きたいんでDistinctをくっつけると、ある意味予想通りですが、 結果は重複が除去されていません。
Dim nameList = fruits.Select(Function(itm) New With {itm.Name, itm.Rank}).Distinct()
'--出力
'りんご A
'みかん A
'ぶどう B
'りんご B
'みかん A
nameList.ToList().ForEach(Sub(itm) Console.WriteLine("{0} {1}", itm.Name, itm.Rank))
ここでも、匿名クラスの各プロパティの前にKeyキーワードを付けないといけません。
Dim nameList = fruits.Select(Function(itm) New With {Key itm.Name, Key itm.Rank}).Distinct()
'--出力
'りんご A
'みかん A
'ぶどう B
'りんご B
nameList.ToList().ForEach(Sub(itm) Console.WriteLine("{0} {1}", itm.Name, itm.Rank))
C#の匿名クラスは、インスタンス作成後に値を変更することができないイミュータブルなオブジェクトで
VBの匿名クラスは、インスタンス作成後に値を変更することができるミュータブルなオブジェクトなんだそうです。
ミュータブルなオブジェクトなので、VBではKeyキーワードを付けることによってequalsメソッドとgetHashcodeメソッドがオーバーライドされ、オブジェクトが等しいか判定しているんですね。
詳しくはコチラ → かるあ のメモ Key キーワードでは GetHashCode と Equals がオーバーライドされるみたい

つまり、匿名クラスのオブジェクトが同じかどうかを判定したいプロパティに、Keyキーワードをつけなければいけないということです。
以下のコードでは、匿名クラスオブジェクトの犬、猫、鳥は、同じポチという名前ですが、Keyキーワードを付けているプロパティが鳥だけ違います。
equalsメソッドでそれぞれのオブジェクトを比較すると、Keyが付いているプロパティの値が同じであれば、等価と判定されているのがわかります。
Dim dog = New With {Key .Name = "ポチ", .Type = "犬", .Squeak = "わんわん"}
Dim cat = New With {Key .Name = "ポチ", .Type = "猫", .Squeak = "にゃぁ"}
Dim bird = New With {.Name = "ポチ", Key .Type = "鳥", .Squeak = "ちゅんちゅん"}

'--出力
'犬と猫は等価
Console.WriteLine("犬と猫は{0}", (If(dog.Equals(cat), "等価", "等価でない")))

'--出力
'犬と鳥は等価でない
Console.WriteLine("犬と鳥は{0}", (If(dog.Equals(bird), "等価", "等価でない")))

0 件のコメント: