g René Stein - LINQ \

Školení Návrhové vzory, OOP a UML


 Sunday, December 21, 2008
Extenzní metoda - binární operace And pro enumerace

V diskuzním fóru se (po dlouhé době :-)) objevil jeden zajímavější dotaz, který se netýká ani toho, jak zobrazit druhý formulář v aplikaci, ba ani autor nebojuje s mizením dat po postbacku v ASP.NET aplikaci.

Ale vážně - autor dotazu by chtěl mít lepší syntaxi pro binární operaci And v enumeracích označených metaatributem Flags. Mně stávající C syntaxe (Rights & Rights.Add == Rights.Add) zcela vyhovuje a žádný další syntaktický cukřík hltat nechci,  ale přesto mě zaujalo, jak by se dal problém, tedy spíš estetická preference náročného tazatele :-),  řešit.

Tazatel přeposlal svůj dotaz i do konference na vývojáři, kde bylo nabídnuto řešení přes dočasné přetypování na typ Object a poté z typu Object na typ int. Jak zaznělo v kritice na vývojáři - "dirty" řešení je funkční, ale opakovaný boxing a unboxing hodnot není zrovna ta pravá vývojářská slast.

Zkusil jsem vymyslet jiné řešení  - přiznám se, že IL jsem nezkoumal a žádné výkonnostní testy nedělal.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace BinaryOpGenericTest
{
    [Flags]
    enum MyFlags
    {
        A = 1,
        B = 2,
        C = 4

    }

    static class EnumExtensions
    {
        private static Dictionary<Type, Delegate> m_operations = new Dictionary<Type, Delegate>();

        public static bool Contains<T>(this T firstOperand, T secondOperand) 
                                                  where T : struct
        {

            Type enumType = typeof(T);
            
            
            if (!enumType.IsEnum)
            {
                throw new InvalidOperationException("Enum type parameter required");
            }


            Delegate funcImplementorBase = null;
            m_operations.TryGetValue(enumType, out funcImplementorBase);

            Func<T, T, bool> funcImplementor = funcImplementorBase as Func<T, T, bool>;
            
            if (funcImplementor == null)
            {
                funcImplementor = buildFuncImplementor(secondOperand);
            }



            return funcImplementor(firstOperand, secondOperand);
        }


        private static Func<T, T, bool> buildFuncImplementor<T>(T val)
                                                            where T : struct
        {
            var first = Expression.Parameter(val.GetType(), "first");
            var second = Expression.Parameter(val.GetType(), "second");
                    
            Expression convertSecondExpresion = Expression.Convert(second, typeof(int));
            var andOperator = Expression.Lambda<Func<T, T, bool>>(Expression.Equal(
                                                                                                       Expression.And(
                                                                                                            Expression.Convert(first, typeof(int)),
                                                                                                             convertSecondExpresion),
                                                                                                       convertSecondExpresion),
                                                                                             new[] { first, second });
            Func<T, T, bool> andOperatorFunc = andOperator.Compile();
            m_operations[typeof(T)] = andOperatorFunc;
            return andOperatorFunc;
        }
    }


    class Program
    {
        static void Main(string[] args)
        {
            MyFlags flag = MyFlags.A | MyFlags.B;

            Console.WriteLine(flag.Contains(MyFlags.A));            
            Console.WriteLine(EnumExtensions.Contains(flag, MyFlags.C));
            Console.ReadLine();
        }
    }
}

Pár poznámek ke kódu.

  1. Nejdůležitější v kódu je generická extenzní metoda Contains. Nelze použít negenerickou metodu rozšiřující třídu Enum, protože poté začneme mít další problémy s přetypováváním hodnot z obecné Enum na konkrétní enumeraci. Řešením není ani negenerická metoda, v níž pracujeme pouze s třídou Enum. Enumerace jsou hodnotové typy, a proto je na  generický parametr T aplikováno alespoň omezení struct. Jestliže bude zájem, mohu tato rozhodnutí, zde jen zběžně zmíněná, vysvětlit v nějakém dalším spotu.
  2. Když si nejsme jisti, že nám byla za generický typ předána enumerace, musíme provést v metodě Contains další kontrolu za běhu aplikace.
  3. Binární operace And je reprezentována delegáty. Delegáty dynamicky vytvářím za běhu aplikace pro každou enumeraci v metodě buildFuncImplementor. Delegát je vytvořen pomocí abstraktního syntaktického stromu (Expression tree) a poté je na výsledném výrazu (Expression) volána metoda Compile, která vrátí delegáta typu Func<T, T, bool>.
  4. Delegáty Func<T, T, bool> ukládáme do objektu Dictionary jako obecný typ Delegate - důvodem je to, že třída EnumExtensions není generická. Místo přetypovávání delegáta z předka Delegate na typ Func<T, T, bool> by bylo možné také použít pozdní vazbu voláním metody DynamicInvoke dostupné  přímo ve třídě Delegate.
  5. Jestliže byste chtěli operaci And rozšířit i na další typy, které lze konvertovat na typ Int, mohla by se Vám hodit moje extenzní metoda pro detekci konverze generické proměnné na typ Int za běhu aplikace.

