Extension do „Linq to Object”. Na przykładzie SplitIntoParts.

„Linq to Object” jest bardzo dobrze wyposażonym mechanizmem. Posiada wiele metod, które generalnie pokrywają 70% potrzeb podczas projektowania aplikacji. Jednak czasami potrzebujemy tych 30%, których nie ma. Poniżej pokazuje jak napisać własną metodę rozszerzeń (extension method) do Linq To Object na przykładzie autorskiej metody SplitIntoParts.

Aby napisać dobrą metodę rozszerzeń należy spełnić kilka warunków:

  • Metoda zawsze powinna sprawdzić parametry wejściowe i rzucić wyjątek ArgumentException jeśli trzeba. Tą praktykę stosuje również na co dzień podczas pisania zwykłych metod. Dzięki temu kod staje bardziej uniwersalny.
  • Dodatkowo należy sprawdzić sekwencję źródłową czy ma wartość różną od Null. Jeśli nie to należy rzucić wyjątek ArgumentNullException.
  • Jeśli sekwencja źródłowa jest pusta należy zgłosić InvalidOperationException.
  • Dodatkowo należy udostępnić wartiant operatora, którego nazwa będzie zawierać przyrostek OrDefault i nie będzie zgłaszać wyjątku InvalidOperationException, zamiast tego zwrócić wartość default(T).

Warunki te pochodzą prosto z kodu oryginalnego „Linq to Object” i są oparte o własnie ten kod. Kod stworzony oryginalnie przez Microsoft. To znaczy wszystkie metody w „Liqu to Object” spełniają powyższe warunki(no może nie wszystkie ; ) )

Poniżej znajduje się moja autorska metoda, która pozwala podzielić sekwencję źródłową IEnumerable<T>  na dowolną ilość części. Metoda zwraca tyle sekwencji IEnumerable<T> ile podaliśmy w parametrze. Ostatnia cześć sekwencji będzie zawierała większą liczbę elementów w zależności czy sekwencje udało się podzielić równo na części czy była jakaś reszta (modulo)

Poniżej kod.

public static class ExtensionMethod
    {
        public static IEnumerable<IEnumerable<T>> SplitInToParts<T>(this IList<T> source, short parts)
        {
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            if (source.Count == 0)
            {
                throw new InvalidOperationException("Source cannot be empty");
            }

            if (parts == 0)
            {
                throw new ArgumentException("Cannot divide by zero.", "parts");
            }

            var totalRows = source.Count;

            var skipRows = 0;

            var takeRows = totalRows / parts;

            for (var part= 0; part< parts; part++)
            {
                var partialList = new List<T>();

                partialList.AddRange(source.Skip(skipRows).Take(takeRows));

                skipRows += takeRows;

                if (part == parts - 2)
                {
                    takeRows = totalRows - skipRows;
                }

                yield return partialList;
            }

        }
    }

Metody rozszerzeń muszą być napisane w statyczniej klasie. Metoda również musi być statyczna a pierwszym parametrem musi być sekwencja na której pracujemy poprzedzona słowem kluczowym this.

Na początku sprawdzamy warunki argumentów wejściowych. Potem sprawdzamy dzielenie przez zero.

Następnie wykonywany jest algorytm dzielenia na części, który najpierw dzieli ilość elementów sekwencji źródłowej na równe części a następnie tworzy listę elementów przy użyciu metod skip i take.

Kolejnym krokiem jest zwrócenie elementów przez słowo kluczowe  „yield return” w metodzie.

Dzięki „yield return” metoda się nie kończy ale zatrzymuje aż do kolejnego żądania o następny element na przykład przez pętle foreach.

Warunek if sprawdza czy jest to przedostatnia cześć do zwrócenia. Jeśli tak to dodaje do ilości zwróconych elementów tą część elementów, która była nie podzielna.

Poniżej przykład testu, który jasno pokazuje o co chodzi w metodzie SplitIntoParts.  [UPDATE] „Mad” dzięki za uwagi. Poprawiłem test wg Twoich uwag aby był bardziej przejrzysty. Poniżej nowa wersja. Powinna być bardziej jasna dla wszystkich.

 [TestMethod]
        public void Split4()
        {
              int[] source = { 1, 2, 3, 4, 5 };

            IEnumerable<IEnumerable<int>> result = source.SplitInToParts(3);

            IEnumerable<IEnumerable<int>> expected = new List<IEnumerable<int>>
            {
                new List<int> { 1 },
                new List<int> { 2 },
                new List<int> { 3, 4, 5 }
            };

            for (var i = 0; i < result.Count(); i++)
            {
                CollectionAssert.AreEqual(result.ToList()[i].ToList(), expected.ToList()[i].ToList());
            }
        }

Test tworzy listę 5 elementów (integerów). Następnie dzieli tą listę na 3 części. Czyli 5 / 3 = 1 . W ostatniej części zostają dodane dwa elementy 4 i 5.

Ważnym pytanie na, które należy sobie postawić przy projektowania metod rozszerzeń jest wydajność.Pamiętajmy, że z definicji takie metody mogą być używane przez inne osoby, które nie będą się zastanawiały nad tym czy metoda jest optymalna, szybka i czy ma obsługę błędów.

Należy zawsze dołożyć wszelkich starań aby metoda rozszerzeń była dobrze zaprojektowania.

Jeśli macie jakiś uwagi do tej powyżej  zapraszam do komentowania do każdej się odniosę.

 

7 przemyśleń nt. „Extension do „Linq to Object”. Na przykładzie SplitIntoParts.

  1. Nie mogę się zgodzić z niektórymi rzeczami.

    1. „Jeśli sekwencja źródłowa jest pusta należy zgłosić InvalidOperationException” – tylko jeśli ma to sens. Pusta kolekcja to często spodziewany rezultat. Moim zdaniem dzielenie pustej listy na części powinno dać puste listy lub pustą listę list.

    2. Metoda, którą napisałeś nie jest intuicyjna. Dzieląc listę [1] na pół spodziewam się [[1], []] (u Ciebie będzie [[], [1]]), natomiast dzieląc [1, 2, 3] spodziewam się [[1,2], [3]] itp. Żadna lista z list podrzędnych nie będzie dłuższa od innej listy o więcej niż 1 element. W Twoim przykładzie długość 7 i 9 wygląda dziwnie.

    3. Test nie jest dobry pod względem czytelności i tego co ma sprawdzać. OutputListResult wprowadza w błąd, bo nie jest to spodziewany rezultat dzielenia, a spodziewane długości podrzędnych list. Poza tym nie widać od razu o co chodzi. Napisałem przykładowy test z użyciem NUnita:

    // Arrange
    int[] source = { 1, 2, 3, 4, 5 };
    int[][] expected = { new[] { 1, 2 }, new[] { 3, 4 }, new[] { 5 } };

    // Act
    IEnumerable<IEnumerable> result = source.SplitInToParts(3);

    // Assert
    CollectionAssert.AreEqual(expected, result);

    1. Punkt 1 jest wyjaśniony w tekscie. Cytuje: „Dodatkowo należy udostępnić wartiant operatora, którego nazwa będzie zawierać przyrostek OrDefault i nie będzie zgłaszać wyjątku InvalidOperationException, zamiast tego zwrócić wartość default(T)” więc to użytkownik decyduje jaką woli postać.

      1. Punkt 2: Napisałem tak, bo wydawało mi się to logiczne pod względem potrzeb jakie miałem. Ale faktycznie tak jak piszesz strasznie to nie intuicyjne jak na to teraz patrze. Zmienię to!

Możliwość komentowania jest wyłączona.