How can I split a generic List (of T) based on a property of a list member?
I have a generic List (of Foo) which contains n objects of Type Foo. One of t开发者_运维知识库he properties of Foo is PropertyA. PropertyA can be one of ValueA, ValueB or ValueC. Is there an easy way of splitting this into three seperate Lists, one for ValueA, one for ValueB and one for ValueC?
I can write some code which loops the original list and adds each item to a new list based on the property value but this does not seem to be very maintainable (what if I suddenly get a ValueD for example?)
**EDIT. I should have mentioned that I'm using version 2.0 of the framework.
In C# with .Net 2.0, I have written (too many times):
//if PropertyA is not int, change int to whatever that type is
Dictionary<int, List<foo>> myCollections =
new Dictionary<int, List<foo>>();
//
foreach(Foo myFoo in fooList)
{
//if I haven't seen this key before, make a new entry
if (!myCollections.ContainsKey(myFoo.PropertyA))
{
myCollections.Add(myFoo.PropertyA, new List<foo>());
}
//now add the value to the entry.
myCollections[myFoo.PropertyA].Add(myFoo);
}
//
// now recollect these lists into the result.
List<List<Foo>> result = new List<List<Foo>>();
foreach(List<Foo> someFoos in myCollections.Values)
{
result.Add(someFoos);
}
Nowadays, I just write:
List<List<foo>> result = fooList
.GroupBy(foo => foo.PropertyA)
.Select(g => g.ToList())
.ToList();
Or
ILookup<TypeOfPropertyA, foo>> result = fooList.ToLookup(foo => foo.PropertyA);
In C# I would write:
List<List<foo>> result = fooList
.GroupBy(foo => foo.PropertyA)
.Select(g => g.ToList())
.ToList();
If you want exactly 3 lists for valueA, valueB and valueC (even if one of them is empty):
Dim listA = (From x in myList Where x.PropertyA = ValueA).ToList()
Dim listB = (From x in myList Where x.PropertyA = ValueB).ToList()
...
Otherwise, use the GroupBy operator as suggested by others.
EDIT: Since you are using Framework 2.0, I guess you'll have to resort to your loop idea. A generic algorithm implementing GroupBy shouldn't be too difficult, though. Something along the lines of
Dim dic as New Dictionary(Of TypeOfYourValues, List(Of Foo))
For Each e As Foo In myList
If Not dic.ContainsKey(e.PropertyA) Then
dic(e.PropertyA) = New List(Of Foo)
End if
dic(e.PropertyA).Add(e)
Next
Then loop through the values of the dictionary.
You can use Enumerable.GroupBy:
var groupings = list.GroupBy(x => x.PropertyA);
foreach(var grouping in groupings)
{
// grouping.Key is the grouped value
foreach(var entry in grouping)
{
// process
}
}
See below the C# for the VB.Net version - note that there's one additional class (FooFinder) since there are no anonymous methods in VB.NET, so I needed something to be able to store the match state.
Here's a more "functional" way to accomplish the same thing, but still using C# 2.0 syntax. Note the important difference from other solutions (looping/dictionaries) is the use of the FindAll method on List, which will iterate over your collection and return all items for which the delegate returns true. C#:
using System;
using System.Collections.Generic;
namespace SplitList
{
class Program
{
class Foo
{
public Foo(string propertyA, int number)
{
_propertyA = propertyA;
_number = number;
}
private int _number;
private string _propertyA;
public string PropertyA
{
get { return _propertyA; }
}
public int Number
{
get { return _number; }
}
}
static void Main(string[] args)
{
List<Foo> foos = new List<Foo>();
foos.Add(new Foo("ValueA", 1));
foos.Add(new Foo("ValueA", 2));
foos.Add(new Foo("ValueA", 3));
foos.Add(new Foo("ValueA", 4));
foos.Add(new Foo("ValueB", 5));
foos.Add(new Foo("ValueB", 6));
foos.Add(new Foo("ValueC", 7));
foos.Add(new Foo("ValueC", 8));
foos.Add(new Foo("ValueC", 9));
List<Foo> aFoos = foos.FindAll(delegate(Foo f) { return f.PropertyA == "ValueA"; });
List<Foo> bFoos = foos.FindAll(delegate(Foo f) { return f.PropertyA == "ValueB"; });
List<Foo> cFoos = foos.FindAll(delegate(Foo f) { return f.PropertyA == "ValueC"; });
WriteFoos("ValueA", aFoos);
WriteFoos("ValueB", bFoos);
WriteFoos("ValueC", cFoos);
Console.ReadLine();
}
private static void WriteFoos(string propertyAValue, List<Foo> list)
{
Console.WriteLine("Group {0}:", propertyAValue);
list.ForEach(delegate(Foo f)
{
Console.WriteLine("Number:{0}, PropertyA:{1}", f.Number, f.PropertyA);
});
}
}
}
VB.NET:
Module Module1
Class FooFinder
Public Sub New(ByVal propertyAValue As String)
Me.PropertyAValue = propertyAValue
End Sub
Public ReadOnly PropertyAValue As String
Function Matches(ByVal f As Foo) As Boolean
Return (f.PropertyAValue = Me.PropertyAValue)
End Function
End Class
Class Foo
Public Sub New(ByVal propertyAValue As String, ByVal number As Integer)
_propertyAValue = propertyAValue
_number = number
End Sub
Private _propertyAValue As String
Private _number As Integer
Public Property PropertyAValue() As String
Get
Return _propertyAValue
End Get
Set(ByVal value As String)
_propertyAValue = value
End Set
End Property
Public Property Number() As Integer
Get
Return _number
End Get
Set(ByVal value As Integer)
_number = value
End Set
End Property
End Class
Sub Main()
Dim foos As New List(Of Foo)
foos.Add(New Foo("ValueA", 1))
foos.Add(New Foo("ValueA", 2))
foos.Add(New Foo("ValueA", 3))
foos.Add(New Foo("ValueB", 4))
foos.Add(New Foo("ValueB", 5))
foos.Add(New Foo("ValueC", 6))
foos.Add(New Foo("ValueC", 7))
foos.Add(New Foo("ValueC", 8))
foos.Add(New Foo("ValueC", 9))
Dim aFoos As List(Of Foo) = foos.FindAll(AddressOf New FooFinder("ValueA").Matches)
Dim bFoos As List(Of Foo) = foos.FindAll(AddressOf New FooFinder("ValueB").Matches)
Dim cFoos As List(Of Foo) = foos.FindAll(AddressOf New FooFinder("ValueC").Matches)
WriteFoos("ValueA", aFoos)
WriteFoos("ValueB", bFoos)
WriteFoos("ValueC", cFoos)
Console.ReadLine()
End Sub
Private Sub WriteFoos(ByVal propertyAValue As String, ByVal list As List(Of Foo))
Console.WriteLine("PropertyAValue:{0}", propertyAValue)
For Each f As Foo In list
Console.WriteLine("Number:{0}, PropertyAValue:{1}", f.Number, f.PropertyAValue)
Next
End Sub
End Module
var query = from foo in list
group foo by foo.PropertyA;
List<Foo> valueAGroup = query.First(g => g.Key == ValueA).ToList();
List<Foo> valueBGroup = query.First(g => g.Key == ValueB).ToList();
List<Foo> valueCGroup = query.First(g => g.Key == ValueC).ToList();
Or you could leave out the ToList()
calls if an IEnumerable<Foo>
is good enough.
If it is possible that for a ValueX there are no items for which PropertyA equals ValueX, then First
will throw an exception. In that case, it's a good idea to do this:
List<Foo> valueXGroup = (query.FirstOrDefault(g => g.Key == ValueX) ??
Enumerable.Empty<Foo>()).ToList();
This will give you an empty list instead of throwing an exception.
精彩评论