Sunday, December 21, 2008 12:22:20 PM (Central Europe Standard Time, UTC+01:00)  #     
Comments [4]  .NET Framework | Compact .Net Framework | LINQ


 Sunday, October 05, 2008
Adaptéry pro funktory v C++ => Adaptéry pro funkce v C#

V C++ je snadné napsat takzvané adaptéry pro funkce, respektive pro funktory - objekty, chovající se jako funkce. K čemu jsou adaptéry dobré? Představme si, že máme napsanou funkci equal_to, která přijímá dva argumenty a vrátí true, jestliže jsou oba argumenty shodné, jinak vrátí false. Jedná se tedy o binární funktor, protože přijímá dva argumenty. Nyní potřebujeme pomocí stl metody find_if vyhledat v naší kolekci všechny prvky, jejichž hodnota je rovna 10. Podmínku v metodě find_if musí představovat unární funktor (funktor přijímající jeden argument - prvek v kolekci - a vracející true jen v případě, že prvek v kolekci podmínku splňuje). Je zřejmé, že binární funktor nemůžeme použít na místě, kde je očekáván unární funktor. V C++ můžeme ale v této situaci namísto psaní dalšího unárního jednoúčelového funktoru využít speciálního adaptéru, jehož účelem je konverze binárního funktoru na unární. Adaptér, který přijde vhod pro naše účely, se jmenuje binder1st (zde by bylo možné použít i adaptér binder2nd). Adaptér binder1st očekává, že mu předáte binární funktor, který má být převeden na unární  a hodnotu, která má být vždy použita jako první argument (proto ...1st) při volání binárního funktoru. Adaptér binder2nd se od adaptéru binder1st liší jen tím, že předaná hodnota bude použita vždy jako druhý argument předaného binárního funktoru. Jinými slovy - při volání funktoru binder1st je kolekce spokojena, že dostala unární funktor, ale náš funktor binder1st interně deleguje volání na binární funktor, kterému předá jako první argument hodnotu, kterou jsme zadali při vytvoření adaptéru binder1st, a jako druhý argument objekt z kolekce, na kterém se má otestovat platnost podmínky.

binder1st<equal_to<int> > equalPredicate = bind1st(equal_to<int>(), 10);
iterator it1 = find_if(v1.begin(), v1.end(), equalPredicate);

V předchozím kódu jsme vytvořili adaptér (unární funktor) nazvaný equalPredicate, který zprostředkovává přístup k binárnímu funktoru equal_to. Skutečnost, že je  funktor equal_to binárním funktorem, poznáme z jeho deklarace.

template<class Type>
   struct equal_to : public binary_function<Type, Type, bool> 
   {
      bool operator()(
         const Type& _Left, 
         const Type& _Right
      ) const;
   };

Na druhém řádku  příkladu adaptér equalPredicate předáme funkci find_if, která porovná každý element v kolekci (const Type&  _Right)  s hodnotou 10. Funkce vrátí první prvek, který vyhoví podmínce _Left==Right  (konkrétně v našem případě jde o podmínku  10 == PrvekVKolekci). Konstantní hodnota 10 byla předána funkci bind1st a bude  představovat při každém volání "adaptovaného" funktoru equal_to  adaptérem equalPredicate  hodnotu argumentu _Left operátoru(). Funkce bind1st je "syntaktickým cukrem", který zjednodušuje vytváření adaptéru, protože nemusíme specifikovat všechny typové parametry adaptéru binder1st, ale spolehneme se na typovou inferenci provedenou kompilátorem.

Konec rychlé exkurze do C++. I v C# nám mohou adaptéry pro delegáty přijít vhod. Představme si, že již máme napsanou třídu, která vrací výsledek porovnání dvou hodnot ("je menší než", "je větší než").

    static class ComparerEx
    {
        public static bool GreaterThan<T>(T a, T b)
        {
            return Comparer<T>.Default.Compare(a, b) > 0;
        }

        public static bool LessThan<T>(T a, T b)
        {
            return Comparer<T>.Default.Compare(a, b) < 0;
        }
    }

Funkce chceme použít v LINQ podmínkách (např. můžeme chtít z kolekce celých čísel vrátit jen všechna čísla, jež jsou větší než 10). Ale také můžeme chtít sadu podmínek, které můžeme libovolně kombinovat a skládat tak jednoduše výrazy typu "všechny hodnoty z kolekce, jež jsou větší než 20, ale menší než 90". Stejně tak můžeme chtít za chvíli podmínku znegovat a máme zájem o hodnoty nepatřící do intervalu 20-90. Namísto psaní "jednoúčelových" (i anonymních) metod si můžeme jednotlivé podmínky předpřipravit a pomocí adaptérů pro delegáty je skládat do složitějších podmínek. Také můžeme chtít stejnou podmínku použít při restrikci v LINQu (Where extenze pracující s delegátem typu Func<  >) i při práci se staršími metodami (např. FindAll u List<T>), které očekávají odkaz na delegáta typu Predicate. To vše nám speciální adaptéry pro delegáty v C# umožní.

Nejprve se podívejme na použití adaptérů.

 

