BETA
Aby się zalogować, najpiew wybierz portal.
Aby się zarejestrować, najpiew wybierz portal.
Podaj słowa kluczowe
Słowa kluczowe muszą mieć co najmniej 3 sąsiadujące znaki alfanumeryczne
Pole zawiera niedozwolone znaki

Baza wiedzy











Projektowanie źródeł danych LINQ (część 1)

27-02-2008 08:36 | Jacek Matulewski
Artykuł opisuje proces projektowania źródeł danych LINQ na przykładzie generatora liczb pseudolosowych. W pierwszym podejściu przygotowuję źródło implementujące interfejs IEnumerable. W takim przypadku filtrowanie i sortowanie zwracanej przez źródło kolekcji może być realizowane automatycznie. Kolejnym etapem jest wydzielenie osobnej klasy implementującej interfejs IQueryProvider i zamknięcie w niej wszystkich metod specyficznych dla konkretnego problemu. Dzięki temu można przygotować towarzyszą

Projektowanie
źródeł danych LINQ
(część 1)

Sposób implementacji klas pełniących funkcję źródeł danych LINQ zmienił się nieco w ostatecznej wersji platformy .NET 3.5. To spowodowało, że większość spośród nielicznych dostępnych w sieci opisów przestała być w pełni użyteczna. Postanowiłem zatem uzupełnić niniejszą książkę opisem prostego projektu źródła danych LINQ, które udostępniać będzie zbiór losowo generowanych liczb typu double lub int. Jest to przykład o tyle nietypowy, że nie odnosi się do zewnętrznego źródła danych, ale nie na tym polegać będą najpoważniejsze uproszczenia.

IEnumerable

Aby utworzyć najprostsze źródło danych wystarczy przygotować klasę implementującą interfejs IEnumerable, do którego, jak dobrze wiemy, przypisane są wszystkie rozszerzenia LINQ. Wówczas obsługa zapytań odbywa się niemal automatycznie – obecność rozszerzeń pozwala na stosowanie nowych operatorów, a więc i formułowanie zapytań LINQ. Tak przygotowane źródło nie wymaga analizy zapytania, co jest dość żmudnym zadaniem programistycznym, ale jednocześnie nie daje pełnej kontroli nad procesem pobieraniem danych ze źródła. W uproszczeniu można tę alternatywę przedstawić następująco: albo pobieramy wszystkie dane ze wskazanego źródła i udostępniamy w postaci kolekcji, która może być następnie filtrowana i sortowana za pomocą zapytań LINQ to Objects albo idziemy na całość, implementujemy interfejs IQueryable<> i samodzielnie analizujemy zapytanie udostępniane w strukturze drzewa (tzw. drzewa wyrażenia), a uzyskane w ten sposób parametry wykorzystujemy przy pobieraniu danych. Najlepszym przykładem jest budowanie zapytania SQL, które wysyłane jest do relacyjnej bazy danych.

Zacznijmy zatem od najprostszego przypadku. Interfejs IEnumerable, który zaimplementujemy zdefiniowany jest w przestrzeni nazw System.Collections, którą należy włączyć do projektu. Interfejs ten definiuje tylko jedną metodę abstrakcyjną GetEnumerator. Zwraca ona udostępniane przez nas dane w postaci kolekcji, a konkretnie referencję do kolekcji IEnumerator. Zdefiniujmy zatem klasę ŹródłoLiczbLosowych, która będzie implementowała tę klasę. Dla wygody wyposażyliśmy naszą klasę w metodę Execute, która w naszym przykładzie zajmuje się generowaniem liczb losowych. Można sobie jednak wyobrazić, że metoda ta generuje lub pobiera z zewnętrznego źródła dane o bardziej praktycznym zastosowaniu. Konstruktor klasy pobiera tylko jeden argument, w którym ustalany jest rozmiar generowanej kolekcji. Cała klasa przedstawiona jest na listingu 1.

Listing 1. Najprostsze źródło danych dla zapytań LINQ

class ŹródłoLiczbLosowych : IEnumerable

{

   int size = 0;

 

   public ŹródłoLiczbLosowych(int size)

   {

      if (size < 1) throw new Exception("Rozmiar musi być liczbą większą od zera");

      else this.size = size;

   }

 

   IEnumerator IEnumerable.GetEnumerator()

   {

      return Execute().GetEnumerator();

   }

 

   public List<double> Execute()

