Przykładowy kod dokonujący grupowania w LINQ:
class TestObj
{
private static System.Random s_random = new System.Random();
public int Value1 = s_random.Next(5);
public int Value2 = s_random.Next(5);
public int Value3 = s_random.Next(5);
public void Operation()
{
System.Console.WriteLine("v1: {0}, v2: {1}, v3: {2}", Value1, Value2, Value3);
}
}
public void Test()
{
var obj_list = from el in Enumerable.Range(0, 10)
select new TestObj();
var grouped = from obj in obj_list
group obj by obj.Value1;
grouped.ForEach(g =>
{
System.Console.WriteLine("Grupa:");
g.ForEach(obj => obj.Operation());
});
}
Przykładowy wynik:
Grupa: v1: 4, v2: 4, v3: 2 v1: 4, v2: 1, v3: 1 v1: 4, v2: 4, v3: 1 Grupa: v1: 2, v2: 0, v3: 3 v1: 2, v2: 4, v3: 0 v1: 2, v2: 3, v3: 0 Grupa: v1: 1, v2: 1, v3: 0 v1: 1, v2: 2, v3: 3 Grupa: v1: 0, v2: 1, v3: 3 v1: 0, v2: 3, v3: 1
Pytanie brzmi jak pogrupować najpierw po
Value1
, a później po
Value2
. Możemy to zrobić sposób bardziej skomplikowany:
public void Test()
{
var obj_list = from el in Enumerable.Range(0, 10)
select new TestObj();
var grouped = from gr1 in
(from obj1 in obj_list
group obj1 by obj1.Value1 into g1
select from obj2 in g1
group obj2 by obj2.Value2)
from gr2 in gr1
select gr2;
grouped.ForEach(g =>
{
System.Console.WriteLine("Grupa:");
g.ForEach(obj => obj.Operation());
});
}
Albo prostszy:
public void Test()
{
var obj_list = from el in Enumerable.Range(0, 10)
select new TestObj();
var grouped = from obj in obj_list
group obj by new { obj.Value1, obj.Value2 };
grouped.ForEach(g =>
{
System.Console.WriteLine("Grupa:");
g.ForEach(o => o.Operation());
});
}
Przykładowe wyniki:
Grupa: v1: 1, v2: 2, v3: 3 Grupa: v1: 2, v2: 3, v3: 3 Grupa: v1: 3, v2: 1, v3: 3 Grupa: v1: 0, v2: 1, v3: 0 Grupa: v1: 1, v2: 3, v3: 4 v1: 1, v2: 3, v3: 1 v1: 1, v2: 3, v3: 4 Grupa: v1: 2, v2: 2, v3: 4 Grupa: v1: 4, v2: 4, v3: 1 Grupa: v1: 2, v2: 0, v3: 1
Takie coś też jest możliwe:
public void Test()public void Test()
{
var obj_list = from el in Enumerable.Range(0, 10)
select new TestObj();
var grouped = from obj in obj_list
group obj by new { obj.Value1, v = obj.Value2 + obj.Value3};
grouped.ForEach(g =>
{
System.Console.WriteLine("Grupa:");
g.ForEach(o => o.Operation());
});
}
Przykładowe wyniki:
Grupa: v1: 4, v2: 2, v3: 4 v1: 4, v2: 4, v3: 2 Grupa: v1: 2, v2: 0, v3: 3 Grupa: v1: 1, v2: 2, v3: 1 Grupa: v1: 4, v2: 0, v3: 1 Grupa: v1: 4, v2: 4, v3: 4 Grupa: v1: 2, v2: 1, v3: 1 Grupa: v1: 0, v2: 4, v3: 3 Grupa: v1: 2, v2: 2, v3: 3 Grupa: v1: 0, v2: 3, v3: 0
Wygląda na to, że w w części
group
możemy dodać co nam się żywnie podoba. Np. takie coś kompiluje się poprawnie, ale nie daje prawidłowych wyników grupowania:
class TestObj
{
private static System.Random s_random = new System.Random();
public int Value1 = s_random.Next(5);
public int Value2 = s_random.Next(5);
public int Value3 = s_random.Next(5);
public void Operation()
{
System.Console.WriteLine("v1: {0}, v2: {1}, v3: {2}", Value1, Value2, Value3);
}
}
public class T
{
private static int counter = 0;
public T()
{
counter++;
System.Console.WriteLine(counter);
}
public int v1;
public int v23;
}
public void Test()
{
var obj_list = from el in Enumerable.Range(0, 10)
select new TestObj();
var grouped = from obj in obj_list
group obj by new T() { v1 = obj.Value1, v23 = obj.Value2 + obj.Value3 };
grouped.ForEach(g =>
{
System.Console.WriteLine("Grupa \t\t\t\t\t v1={0}, v2={1}", g.Key.v1, g.Key.v23);
g.ForEach(o => o.Operation());
});
}
Przykładowy wynik:
1 2 3 4 5 6 7 8 9 10 Grupa v1=0, v2=5 v1: 0, v2: 2, v3: 3 Grupa v1=1, v2=3 v1: 1, v2: 2, v3: 1 Grupa v1=3, v2=2 v1: 3, v2: 1, v3: 1 Grupa v1=4, v2=4 v1: 4, v2: 1, v3: 3 Grupa v1=0, v2=6 v1: 0, v2: 2, v3: 4 Grupa v1=0, v2=4 v1: 0, v2: 4, v3: 0 Grupa v1=1, v2=2 v1: 1, v2: 0, v3: 2 Grupa v1=0, v2=1 v1: 0, v2: 0, v3: 1 Grupa v1=4, v2=5 v1: 4, v2: 2, v3: 3 Grupa v1=4, v2=4 v1: 4, v2: 2, v3: 2
Jak widać grupowanie nie działa. A przy okazji widać, że grupowanie dla każdego obiektu tworzy obiekt po którym grupuje. Jest on później dołączany do wyniku jako klucz. Pytanie brzmi dlaczego w tym przypadku grupowanie nie powiodło się. W tym celu musimy posłużyć się programem
Reflector. Preparujemy sobie tego rodzaju kod:
class TestObj
{
private static System.Random s_random = new System.Random();
public int Value1 = s_random.Next(5);
public int Value2 = s_random.Next(5);
public int Value3 = s_random.Next(5);
public void Operation()
{
System.Console.WriteLine("v1: {0}, v2: {1}, v3: {2}", Value1, Value2, Value3);
}
}
public class KeyT
{
public int v1;
public int v23;
}
public void Test()
{
List obj_list = (from el in Enumerable.Range(0, 10)
select new TestObj()).ToList();
var grouped1 = from obj in obj_list
group obj by new KeyT() { v1 = obj.Value1, v23 = obj.Value2 + obj.Value3 };
var grouped2 = from obj in obj_list
group obj by new { v1 = obj.Value1, v23 = obj.Value2 + obj.Value3 };
grouped1.ForEach(g =>
{
System.Console.WriteLine("Grupa \t\t\t\t\t v1={0}, v2={1}", g.Key.v1, g.Key.v23);
g.ForEach(o => o.Operation());
});
System.Console.WriteLine("-------------------------------------------");
grouped2.ForEach(g =>
{
System.Console.WriteLine("Grupa \t\t\t\t\t v1={0}, v2={1}", g.Key.v1, g.Key.v23);
g.ForEach(o => o.Operation());
});
}
Kompilujemy przykład i otwieramy w Reflectorze. I... naprawdę ciężko się połapać. Po ustawieniu w opcjach platformy na
.NET 2.0
i wybrania jako języka dekompilacji C# otrzymujemy dla metody
Test()
:
public void Test()
{
IEnumerable<TestObj> obj_list = Enumerable.Range(0, 10).Select<int, TestObj>(delegate (int el) {
return new TestObj();
});
IEnumerable<IGrouping<KeyT, TestObj>> grouped1 = obj_list.GroupBy<TestObj, KeyT>(delegate (TestObj obj) {
return new KeyT { v1 = obj.Value1, v23 = obj.Value2 + obj.Value3 };
});
var grouped2 = obj_list.GroupBy(delegate (TestObj obj) {
return new { v1 = obj.Value1, v23 = obj.Value2 + obj.Value3 };
});
grouped1.ForEach<IGrouping<KeyT, TestObj>>(delegate (IGrouping<KeyT, TestObj> g) {
Console.WriteLine("Grupa \t\t\t\t\t v1={0}, v2={1}", g.Key.v1, g.Key.v23);
g.ForEach<TestObj>(delegate (TestObj o) {
o.Operation();
});
});
Console.WriteLine("-------------------------------------------");
grouped2.ForEach(delegate (IGrouping<<>f__AnonymousType0<int, int>, TestObj> g) {
Console.WriteLine("Grupa \t\t\t\t\t v1={0}, v2={1}", g.Key.v1, g.Key.v23);
g.ForEach<TestObj>(delegate (TestObj o) {
o.Operation();
});
});
}
Gdybyśmy wybrali platformę
.NET 3.5
to niezależnie czy w źródłach używamy LINQ, czy zapisu takiego jak w dekompilacji Reflector zawsze pokaże nam zapis LINQ. Reflector nie tylko może zdekompilować nasze źródła, ale także te Microsoftowe. Czas dowiedzieć się co się tak naprawdę dzieje w metodzie
GroupBy
. Choć mamy dwa podejścia do grupowania, dowiedzenie się o co chodzi z Reflectora jest prawie nie możliwe. Tak naprawdę kod wykonujący oba grupowania jest taki sam. Różnią się tylko klucze grupowania. Po przeanalizowaniu źródeł w Reflectorze, pierwsze czego spróbowałem to dostarczenie własnego obiektu implementującego
IEqualityComparer
.
class TestObj
{
private static System.Random s_random = new System.Random();
public int Value1 = s_random.Next(5);
public int Value2 = s_random.Next(5);
public int Value3 = s_random.Next(5);
public void Operation()
{
System.Console.WriteLine("v1: {0}, v2: {1}, v3: {2}", Value1, Value2, Value3);
}
}
public class KeyT
{
public int v1;
public int v23;
}
public class KeyComparer : IEqualityComparer<KeyT>
{
public bool Equals(KeyT x, KeyT y)
{
if (x == null)
return false;
if (y == null)
return false;
return (x.v1 == y.v1) && (x.v23 == y.v23);
}
public int GetHashCode(KeyT obj)
{
return obj.v1.GetHashCode() + obj.v23.GetHashCode();
}
}
public void Test()
{
List obj_list = (from el in Enumerable.Range(0, 10)
select new TestObj()).ToList();
//var grouped1 = from obj in obj_list
// group obj by new T() { v1 = obj.Value1, v23 = obj.Value2 + obj.Value3 };
var grouped1 = obj_list.GroupBy(obj => new KeyT() { v1 = obj.Value1, v23 = obj.Value2 + obj.Value3 }, new KeyComparer());
var grouped2 = from obj in obj_list
group obj by new { v1 = obj.Value1, v23 = obj.Value2 + obj.Value3 };
grouped1.ForEach(g =>
{
System.Console.WriteLine("Grupa \t\t\t\t\t v1={0}, v2={1}", g.Key.v1, g.Key.v23);
g.ForEach(o => o.Operation());
});
System.Console.WriteLine("-------------------------------------------");
grouped2.ForEach(g =>
{
System.Console.WriteLine("Grupa \t\t\t\t\t v1={0}, v2={1}", g.Key.v1, g.Key.v23);
g.ForEach(o => o.Operation());
});
}
Oba podejścia zaczęły zwracać takie same wyniki. Różnica pomiędzy nimi rozstrzyga się tutaj:
private Lookup(IEqualityComparer<TKey> comparer)
{
if (comparer == null)
{
comparer = EqualityComparer<TKey>.Default;
}
this.comparer = comparer;
this.groupings = new Grouping<TKey, TElement>[7];
}
Dostarczając do
GroupBy
komparator stworzony jak wyżej dostaniemy także błędy w grupowaniu. Nie dostarczając komparatora, ale definiując obiekt klucza jako:
public class KeyT
{
public int v1;
public int v23;
public virtual int GetHashCode()
{
return v1.GetHashCode() + v23.GetHashCode();
}
public virtual bool Equals(object obj)
{
if (obj == this)
return true;
if (obj == null)
return false;
KeyT t = obj as KeyT;
if (t == null)
return false;
return (t.v1 == v1) && (t.v23 == v23);
}
}
Także wygenerujemy błędne wyniki. Wygląda na to, że jeśli klucz nie jest obiektem anonimowym to komparator musi być dostarczony. Zgodnie z działaniem tworzonego w metodzie
Lookup
komparatora wywołuje on właśnie metody
GetHashCode
i
Equals
klasy
KeyT
. Mimo to ustawiając na nich breakpointy, okazuje się, że nie są one wywoływane. Oto ostateczny przykład:
class TestObj
{
private static System.Random s_random = new System.Random();
public int Value1 = s_random.Next(2);
public int Value2 = s_random.Next(2);
public int Value3 = s_random.Next(2);
public void Operation()
{
System.Console.WriteLine("v1: {0}, v2: {1}, v3: {2}", Value1, Value2, Value3);
}
}
public class KeyT
{
public int v1;
public int v23;
public virtual int GetHashCode()
{
return v1.GetHashCode() + v23.GetHashCode();
}
public virtual bool Equals(object obj)
{
if (obj == this)
return true;
if (obj == null)
return false;
KeyT t = obj as KeyT;
if (t == null)
return false;
return (t.v1 == v1) && (t.v23 == v23);
}
}
[Serializable]
public class ObjectEqualityComparer_<T> : EqualityComparer<T>
{
// Methods
public override bool Equals(object obj)
{
ObjectEqualityComparer_<T> comparer = obj as ObjectEqualityComparer_<T>;
return (comparer != null);
}
public override bool Equals(T x, T y)
{
if (x != null)
{
return ((y != null) && x.Equals(y));
}
if (y != null)
{
return false;
}
return true;
}
public override int GetHashCode()
{
return base.GetType().Name.GetHashCode();
}
public override int GetHashCode(T obj)
{
if (obj == null)
{
return 0;
}
return obj.GetHashCode();
}
}
[Serializable]
public class ObjectEqualityComparer__ : EqualityComparer<KeyT>
{
public override bool Equals(object obj)
{
ObjectEqualityComparer__ comparer = obj as ObjectEqualityComparer__;
return (comparer != null);
}
public override bool Equals(KeyT x, KeyT y)
{
if (x != null)
{
return ((y != null) && x.Equals(y));
}
if (y != null)
{
return false;
}
return true;
}
public override int GetHashCode()
{
return base.GetType().Name.GetHashCode();
}
public override int GetHashCode(KeyT obj)
{
if (obj == null)
{
return 0;
}
return obj.GetHashCode();
}
}
public void Test()
{
List<TestObj> obj_list = (from el in Enumerable.Range(0, 10)
select new TestObj()).ToList();
var grouped1 = obj_list.GroupBy(obj => new KeyT() { v1 = obj.Value1, v23 = obj.Value2 + obj.Value3 });
var grouped2 = obj_list.GroupBy(obj => new KeyT() { v1 = obj.Value1, v23 = obj.Value2 + obj.Value3 }, new ObjectEqualityComparer_<KeyT>());
var grouped3 = obj_list.GroupBy(obj => new KeyT() { v1 = obj.Value1, v23 = obj.Value2 + obj.Value3 }, new ObjectEqualityComparer__());
var grouped4 = obj_list.GroupBy(obj => new { v1 = obj.Value1, v23 = obj.Value2 + obj.Value3 });
System.Console.WriteLine("grouped1: {0}", grouped1.Count());
System.Console.WriteLine("grouped2: {0}", grouped2.Count());
System.Console.WriteLine("grouped3: {0}", grouped3.Count());
System.Console.WriteLine("grouped4: {0}", grouped4.Count());
}
I teraz pytanie jakie będą wyniki.
ObjectEqualityComparer_
i
ObjectEqualityComparer_
zostały utworzone na podstawie źródeł orginalnego
ObjectEqualityComparer
. Przykładowy wyniki działania:
grouped1: 10 grouped2: 10 grouped3: 6 grouped4: 6
Jak więc widać użycie generycznego
ObjectEqualityComparer_
generuje błędne wyniki. Dlatego nie dostarczenie własnego komparatora dla
GroupBy
dawało błędne wyniki. Wystarczy popatrzeć jak w
Lookup
ten komparator jest tworzony. Czemu tak się dzieje nie wiem. Według mnie to jest jakiś bug. Generalnie używając jako klucza nie-anonimowo stworzonego obiektu, trzeba dla niego dostarczyć własny nie-generyczny komparator.