class Program
    {
        static void Main(string[] args)
        {

            
            Random rand = new Random();

            //Vygenerování náhodných čísel v rozsahu 1..100
            List<int> myList = new List<int>(Enumerable.Range(1, 100).Select((i) => rand.Next(1, 100)));

            //Vytvoření predikátu pro where část LINQ dotazu (všechna čísla, kromě čísel v rozsahu 10 - 90
            var predicate = FuncExtension.Bind2nd<int, int, bool>(10, ComparerEx.GreaterThan);
            predicate = FuncExtension.And(predicate, FuncExtension.Bind2nd<int, int, bool>(90, ComparerEx.LessThan));
            predicate = FuncExtension.Not(predicate);
            
            //LINQ dotaz  - v selectu je do anonymního typu vyzvednut i index prvku v kolekci
            var result = myList
                                .Where(predicate)
                                .Select((elem, index) => new {elem, index});
                         

            //Výpis LINQ dotazu
            foreach (var res in result)
            {
                
                Console.WriteLine("{0}:{1}", res.index, res.elem);
                
            }

            //Ukázka konverze podmínky (Func<?, bool> na delegáta typu Predicate očekávaného funkcí FindAll
            var vals = myList.FindAll(FuncExtension.ToPredicate(predicate));
            
            //Musíme dostat stejné výsledky jako v předchozím dotazu s využitím LINQu
            foreach (var val in vals)
            {
                Console.WriteLine(val);
            }

            Console.ReadLine();
        }
    }

V příkladu jsme si naplnili myList náhodnými čísly v intervalu od 1 do 100. Proměnná predicate představuje podmínku.

Použitím adaptéru Bind2nd(FuncExtension.Bind2nd<int, int, bool>(10, ComparerEx.GreaterThan);) vytvoříme podmínku "všechna čísla větší než  10". Vidíme, že jsme funkci ComparerEx.GreaterThan, která očekává dva argumenty, "adaptovali-převedli" na funkci (přesněji řečeno na delegáta), který očekává jeden argument. Druhým argumentem funkce ComparerEx.GreaterThan je vždy konstantní hodnota 10 předaná  při volání funkce Bind2nd.

V dalším kroku vytvoříme podmínku ("všechna čísla menší než 90" - FuncExtension.Bind2nd<int, int, bool>(90, ComparerEx.LessThan)); ) a zkombinujeme ji s předchozí podmínkou pomocí speciálního adaptéru, který představuje operátor And (FuncExtension.And(predicate, FuncExtension.Bind2nd<int, int, bool>(90, ComparerEx.LessThan))). Operátor And je pro zbytek aplikace stále jen obyčejným (unární) delegátem na funkci, která přijímá jeden argument a vrací true nebo false. Nyní máme tedy podmínku "všechna čísla větší než 10 a menší než 90".

