2009-10-11

LINQ - rozszerzenie Except

Oto proste uzupełnienie metody Except(). Bardzo często jej szukam w liście podpowiadającej tak więc teraz jest:
public static IEnumerable<T> Except<T>(this IEnumerable<T> a_enumerable, T a_element)
{
    foreach (T ele in a_enumerable)
    {
        if (!ele.Equals(a_element))
            yield return ele;
    }
}

Uproszczona inicjalizacja kolekcji

Przykład:
public void Test()
{
    List ar1 = new List { new Point(3, 4), new Point(5, 6) };
    ar1.ToString();
}
Kod IL:
.method public hidebysig instance void Test() cil managed
{
    .maxstack 4
    .locals init (
        [0] class [mscorlib]System.Collections.Generic.List`1<valuetype [System.Drawing]System.Drawing.Point> ar1,
        [1] class [mscorlib]System.Collections.Generic.List`1<valuetype [System.Drawing]System.Drawing.Point> <>g__initLocal2d5)
    L_0000: newobj instance void [mscorlib]System.Collections.Generic.List`1<valuetype [System.Drawing]System.Drawing.Point>::.ctor()
    L_0005: stloc.1 
    L_0006: ldloc.1 
    L_0007: ldc.i4.3 
    L_0008: ldc.i4.4 
    L_0009: newobj instance void [System.Drawing]System.Drawing.Point::.ctor(int32, int32)
    L_000e: callvirt instance void [mscorlib]System.Collections.Generic.List`1<valuetype [System.Drawing]System.Drawing.Point>::Add(!0)
    L_0013: ldloc.1 
    L_0014: ldc.i4.5 
    L_0015: ldc.i4.6 
    L_0016: newobj instance void [System.Drawing]System.Drawing.Point::.ctor(int32, int32)
    L_001b: callvirt instance void [mscorlib]System.Collections.Generic.List`1<valuetype [System.Drawing]System.Drawing.Point>::Add(!0)
    L_0020: ldloc.1 
    L_0021: stloc.0 
    L_0022: ldloc.0 
    L_0023: callvirt instance string [mscorlib]System.Object::ToString()
    L_0028: pop 
    L_0029: ret 
}
Widzimy, że inicjalizacja odbywa się poprzez wywołanie metody Add obiektu. Cała konstrukcja listy odbywa się poprzez zmienną tymczasową, dzięki czemu proces ten jest atomowy. Zauważmy też że podczas konstrukcji obiektu nie musimy pisać new List(). Jakie warunki musi spełniać klasa, by użycie takiej inicjalizacji było możliwe? Warunki są dwa. Po pierwsze klasa musi implementować interfejs System.Collections.IEnumerable. Po drugie musi zawierać metodę Add(). Czemu samo posiadanie przez klase metody Add() nie wystarczy nie wiem. Skoro już dano takie ograniczenie o wiele rozsądniejsze było by wymaganie implementacji interfejsu ICollection, który wymaga implementacji metody Add(). Przykład własnej klasy, która pozwala nam na skróconą inicjalizację kolekcji:
class TestA : IEnumerable
{
    public void Add(int a)
    {
    }

    public IEnumerator GetEnumerator()
    {
        throw new NotImplementedException();
    }
}

public void Test()
{
    TestA testa = new TestA { 1, 2 };
    testa.ToString();
}
Wariacje na temat typu parametru metody Add() sobie odpuścimy. Wygląda na to, że musi istnieć bezpośrednia konwersja pomiędzy parametrem metody, a składnikiem inicjalizacji. Teraz przyjrzyjmy się bliżej metodzie Add(). Oto przykład dla metody o dwóch parametrach:
class TestB : IEnumerable
{
    public void Add(int a, int b)
    {
    }

    public IEnumerator GetEnumerator()
    {
        throw new NotImplementedException();
    }
}

public void Test()
{
    TestB testb = new TestB { {1, 2}, {2, 3} };
    testb.ToString();
}
Jest o tyle ważne, że podobnie jak listę możemy inicjalizować słownik:
public void Test()
{
    Dictionary dict = new Dictionary() { { Color.Red, "Red" } };
    dict.ToString();
}

Trzy sposoby inicjalizacji tablicy

Oto one:
public void Test()
{
    int[] ar1 = { 1, 2, 3 };
    int[] ar2 = new [] { 1, 2, 3 };
    int[] ar3 = new int[] { 1, 2, 3 };
}
A czym się różnią ? Niczym:
.method public hidebysig instance void Test() cil managed
{
    .maxstack 3
    .locals init (
        [0] int32[] ar1,
        [1] int32[] ar2,
        [2] int32[] ar3)
    L_0000: nop 
    L_0001: ldc.i4.3 
    L_0002: newarr int32
    L_0007: dup 
    L_0008: ldtoken valuetype <PrivateImplementationDetails>{014E1F63-D1B2-44CE-A8EC-8570A162FE16}/__StaticArrayInitTypeSize=12 <PrivateImplementationDetails>{014E1F63-D1B2-44CE-A8EC-8570A162FE16}::$$method0x600009b-1
    L_000d: call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle)
    L_0012: stloc.0 
    L_0013: ldc.i4.3 
    L_0014: newarr int32
    L_0019: dup 
    L_001a: ldtoken valuetype <PrivateImplementationDetails>{014E1F63-D1B2-44CE-A8EC-8570A162FE16}/__StaticArrayInitTypeSize=12 <PrivateImplementationDetails>{014E1F63-D1B2-44CE-A8EC-8570A162FE16}::$$method0x600009b-2
    L_001f: call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle)
    L_0024: stloc.1 
    L_0025: ldc.i4.3 
    L_0026: newarr int32
    L_002b: dup 
    L_002c: ldtoken valuetype <PrivateImplementationDetails>{014E1F63-D1B2-44CE-A8EC-8570A162FE16}/__StaticArrayInitTypeSize=12 <PrivateImplementationDetails>{014E1F63-D1B2-44CE-A8EC-8570A162FE16}::$$method0x600009b-3
    L_0031: call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle)
    L_0036: stloc.2 
    L_0037: ret 
}
Przy okazji można tutaj powiedzieć, że zawartość tych tablic trzymana jest w specjalnych globalnych zmiennych statycznych (jako ciąg bajtów). I kiedy istnieje konieczność skonstruowania takiej tablicy kopiowany jest tylko obszar pamięci. Kolejny pzykład:
public void Test(int x)
{
    int[] ar = {1, x};
    ar.ToString();
}
Kod IL:
.method public hidebysig instance void Test(int32 x) cil managed
{
    .maxstack 3
    .locals init (
        [0] int32[] ar,
        [1] int32[] CS$0$0000)
    L_0000: ldc.i4.2 
    L_0001: newarr int32
    L_0006: stloc.1 
    L_0007: ldloc.1 
    L_0008: ldc.i4.0 
    L_0009: ldc.i4.1 
    L_000a: stelem.i4 
    L_000b: ldloc.1 
    L_000c: ldc.i4.1 
    L_000d: ldarg.1 
    L_000e: stelem.i4 
    L_000f: ldloc.1 
    L_0010: stloc.0 
    L_0011: ldloc.0 
    L_0012: callvirt instance string [mscorlib]System.Object::ToString()
    L_0017: pop 
    L_0018: ret 
}
Widzimy, że w tym przypadku tworzenie zainicjalizowanej tablicy odbywa się poprzez jej utworzenie i ustawianiu indeks po indeksie jej zawartości. Podczas całego tego procesu referencja do tablicy przechowywana jest w zmiennej pomocniczej, dzięki czemu cały proces widziany z zewnątrz jest atomowy. Tak samo (inicjalizacja element po elemencie) dzieje się w przypadku tablic struktur i obiektów.

LINQ - Przyspieszenie metody Count()

Metoda Count() zlicza nam ilość elementów w enumeracji. Bardzo często chcemy sprawdzić, czy nasz zbiór zawiera określoną ilość elementów. Nie ma wtedy sensu zliczać wszystkich elementów, musimy tylko zliczyć tyle ile nam potrzeba. Oto przykład:
var gr = from n in new int[] { 1, 1, 1, 2, 2 }
         where n == 1
         select n;

bool b1 = gr.Count() == 0;
bool b2 = gr.Count() != 0;
bool b3 = gr.Count() == 2;
bool b4 = gr.Count() != 2;
bool b5 = (gr.Count() >= 2) && (gr.Count() <= 3);
Wraz ze wzrostem ilości elementów w gr całość zaczyna działać coraz wolniej. Oczywiście możemy przekształcić gr w listę lub tablice, wtedy metoda Count() będzie działać błyskawicznie, ale będzie się to wiązało z kosztem czasowym (i pamięciowym) utworzeniem takiego obiektu (umiejętne wykorzystanie metody ToList() i ToArray() aby na pewnym etapie uzyskać wynik, którego już nie trzeba będzie ponownie wyliczać, także pozwala nam na przyspieszenie wyrażeń LINQ). Próbując opierać się tylko tym co dostarcza nam LINQ, powyższy przykład możemy przyspieszyć tak:
var gr = from n in new int[] { 1, 1, 1, 2, 2 }
         where n == 1
         select n;

bool b1 = !gr.Any();
bool b2 = gr.Any();
bool b3 = gr.Take(3).Count() == 2;
bool b4 = gr.Take(3).Count() != 2;
bool b5 = (gr.Take(2).Count() == 2) && (gr.Take(4).Count() <= 3);
Pierwsze dwa testy sprowadzają się do pobrania ze zbioru tylko jednego elementu. Test b3 i b4 wymaga pobrania tylko jednego więcej elementu niż jest wymagane (metoda Take() nie zwraca trzech elementów, zwraca enumerator, który zwróci co najwyżej trzy elementy). Test b5 może być w pesymistycznym przypadku tak wolny jak poprzednio, ale generalnie powinien być szybszy. Ciągle jednak możemy go przyspieszyć rezygnując z dwukrotnej enumeracji po zbiorze za pomocą poniższej metody:
public static bool IsCountInRange<T>(this IEnumerable<T> a_enumerable, int a_min, int a_max)
{
    Debug.Assert(a_min >= 0);
    Debug.Assert(a_max >= a_min);

    int count = 0;
    IEnumerator<T> enumerator = a_enumerable.GetEnumerator();

    while (enumerator.MoveNext())
    {
        count++;
        if (count > a_max)
            return false;
    }

    return (count >= a_min);
}
Przy jej pomocy powyższy przykład możemy zapisać:
var gr = from n in new int[] { 1, 1, 1, 2, 2 }
         where n == 1
         select n;

bool b1 = !gr.Any();
bool b2 = gr.Any();
bool b3 = gr.Take(3).Count() == 2;
bool b4 = gr.Take(3).Count() != 2;
bool b5 = gr.IsCountInRange(2, 3);
Metoda IsCountInRange() sprawdza, czy ilość elementów zawiera się w podanym zakresie. W przeciwieństwie do poprzedniego testu b5 tutaj enumeracja po zbiorze dokonywana jest jednokrotnie.