   {

      Random random = new Random();

      List<double> lista = new List<double>(size);

      for (int i = 0; i < size; i++)

      {

         double liczba = random.NextDouble();

         lista.Add(liczba);

      }

      return lista;

   }

}

 

Korzystając z powyższej klasy możemy skonstruować zapytanie, które pobierze 10 liczb losowych typu double o wartościach z zakresu od 0.0 do 1.0. Pokazuje to listing 2.

Listing 2. Wyróżniona linia zaznacza zapytanie LINQ pobierające dane z naszego źródła danych

private void button1_Click(object sender, EventArgs e)

{

   ŹródłoLiczbLosowych zrodlo = new ŹródłoLiczbLosowych(10);

   var zapytanie = from double liczba in zrodlo select liczba;

 

   string s = "Liczby losowe:\n";

   foreach (double liczba in zapytanie) s += liczba.ToString() + "\n";

   MessageBox.Show(s);

}

Ponieważ nie korzystamy z typu ogólnego IEnumerable konieczne jest jawne podanie typu zmiennych umieszczanych w kolekcji i dlatego zmienna lokalna liczba musi być zadeklarowana jako double.

Jak zapowiadałem definiując w ten sposób źródło danych „za darmo” otrzymujemy metody sortujące i filtrujące dane. Korzystamy bowiem z metod zdefiniowanych dla kolekcji, którą zwracamy. Dzięki temu możemy skonstruować zapytanie korzystające z operatorów where i orderby:

var zapytanie = from double liczba in zrodlo where liczba > 0.5 orderby liczba select liczba;

Wówczas zwrócone zostaną tylko te z wygenerowanych liczb, które są większe od 0.5, a ponadto zwracane liczby zostaną posortowane od najmniejszej do największej.

Podchodząc w ten sposób do projektowania źródła LINQ nasze zadanie ogranicza się w zasadzie do wygenerowania odpowiedniej kolekcji. Resztę załatwia technologia LINQ to Objects. Dane są w całości pobierane do pamięci, ale to jest dość typowe w ADO.NET, więc nie musi budzić naszego niepokoju.

IEnumerable<T>

Zazwyczaj to nie typ IEnumerable, ale jego parametryczna wersja jest implementowana przy tworzeniu źródeł dla zapytań LINQ. W takim przypadku konieczne jest zaimplementowanie dwóch wersji metody GetEnumerator (listing 3). W zmodyfikowanej wersji metody Execute (jej sygnatura to public IEnumerator<T> GetEnumerator()) rozpoznaję typ parametru za pomocą Reflection i reaguję tylko na int i double. To rozwiązanie, które mi samemu niezbyt się podoba, ale niestety w bibliotekach .NET nie ma interfejsu pozwalającego rozróżnić typy liczbowe, co pozwoliłoby na znacznie bardziej eleganckie zbudowanie klasy źródła liczb losowych. Na szczęście ten „brzydki” fragment kodu zamknięty jest w metodzie Execute i nie wpływa na tę część klasy, którą Czytelnik mógłby kopiować do własnych źródeł LINQ.

Listing 3. Parametryczna wersja źródła

class ŹródłoLiczbLosowych<T> : IEnumerable, IEnumerable<T>

{

   int size = 0;

 

   public ŹródłoLiczbLosowych(int size)

   {

      if (size < 1) throw new Exception("Rozmiar musi być liczbą większą od zera");

      else this.size = size;

   }

 

   public IEnumerator<T> GetEnumerator()

   {

      return ((IEnumerable<T>)Execute()).GetEnumerator();

   }

 

   IEnumerator IEnumerable.GetEnumerator()

   {

      return Execute().GetEnumerator();

   }

 

   public IEnumerable<T> Execute()

   {

      if (typeof(T) == typeof(double)) return (IEnumerable<T>)ExecuteDouble();

      if (typeof(T) == typeof(int)) return (IEnumerable<T>)ExecuteInt();

 

      throw new Exception("Parametry typu innego niż double nie są obsługiwane");

   }

 

   private List<double> ExecuteDouble()

   {

      Random random = new Random();

      List<double> lista = new List<double>(size);

      for (int i = 0; i < size; i++)

      {

         double liczba = random.NextDouble();

         lista.Add(liczba);

      }

      return lista;

   }

 

   private List<int> ExecuteInt()

   {

      Random random = new Random();

      List<int> lista = new List<int>(size);

      for (int i = 0; i < size; i++)

      {

         int liczba = random.Next();

         lista.Add(liczba);

      }

      return lista;

   }

}