Naše konečná podmínka ale má mít podobu (""všechna čísla s výjimkou čísel větších než 10 a menších než 90"). Proto použijeme další speciální adaptér Not, který v předchozích krocích sestavenou podmínku zneguje (FuncExtension.Not(predicate);)

Z kolekce myList vybereme přes LINQ všechna čísla splňující podmínku (proměnná predicate s definicí podmínky je argumentem extenzní metody Where) a vypíšeme je do konzole.

Nakonec ještě stejnou podmínku chceme předat metodě FindAll. Metoda FindAll ale očekává delegáta nazvaného Predicate, a proto použijeme další "adaptující" funkci ToPredicate, který stávající definici podmínky konvertuje na Predicate.

Jak adaptéry pracují? Podívejme se na funkci Bind2nd.

 public static Func<T0, R> Bind2nd<T0, T1, R>(T1 bindValue, Func<T0, T1, R> originalFunc)
        {
            return (arg => originalFunc(arg, bindValue));
        }

Bind2nd je generická funkce, která jako argument (T1 bindValue) očekává hodnotu, která bude představovat vždy druhý argument adaptovaného delegáta (Func<T0, T1, R> originalFunc - funkce přijímající dva argumenty, první typu T0, druhý typu T1 a vracející R). Funkce vrátí nového delegáta (Func<T0, R>), který ukazuje na funkci očekávající jeden argument  typu  T0 a vracející instanci generického typu R. Delegát při svém spuštění pouze vezme předaný argument (arg) a poskytne jej jako první argument delegátovi originalFunc, kterému současně vždy předá jako druhý argument hodnotu v původním argumentu bindValue.

Podobně fungují i další adaptéry. Pro zajímavost se podívejme na adaptér ToPredicate, který z předaného delegáta vytvoří delegáta typu Predicate, čehož jsme využili v předchozím příkladu.

public static Predicate<T> ToPredicate<T>(Func<T, bool> originalFunc)
        {
            return arg => originalFunc(arg);
        }

Funkce očekává ve svém argumentu originalFunc odkaz na delegáta typu Func, který přijímá jeden argument typu T a vrací bool. My vrátíme delegáta typu Predicate<T>, přičemž vrácený lambda výraz deleguje vykonání funkce na původního delegáta originalFunc. Pro zbytek aplikace je delegát Func<T, bool> skryt za rozhraním adaptéru Predicate<T>, který nám pomohl pro funkci FindAll přeložit podmínku "v neznámém jazyce" do srozumitelné řeči.

Následuje kompletní výpis kódu adaptérů. To, co nám prozatím chybí, je ekvivalent funkce bind1st (bind2nd) z C++, který by nám zjednodušil zápis podmínek bez nutnosti zadávat "ručně" generické argumenty. Ale o tom popřemýšlím zase "někdy jindy". :-)

 

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace FunctionExtensions
{
    static class FuncExtension
    {
        
        public static Func<T1, R> Bind1St<T0, T1, R>(T0 bindValue, Func<T0, T1, R> originalFunc)
        {
            return (arg => originalFunc(bindValue, arg));
        }

        public static Func<T0, R> Bind2nd<T0, T1, R>(T1 bindValue, Func<T0, T1, R> originalFunc)
        {
            return (arg => originalFunc(arg, bindValue));
        }

        
        public static Func<T0, bool> Not<T0>(Func<T0, bool> originalFunc)
        {
            return (arg => !originalFunc(arg));
        }

        public static Func<T0, T1, bool> Not<T0, T1>(Func<T0, T1, bool> originalFunc)
        {
            return ((arg1, arg2) => !originalFunc(arg1, arg2));
        }

        public static Func<T0, T1, T2, bool> Not<T0, T1, T2>(Func<T0, T1, T2, bool> originalFunc)
        {
            return ((arg1, arg2, arg3) => !originalFunc(arg1, arg2, arg3));
        }

        public static Func<T0, T1, bool> And<T0, T1>(Func<T0, T1, bool> originalFunc, Func<T0, T1, bool> originalFunc2)
        {
            return ((arg1, arg2) => originalFunc(arg1, arg2) && originalFunc(arg1, arg2));
        }

        public static Func<T0, bool> And<T0>(Func<T0, bool> originalFunc, Func<T0, bool> originalFunc2)
        {
            return (arg1  => originalFunc(arg1) && originalFunc2(arg1));
        }

        public static Func<T0, T1, bool> Or<T0, T1>(Func<T0, T1, bool> originalFunc, Func<T0, T1, bool> originalFunc2)
        {
            return ((arg1, arg2) => originalFunc(arg1, arg2)  || originalFunc(arg1, arg2));
        }
        
        public static Func<T0, bool> Or<T0>(Func<T0, bool> originalFunc, Func<T0, bool> originalFunc2)
        {
            return (arg1 => originalFunc(arg1) || originalFunc2(arg1));
        }

        public static Predicate<T> ToPredicate<T>(Func<T, bool> originalFunc)
        {
            return arg => originalFunc(arg);
        }
    }
}

Sunday, October 05, 2008 5:34:45 PM (Central Europe Standard Time, UTC+01:00)  #     
Comments [4]  .NET Framework | Compact .Net Framework | LINQ | Návrhové vzory


 Monday, September 15, 2008
Několik poznámek k přetypovávání generických kolekcí

Při práci s generickými kolekcemi asi každy občas zatouží převést generickou kolekci s objekty typu B na generickou kolekci s objekty typu A, přičemž instinktivně očekává, že když je typ A předkem typu B, žádný problém při konverzi nenastane a navíc půjde o konverzi implicitní - automatickou. Instinkty, intuice a další feminní rysy jsou ale při programování spíš zátěží (že by jeden z hlavních důvodů, proč je stále tak málo programátorek? ) :-) )

Konkrétně - mějme tyto dvě třídy.

 

    class Test
    {

        public override string ToString()
        {
            return "Test";
        }
    }


    class SpecialTest : Test
    {
        public override string ToString()
        {
            return "SpecialTest";
        }
    }
    
Vytvoříme si kolekci (List) odvozených tříd SpecialTest.
List<SpecialTest> srcList = new List<SpecialTest> { new SpecialTest(), new SpecialTest() };

Při pokusu přetypovat List<SpecialTest> na kolekci objektů typu Test (List<Test>)  neuspějeme.

List<Test> invalidAttemptList = srcList;

Cannot implicitly convert type 'System.Collections.Generic.List<CollectionInheritance.SpecialTest>' to 'System.Collections.Generic.List<CollectionInheritance.Test>'). Důvod je zřejmý - dva generické objekty List, jeden s generickým argumentem Test a druhý s generickým argumentem SpecialTest, jsou dvě zcela rozdílné a nezávislé třídy a skutečnost, že třída SpecialTest je potomkem třídy Test, neznamená, že by stejný vztah platil mezi třídami List<SpecialTest> a List<Test>. Autoři jazyka C# (dle svých vyjádření prozatím?) zavedli toto omezení kvůli typové bezpečnosti.

Možností, jak konverzi provést, je mnoho. Vyjmenujme alespoň ty, které nově přinesl LINQ.

LINQ nám nabízí pro daný účel metodu Cast, která zkonvertuje prvky ze zdrojové kolekce (IEnumerable) na (generický) typ předaný metodě.

IEnumerable<Test> ieList = srcList.Cast<Test>();

