List(Of T)の任意のプロパティが最大の要素を取り出したい

タグの編集
投稿者 ジョージ  (社会人) 投稿日時 2012/1/18 13:27:18
教えてください。
あるクラスclass1というものがあるものとします。
ここで、このクラスclass1のリストList(Of class1)があり、この中にあるclass1インスタンスのうち
class1のプロパティAが最大のもの(仮にAの型はIntegerとします)を取得したいのですが、
とりあえず、今は(リストが空でないという前提ですが)リストのソートをプロパティAの降順で行い、インデックスが0のものを取得するようにしていますが、
もっとスマートに、1行で該当インスタンスを取得する方法はないでしょうか?
Linqとか、そういう方法を使うのかもしれませんが、勉強不足でよく理解できません。
この機会に色々と勉強したいと思いますので、どうぞよろしくお願いします。

VB2008を使っています。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2012/1/18 14:41:16
> リストのソートをプロパティAの降順で行い、インデックスが0のものを取得
Private data As New List(Of Class1)()
だとして、こうかな。他の書き方もできますけど。
Dim result As Class1 = data.OrderByDescending(Function(c) c.A).FirstOrDefault()
投稿者 ジョージ  (社会人) 投稿日時 2012/1/18 15:16:04
魔界の仮面弁士さま、ご回答ありがとうございました。
教えて頂いた方法でうまくいきました。
また、リストが空の場合は、サンプルのresultがNothingになることも確認しました。
ありがとうございました。

後学のため、その他の方法もお教え頂けますでしょうか?
どうぞよろしくお願いします。
投稿者 shu  (社会人) 投稿日時 2012/1/18 15:50:10
Dim MaxItm = SrcList.Aggregate(Function(itm1, itm2) If(itm1.A > itm2.A, itm1, itm2))

こんなんとか。
投稿者 shu  (社会人) 投稿日時 2012/1/18 15:55:04
上記は空だとエラーになります。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2012/1/18 18:34:08
> リストのソートをプロパティAの降順で行い、インデックスが0のものを取得
元のコードは、こういう感じだったのでしょうか。

data.Sort(Function(x, y) y.A.CompareTo(x.A))
Dim ret As Class1 = data(0)



> その他の方法もお教え頂けますでしょうか?
たとえば:

Dim ret0 As Class1 = data.DefaultIfEmpty().Aggregate(Function(x, y) If(x.A < y.A, y, x))
Dim ret1 As Class1 = data.OrderByDescending(Function(c) c.A).FirstOrDefault()
Dim ret2 As Class1 = (From c In data Order By c.A Descending).FirstOrDefault()
Dim ret3 As Class1 = data.OrderBy(Function(c) c.A).LastOrDefault()
Dim ret4 As Class1 = (From c In data Order By c.A).LastOrDefault()
Dim ret6 As Class1 = data.Where(Function(x) x.A = data.Max(Function(y) y.A)).FirstOrDefault()
Dim ret7 As Class1 = (From x In data Where x.A = Aggregate y In data Into Max(y.A)).FirstOrDefault()

0 は shu さんのコードに少し手を加えて、
リストが 0 件だった場合に Nothing を返すようにしたものです。

1 は先の私のコードですね。2,4はクエリ構文、1,3 はメソッド構文です。
1と2 は同じ処理です(降順の先頭項目)。
同様に、3と4も同じ処理を意味します(昇順の最終項目。

6と7 も結果は同じですが、処理手順が異なります。こちらは
最大値の調査とクラスの取り出しとで別の工程になっているため、
A が最大となるインスタンスが複数あった場合に、それらすべてを
取り出すような場合に使った方が良いでしょう。たとえばこんな感じです。

'ToArray(あるいはToList)するかどうかはお好みで。 
Dim list1() As Class1 = data.Where(Function(x) x.A = data.Max(Function(y) y.A)).ToArray()
Dim list2() As Class1 = (From x In data Where x.A = Aggregate y In data Into Max(y.A)).ToArray()
投稿者 ジョージ  (社会人) 投稿日時 2012/1/19 09:38:43
shuさま、魔界の仮面弁士さま、ご回答ありがとうございました。
色々な方法があるのですね。
これらにつき勉強していきます。

ありがとうございました。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2012/1/19 11:39:24
少し補足。というか訂正です。

先に提示した
Dim list1() As Class1 = data.Where(Function(x) x.A = data.Max(Function(y) y.A)).ToArray()
Dim list2() As Class1 = (From x In data Where x.A = Aggregate y In data Into Max(y.A)).ToArray()
の方式は、処理効率がかなり悪いので避けた方がよさそうです。

これは上記のパイプライン処理が、最大値を何度も算出させてしまうためであり、いわば
Dim list3 As New List(Of Class1)()
For Each x In data
 If x.A = GetMax(data) Then  '最大値の取得 
  list3.Add( x )
 End If
Next
に相当する処理が実行されてしまうためです。

この場合、元のデータ件数が n 個あったとすれば、上記の処理によって
プロパティA が「n×(n+1)回」も走査されることになります。
5件あったら30回ですし、1000件あったら百万回以上です。


最大値の取得は一回だけ行われれば十分なため、これを
Dim list4 As New List(Of Class1)()
Dim maxValue As Integer = GetMax(data)  '最大値の取得 
For Each x In data
 If x.A = maxValue Then
  list3.Add( x )
 End If
Next

のように処理すれば、走査回数を「n×2回」にまで減らすことができます。
5件で10回ですし、1000件でも2000回で済みます。


これに相当する Linq は、たとえばこんな感じで書けます。(2 行になってしまいますが…)
Dim maxValue As Integer = (From x In data Select x.A).Max()
Dim list5() As Class1 = data.Where(Function(x) x.A = maxValue).ToArray()
Dim list6() As Class1 = (From x In data Where x.A = maxValue).ToArray()


# 1 行で済ませられないかな…。
投稿者 ジョージ   (社会人) 投稿日時 2012/1/19 11:49:53
魔界の仮面弁士さま、補足ありがとうございました。
今後ともよろしくお願い申し上げます。