Uwaga! Ze względu na późne rozpoznawanie typu parametru poniższy kod należy uznać za niezbyt porządny. Nie dotyczy to jednak tej części, która powtarzać będzie się w innych źródłach danych LINQ, a jedynie naszej wersji metody Execute.

Teraz możemy nasze zapytanie zmodyfikować tak, aby wskazać w nim jaki typ liczb losowych nas interesuje (listing 4). Ze względu na jawną parametryzację nie ma już konieczności wskazywania typu elementu w zapytaniu.

Listing 4. Nowa postać zapytań

private void button1_Click(object sender, EventArgs e)

{

   ŹródłoLiczbLosowych<double> zrodloDouble = new ŹródłoLiczbLosowych<double>(10);

   var zapytanieDouble = from liczba in zrodloDouble

                         where liczba > 0.5

                         orderby liczba

                         select liczba;

 

   string s = "Liczby losowe typu double z zakresu od 0 do 1:\n";

   foreach (var liczba in zapytanieDouble) s += liczba.ToString() + "\n";

   MessageBox.Show(s);

 

   //--------------------------------

 

   ŹródłoLiczbLosowych<int> zrodloInt = new ŹródłoLiczbLosowych<int>(10);

   var zapytanieInt = from liczba in zrodloInt

                      where liczba > int.MaxValue/2

                      orderby liczba

                      select liczba;

 

   s = "Liczby losowe typu int z zakresu od 0 do "+int.MaxValue.ToString()+":\n";

   foreach (var liczba in zapytanieInt) s += liczba.ToString() + "\n";

   MessageBox.Show(s);

}

Rozdział źródła i jego interfejsu

Ze względu na higienę pracy warto rozdzielić tę część naszego źródła danych LINQ, która jest bezpośrednio odpowiedzialna za generowanie danych od tej, która związana jest z implementacją interfejsów wymaganych od źródeł LINQ. Stwórzmy zatem klasę GeneratorLiczbLosowych<T> i przenieśmy do niej metodę Execute razem z metodami ExecuteInt i ExecuteDouble, które są z tej pierwszej wywoływane (wszystkie są identyczne jak na listingu 3). Nową klasę wyposażmy w pole size, którego wartość ustalana jest konstruktorze. Po utworzeniu nowej klasy możemy z klasy ŹródłoLiczbLosowych<T> usunąć metodę Execute i jej córki oraz pole size. Zamiast tego należy do niej dodać pole źródło typu GeneratorLiczbLosowych<T>. Najlepiej inicjację tego pola przeprowadzić w konstruktorze. Kod źródłowy obu klas znajdzie Czytelnik na dołączonej płycie (katalog z numerem 3) lub poniżej w listingu 5. Klasa ŹródłoLiczbLosowych<T>, poza referencją do klasy GeneratorLiczbLosowych<T> nie zawiera już żadnych odniesień do konkretnego problemu. Aby i ten ślad usunąć skorzystamy z interfejsu IQueryProvider. To z jednej strony sformalizuje nasz podział na zasadnicze źródło danych i interfejs pozwalający na stosowanie wobec nich zapytań LINQ, a z drugiej pozwoli na zastąpienie referencji do klasy GeneratorLiczbLosowych<T> referencją do obiektu typu IQueryProvider. Interfejs IQueryProvider został dodany dopiero do wersji Beta 2 platformy .NET 3.5, więc dla osób uczących się projektowania źródeł danych LINQ z jednego z opisów w MSDN, które pojawiły się w 2006 wraz z udostępnieniem wersji Beta 1 bibliotek LINQ może być zaskoczeniem. Nowy interfejs wymusza obecność metod Execute i jej wersji parametrycznej oraz metod CreateQuery i jej wersji parametrycznej. Wszystkie cztery przyjmują argument typu System.Linq.Expressions.Expression. Na razie będzie on przez nas całkowicie ignorowany, ale warto już zwrócić na niego uwagę, bo przekazuje on zapytanie LINQ (zapisany w strukturze drzewa wyrażeń), które już niedługo będziemy sami analizować i obsługiwać. Pole tego typu dodałem do klasy ŹródłoLiczbLosowych, ale na razie inicjuję je „pustym” wyrażeniem Expression.Constant(this). Metody te będą wywoływane w momencie wykonania instrukcji zawierającej zapytanie LINQ i tędy uzyskamy informacje o konkretnej postaci wyrażenia zawierającego informacje o zapytaniu. W efekcie klasa ŹródłoLiczbLosowych<T>, w której referencję do GeneratorLiczbLosowych<T> możemy już zmienić na referencję do IQueryProvider, zmienia się w ogólne narzędzie, którego można użyć w dowolnym projekcie, gdyż nie zawiera żadnego odniesienia do problemu liczb losowych. Aby to podkreślić przemianowała tę klasę na Query. Całość widoczna jest na listingu 5. W drugiej części artykułu klasa Query ulegnie jeszcze drobnym modyfikacjom, ale pozostanie niewielka.