Jestliže by nebylo možné všechny prvky v kolekci převést na 'Test', bude vyvolána výjimka. Když máme v kolekci směs objektů z různých tříd nebo podporujících různá rozhraní, můžeme přetypovat pomocí dalšího standardního LINQ operátoru OfType, který do výsledné kolekce vloží jen ty objekty, které se pomocí operátoru as podaří přetypovat na cílový typ. Objekty, které přetypovat na cílový typ (v následujícím kódu tedy na  typ Test) nelze, jsou ignorovány.

IEnumerable<Test> ieList2 = srcList.OfType<Test>();

Jestliže nechceme mít jako výslednou kolekci typ IEnumerable, můžeme použít další LINQ operátor ToList() a výsledek přetypování nám bude vrácen v instanci List<T>.

List<Test> testList = srcList.OfType<Test>().ToList();

Alternativou k předchozímu zápisu může být využití konstruktoru třídy List.

List<Test> testList2 = new List<Test>(srcList.OfType<Test>());

 

Jestliže nechcete vždy pracovat jen s kolekcí typu List a chcete přetypovávat například na typové kolekce, využijete mé extenzní metody.

 

Collection<Test> trgList = srcList.WideningConvert<SpecialTest, Test, Collection<Test>>();

Metodě WideningConvert předáte jako typové argumenty aktuální typ v generické kolekci (SpecialTest), cílový-výsledný typ, na který má být zdrojový typ převeden (Test) a typ kolekce, která má být vrácena. Kolekce musí mít bezparametrický konstruktor a také musí podporovat rozhraní ICollection<T>.

Pomocí extenzní metody lze přetypovávat i z kolekce objektů typu "předek" na kolekci "potomků".

  

Collection<SpecialTest> nextList = trgList.NarrowingConvert<Test, SpecialTest, Collection<SpecialTest>>();
 
 

Metoda WideningConvert svým názvem dává najevo, že je určena pro implicitní ("bezpečnou") konverzi, kdy převádíte kolekci potomků na kolekci předků. Obdobně, metoda NarrowingConvert přetypovává kolekci objektů typu "předek" na kolekci objektů typ "potomek (explicitni konverze). Metoda NarrowingConvert se pokusí převést každý objekt ve zdrojové kolekci na cílový typ ("Potomek") pomocí operátoru as. Jestliže přetypování selže, je zdrojový objekt ignorován, a proto může výsledná kolekce vrácená metodou NarrowingConvert obsahovat méně prvků než kolekce zdrojová.

 

Zde je kompletní výpis metod. Bylo by samozřejmě možné začít uvažovat nad zjednodušením kódu pro přetypovávání, ale to si necháme "napříště".

 

    public static class ConvertExtensions
    {
        /// <summary>
        /// Metoda převede kolekci - metoda je určena pro přetypování generické kolekce s "potomky" na generickou kolekci s "předkem"
        /// </summary>
        /// <typeparam name="T0">Typ elementu v zdrojové kolekci</typeparam>
        /// <typeparam name="P">Typ elementu v cílové kolekci</typeparam>
        /// <typeparam name="R">Typ cílové kolekce</typeparam>
        /// <param name="source">Zdrojová kolekce</param>
        /// <returns>Cílovou kolekci s převedenými elementy</returns>
        public static R WideningConvert<T0, P, R>(this IEnumerable<T0> source) 
                                             where R : ICollection<P>, new()
                                             where T0: P            
        {

            if (source == null)
            {
                throw new ArgumentNullException();
            }
            R retCol = new R();

            foreach (T0 srcElem in source)
            {
                retCol.Add(srcElem);
            }
            return retCol;
        }

        /// <summary>
        /// Metoda převede kolekci - metoda je určena pro přetypování generické kolekce s "předkem" na generickou kolekci "potomků"
        /// </summary>
        /// <typeparam name="T0">Typ elementu v zdrojové kolekci</typeparam>
        /// <typeparam name="P">Typ elementu v cílové kolekci</typeparam>
        /// <typeparam name="R">Typ cílové kolekce</typeparam>
        /// <param name="source">Zdrojová kolekce</param>
        /// <returns>Cílovou kolekci s převedenými elementy</returns>
        ///<remarks>Počet prvků v cílové kolekcí může být menší než počet prvků v zdrojové kolekci, protože veškeré objekty, které nelze zkonvertovat na <typeparamref name="R"/>, jsou ignorovány</remarks>
        public static R NarrowingConvert<T0, P, R>(this IEnumerable<T0> source)
                                                  where R : ICollection<P>, new()
                                                  where P : class, T0
        {

            if (source == null)
            {
                throw new ArgumentNullException();
            }
            R retCol = new R();

            foreach (T0 srcElem in source)
            {

                P retValue = srcElem as P;

                if (retValue != null)
                {
                    retCol.Add(retValue);
                }
            }
            return retCol;
        }
      
                                             
    }

Monday, September 15, 2008 1:29:48 PM (Central Europe Standard Time, UTC+01:00)  #     
Comments [4]  .NET Framework | Compact .Net Framework | LINQ


 Friday, May 09, 2008
LINQ II - přetypovávání i vnořených anonymních datových typů z jiné assembly

V předchozím spotu jsem byl schopen pracovat s anonymními datovými typy, i když byly dotazy a výsledné sady dat vytvořeny v jiné assembly. Odstranění vrozené xenofobie v praxi.:)

Náš kód ale vygeneruje výjimku, jestliže anonymní datový typ z jiné assembly obsahuje další vnořené anonymní datové typy jako v následujícím upraveném příkladu. Vlastnost InnerAT vrací další anonymní datový typ, který  pro zajímavost obsahuje odkaz ještě na další anonymní datový typ.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LINQTEST
{
    public class TestAT
    {
        public static object GetResult()
        {
            string[] rows = { "Toyota", "Lexus", "Audi" };

            var test = from row in rows
                       select new
                       {
                           FirstLetter = row[0],
                           Index = 110,
                           Original = row,
                           InnerAT = new { X = row[1], B = new {A=1}}
                       };
            
            return test;

        }
    }
}

Řešení spočívá v úpravě extenzí a to tak, že přidáme privátní metodu GetTypeInstance a přeneseme do ní většinu kódu z extenze ToAnonymousType. Metoda GetTypeInstance při neshodě datového typu očekávaného parametrem "našeho - v naší assembly dostupného" konstruktoru anonymního datového typu a datového typu vlastnosti anonymního datového typu z "cizí" assembly rekurzivně přenese data z "cizího" anonymního datového typu do "našeho".

 

using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Reflection;
using System.Collections;
using LINQTEST;

namespace LINQAnonymous
{
    /// <summary>
    /// Rozšíření pro LINQ
    /// </summary>
    static class RSLinqExtensions
    {

        /// <summary>
        /// Metoda přetypuje objekt na anonymní typ, jehož struktura byla předána v parametru <paramref name="prototype"/>
        /// </summary>
        /// <typeparam name="T">Kompilátorem odvozený anonymní typ</typeparam>
        /// <param name="prototype">Prototyp se strukturou anonymního typu</param>
        /// <returns>Instanci anonymního typu, nebo null, jestliže konverzi nelze provést</returns>
        /// <remarks>Metoda se pokusí převést data z různých assembly</remarks>
        public static T ToAnonymousType<T>(this object obj, T prototype)
                                        where T: class
        {
            
            
            T atiObj = obj as T;
            
            if (atiObj == null)
            {
                
                atiObj = GetTypeInstance(obj, prototype.GetType()) as T;
               
            }
                                                     
            return (atiObj);
        }
    

        private static object GetTypeInstance(object obj, Type expected)
        {
            object atiObj = null;

            ConstructorInfo constructorInfo = expected.GetConstructors()[0];
                
                if (constructorInfo == null)
                {
                    return null;
                }

                ParameterInfo[] paramInfos = constructorInfo.GetParameters();                
                PropertyInfo[] origProperties = obj.GetType().GetProperties();
                

                if (paramInfos.Count() != origProperties.Count())
                {
                    return null;
                }

                object[] paramArgs = new object[paramInfos.Count()];


                for (int i = 0; i < paramArgs.Length; i++)
                {
                    PropertyInfo origProperty = origProperties.Where(prop => prop.Name == paramInfos[i].Name).FirstOrDefault();

                    if (origProperty == null)
                    {
                        return null;
                    }

                    object val = origProperty.GetValue(obj, null);
                    if (origProperty.PropertyType != paramInfos[i].ParameterType)
                    {
                        val = GetTypeInstance(val, paramInfos[i].ParameterType);
                    }

                    paramArgs[i] = val;
                }

                atiObj = constructorInfo.Invoke(paramArgs);
                return atiObj;
        }
        /// <summary>
        /// Metoda vrátí
        /// </summary>
        /// <typeparam name="T">Kompilátorem odvozený anonymní typ</typeparam>
        /// <param name="prototype">Prototyp se strukturou anonymního typu</param>
        /// <returns>List instancí anonymního typu, nebo null, jestliže konverzi nelze provést</returns>
        /// <remarks>Metoda se pokusí převést data z různých assembly</remarks>
        public static List<T> CastToList<T>(this object obj, T prototype)
                                 where T : class
        {
            List<T> list = new List<T>();
            IEnumerable<T> enumerable = obj as IEnumerable<T>;

            if (enumerable != null)
            {
                list.AddRange(enumerable);
            }
            else
            {
                    IEnumerable enumObjects = obj as IEnumerable;
                    if (enumObjects == null)
                    {
                        return null;
                    }
                    
                foreach (object enumObject in enumObjects)
                    {
                        T currObject = ToAnonymousType(enumObject, prototype);
                        if (currObject == null)
                        {
                            //K čistění listu by neměl být důvod, ale garantujeme, že nevrátíme částečně naplněný list
                            list.Clear();
                            return list;
                        }

                        list.Add(currObject);
                    }
                
            }

            return list;
        }
    }
    

Při přetypovávání stačí stále jen zadat prototyp anonymního datové typu.

 

//Anonymní typ z jiné assembly!
            var result2 = TestAT.GetResult().CastToList(new {FirstLetter = default(char), 
                                                        Index =default(int),
                                                        Original = default(string),
                                                        InnerAT = new { X = default(char), B = new { A = default(int) } }
            })
                                                       ;
            foreach (var res in result2)
            {
                Console.WriteLine(res.FirstLetter);
                Console.WriteLine(res.Original);
            }