Listing 5. Rozdzielone klasy źródła danych LINQ. Klasa ŹródłoDanychLosowych została przemianowana na Query

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

 

using System.Collections;

using System.Linq.Expressions;

 

namespace LINQ_to_Random

{

   class Query<T> : IEnumerable, IEnumerable<T>

   {

      IQueryProvider źródło = null;

      Expression wyrażenie = null;

 

      public Query(IQueryProvider źródło)

      {

         if (źródło == null) throw new ArgumentNullException("źródło");

         this.źródło = źródło;

         wyrażenie = Expression.Constant(this);

      }

 

      #region IEnumerable,IEnumerable<T>

      public IEnumerator<T> GetEnumerator()

      {

         return ((IEnumerable<T>)źródło.Execute(wyrażenie)).GetEnumerator();

      }

 

      IEnumerator IEnumerable.GetEnumerator()

      {

         return ((IEnumerable)źródło.Execute(wyrażenie)).GetEnumerator();

      }

      #endregion

   }

 

   class GeneratorLiczbLosowych<T> : IQueryProvider

   {

      int size = 0;

 

      public GeneratorLiczbLosowych(int size)

      {

          if (size < 1) throw new Exception("Rozmiar musi być liczbą większą od zera");

          else this.size = size;

      }

 

      public IEnumerable<T> Execute()

      {

         if (typeof(T) == typeof(double)) return (IEnumerable<T>)ExecuteDouble();

         if (typeof(T) == typeof(int)) return (IEnumerable<T>)ExecuteInt();

 

         throw new Exception("Parametry typu innego niż double nie są obsługiwane");

      }

 

      private List<double> ExecuteDouble()

      {

         Random random = new Random();

         List<double> lista = new List<double>(size);

         for (int i = 0; i < size; i++)

         {

            double liczba = random.NextDouble();

            lista.Add(liczba);

         }

         return lista;

      }

 

      private List<int> ExecuteInt()

      {

         Random random = new Random();

         List<int> lista = new List<int>(size);

         for (int i = 0; i < size; i++)

         {

            int liczba = random.Next();

            lista.Add(liczba);

         }

         return lista;

      }

 

      #region IQueryProvider

      IQueryable IQueryProvider.CreateQuery(Expression expression)

      {

         return new Query<T>(this).AsQueryable();

      }

 

      IQueryable<S> IQueryProvider.CreateQuery<S>(Expression expression)

      {

         return (IQueryable<S>)(new Query<T>(this).AsQueryable());

      }

 

      S IQueryProvider.Execute<S>(Expression expression)

      {

         return (S)this.Execute();

      }

 

      object IQueryProvider.Execute(Expression expression)

      {

         return this.Execute();

      }

      #endregion

   }

}

Inicjacja źródła danych dla generatora działającego na liczbach double powinna teraz przyjąć postać widoczną na listingu 6.

Listing 6. Nowy sposób tworzenie obiektu reprezentującego źródło danych

Query<double> zrodloDouble =

   new Query<double>(new GeneratorLiczbLosowych<double>(10));

var zapytanieDouble = from liczba in zrodloDouble

                      where liczba > 0.5

                      orderby liczba

                      select liczba;

string s = "Liczby losowe typu double z zakresu od 0 do 1:\n";

foreach (var liczba in zapytanieDouble) s += liczba.ToString() + "\n";

MessageBox.Show(s);

 

Powyższy artykuł jest fragmentem przygotowywanej przeze mnie książki pt. „C# 3.0 i .NET 3.5. Technologia LINQ”, która ukaże się niebawem nakładem Wydawnictwa Helion.

 

Jacek Matulewski
e-mail: jacek@phys.uni.torun.pl
WWW: http://www.fizyka.umk.pl/~jacek

 

 

tagi: LINQ

Komentarze 0

pkt.

Zaloguj się lub Zarejestruj się aby wykonać tę czynność.