            Console.WriteLine(TestAT.
                                    GetResult().
                                    CastToList(new
                                    {
                                        FirstLetter = default(char),
                                        Index = default(int),
                                        Original = default(string),
                                        InnerAT = new { X = default(char), B = new { A =default(int)} }
                                    }
                                    ).
                                    Where(car => car.FirstLetter == 'T')
                                     .FirstOrDefault()
                                     .ToString());
            Console.ReadLine();

Friday, May 09, 2008 9:09:26 AM (Central Europe Standard Time, UTC+01:00)  #     
Comments [0]  .NET Framework | ASP.NET | Compact .Net Framework | LINQ | Windows Forms


 Thursday, May 08, 2008
LINQ - anonymní typ deklarovaný v jedné assembly dostupný v metodách další assembly?
.Net Framework

Anonymní datové typy v LINQu nelze použít jako návratový typ z metody a jediný způsob, jak anonymní typ z metody předat, je použít jako návratovou hodnotu typ object, protože v .Net Frameworku - jak je všeobecně známo - všechny třídy přímo či nepřímo dědí z třídy Object. Navíc platí, že anonymní typ je kompilátorem vždy deklarován jako internal a jeho použití je tak striktně omezeno na jednu assembly.

Jde o rozumné omezení a anonymní datové typy bychom neměli zneužívat k nesmyslům typu "hezká syntaxe pro generování objektů Dictionary", které si našly cestu i do připravovaného (a už dnes "přehypovaného") MVC frameworku pro ASP.NET.

V různých diskuzích se ale stále dokola objevuje dotaz, jak anonymní typ z metody vráti. A každé omezení se dá samozřejmě obejít - když nefunguje ani bodový systém na silnicích, proč nenajít hrubý trik ve stylu "osoby blízké" i pro erozi různých omezení u anonymního datového typu. :) Znovu alibisticky varuji všechny před zařazením následujících nehezkých triků do svého arzenálu běžných postupů při vývoji, protože všechny postupy spoléhají na chování kompilátoru C#, které není garantováno a které se může v další verzi nebo i jen při vydání service packu .Net Frameworku bez varování změnit.

Pro vrácení anonymního datového typu z metody použijeme hezký hack od Tomáše, který se ujal pod názvem "Cast By Example". Zjednodušeně řečeno - sice nemůžeme používat při přetypovávání názvy anonymních datových typů (tříd), protože anonymní datové typy jsou generovány až při kompilaci, ale můžeme kompilátoru dát při přetypování "vzor", jaký anonymní datový typ nám bude vyhovovat. Podrobnosti si můžete najít v odkazovaném článku Tomáše Petříčka  = zde jen připomenu, že technika využívá současného chování kompilátoru, který pro různé deklarace anonymních datových typů se stejnými vlastnostmi generuje v jedné assembly vždy právě jednu třídu.

Napsal jsem jednoduše použitelné extenze, které vám dovolí nejen přetypovat jednu instanci "object" na anonymní datový typ, ale můžete přetypovat množiny záznamů na (anonymně ;-)) typovou kolekci List<NějakýAnonymniTyp>, a dokonce je možné jednoduše použít anonymní datové typy z jiné assembly.

 

/// <summary>
    /// Rozšíření pro LINQ
    /// </summary>
    static class RSLinqExtensions
    {

        /// <summary>
        /// Metoda přetypuje objekt na anonymní typ, jehož struktura byla předána v parametru <paramref name="prototype"/>
        /// </summary>
        /// <typeparam name="T">Kompilátorem odvozený anonymní typ</typeparam>
        /// <param name="prototype">Prototyp se strukturou anonymního typu</param>
        /// <returns>Instanci anonymního typu, nebo null, jestliže konverzi nelze provést</returns>
        /// <remarks>Metoda se pokusí převést data z různých assembly</remarks>
        public static T ToAnonymousType<T>(this object obj, T prototype)
                                        where T: class
        {
            
            
            T atiObj = obj as T;
            
            if (atiObj == null)
            {

                ConstructorInfo constructorInfo = typeof(T).GetConstructors()[0];
                
                if (constructorInfo == null)
                {
                    return null;
                }

                ParameterInfo[] paramInfos = constructorInfo.GetParameters();                
                PropertyInfo[] origProperties = obj.GetType().GetProperties();
                

                if (paramInfos.Count() != origProperties.Count())
                {
                    return null;
                }

                object[] paramArgs = new object[paramInfos.Count()];


                for (int i = 0; i < paramArgs.Length; i++)
                {
                    PropertyInfo origProperty = origProperties.Where(prop => prop.Name == paramInfos[i].Name).FirstOrDefault();
                    
                    if (origProperty == null)
                    {
                        return null;
                    }
                                        
                    
                    paramArgs[i] = origProperty.GetValue(obj, null);                    
                }
                
                atiObj = constructorInfo.Invoke(paramArgs) as T;
            }
            
            return (atiObj);
        }
    
        /// <summary>
        /// Metoda vrátí
        /// </summary>
        /// <typeparam name="T">Kompilátorem odvozený anonymní typ</typeparam>
        /// <param name="prototype">Prototyp se strukturou anonymního typu</param>
        /// <returns>List instancí anonymního typu, nebo null, jestliže konverzi nelze provést</returns>
        /// <remarks>Metoda se pokusí převést data z různých assembly</remarks>
        public static List<T> CastToList<T>(this object obj, T prototype)
                                 where T : class
        {
            List<T> list = new List<T>();
            IEnumerable<T> enumerable = obj as IEnumerable<T>;

            if (enumerable != null)
            {
                list.AddRange(enumerable);
            }
            else
            {
                    IEnumerable enumObjects = obj as IEnumerable;
                    if (enumObjects == null)
                    {
                        return null;
                    }
                    
                foreach (object enumObject in enumObjects)
                    {
                        T currObject = ToAnonymousType(enumObject, prototype);
                        if (currObject == null)
                        {
                            //K čistění listu by neměl být důvod, ale garantujeme, že nevrátíme částečně naplněný list
                            list.Clear();
                            return list;
                        }

                        list.Add(currObject);
                    }
                
            }

            return list;
        }
    }

Komentáře u metod by měly dostatečně popisovat funkci extenzí. Metoda ToAnonymousType předpokládá, že chcete přetypovat na instanci anonymního typu (např. při použití metody Single v LINQu), metoda CastToList pracuje s množinou (IEnumerable<T>) instancí anonymního datového typu. Většina kódu v obou metodách ošetřuje situaci, kdy pracujete s anonymním datovým typem z jiné (referencované) assembly, jehož data je potřeba přenést do instance anonymního datového typu v aktuální assembly.

Použití extenzí - nejprve u anonymního datového typu deklarovaného v assembly, ve které je také náš LINQ dotaz.

using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Reflection;
using System.Collections;
using LINQTEST;

class Program
    {

        //Anonymní typ deklarovaný v této (exe) assembly
        private static object GetLetters()
        {
           string[] names = {"Rene", "Petra", "Kamilka"};

           var test = from name in names
                      select new {FirstLetter = name[0], Index=1};
           return test;
        }


        static void Main(string[] args)
        {
            var result = GetLetters().CastToList(new {FirstLetter = default(char),
                                                      Index =default(int)}
                                                 );
            foreach (var res in result)
            {
                Console.WriteLine(res.FirstLetter);
            }

}

Metodě CastToList jsme predali "vzor" anonymího datového typu (new {FirstLetter = default(char), Index =default(int)}) a hodnoty vlastností jsme u prototypu inicializovali s využitím klíčového slova default. V metodě Main v cyklu foreach je funkční intellisense a můžeme pracovat zcela typově s proměnnou res. Jenom zdůrazním, že nyní žádná reflexe nebyla použita! Metoda CastToList s využitím automatické typové inference kompilátoru pouze zkopírovala prvky v IEnumerable<T> do našeho typového generického Listu.

if (enumerable != null)
            {
                list.AddRange(enumerable);
            }

Reflexe je využita při konverzi anonymního typu deklarovaného v jiné assembly. Předpokládejme, že v jiné assembly nazvané např. LINQTest máme další metodu vracející množinu dat skrytou opět za obecným rozhraním "služebníka zcela neužitečného" neboli třídy object.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LINQTEST
{
    public class TestAT
    {
        public static object GetResult()
        {
            string[] rows = { "Toyota", "Lexus", "Audi" };

            var test = from row in rows
                       select new { FirstLetter = row[0],
                                    Index=110,
                                    Original = row
                                  };
            
            return test;

        }
    }
}

Zkompilovanou assembly LINQTest zareferencujeme v našem projektu. Kód pro práci s anonymní datovým typem v jiné assembly se z pohledu uživatele LINQ extenze nijak nezměnil od předchozího příkladu.

 

class Program
    {


        static void Main(string[] args)
        {
            //Anonymní typ z jiné assembly!
            var result2 = TestAT.GetResult().CastToList(new {FirstLetter =  default(char), 
                                                        Index =default(int),
                                                        Original = default(string)}
                                                       );
            foreach (var res in result2)
            {
                Console.WriteLine(res.FirstLetter);
                Console.WriteLine(res.Original);
            }


            Console.WriteLine(TestAT.
                                    GetResult().
                                    CastToList(new
                                    {
                                        FirstLetter = default(char),
                                        Index = default(int),
                                        Original = default(string)
                                    }).
                                    Where(car => car.FirstLetter == 'T')
                                     .FirstOrDefault()
                                     .ToString());
            Console.ReadLine();
        }
    }

Jak si můžete všimnout, po cyklu foreach si požádám o data z jiné assembly znovu a poté nad vrácenou typovou kolekci vytvořím další projekci. A ani mě nemusí zajímat, že se mi pod rukama zcela změnil typ používaných objektů. :-)

Docela zábavná záležitost ne? ;-)

LINQ II - přetypovávání i vnořených anonymních datových typů z jiné assembly


Thursday, May 08, 2008 3:00:43 PM (Central Europe Standard Time, UTC+01:00)  #     
Comments [0]  .NET Framework | ASP.NET | Compact .Net Framework | LINQ | Windows Forms