\


 Sunday, 21 December 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, 21 December 2008 12:22:20 (Central Europe Standard Time, UTC+01:00)       
Comments [4]  .NET Framework | Compact .Net Framework | LINQ


Sunday, 21 December 2008 14:36:53 (Central Europe Standard Time, UTC+01:00)
Diky za velmi zajimave reseni, pouzit na tohle Expression Tree by me tedy nenapadlo - ale to je proto, ze o nich nic moc nevim. Buhuzel to funguje pouze pod FW 3.5.

Udelal jsem si par vykonostnich testu - otazka je, zda jsou dobre. FirstRun a SecondRun jsou to kvuli zkompilovani kodu
do IL.

* Jasny vykonostni vitez je opravdu Enum - ten je naprosto bleskovy.

* Reseni s boxovanim (ale bez volani extension metody, primo ve smycce)15x pomalejsi

* Reseni s boxovanim a expression tree vychazi vykonostne zhruba na stejno, asi 40x pomalejsi. Ale vsimnete si, ze jsem musel delat 10 000 000 volani, abych dostal nejaka podstatna cisla. Pokud tedy Contains pouziji nekde 4x v programu, vubec nema smysl nad vykonem uvazovat.

Enum FirstRun: 1 runs takes 0 mSec
Enum SecondRun: 1 runs takes 0 mSec
Enum: 10 000 000 runs takes 48 mSec
ContainsWithBoxingWithoutFunctionCall FirstRun: 1 runs takes 0 mSec
ContainsWithBoxingWithoutFunctionCall SecondRun: 1 runs takes 0 mSec
ContainsWithBoxingWithoutFunctionCall: 10 000 000 runs takes 695 mSec
ContainsWithBoxing FirstRun: 1 runs takes 2 mSec
ContainsWithBoxing SecondRun: 1 runs takes 0 mSec
ContainsWithBoxing: 10 000 000 runs takes 1993 mSec
ContainsWithExpressionTree FirstRun: 1 runs takes 17 mSec
ContainsWithExpressionTree SecondRun: 1 runs takes 0 mSec
ContainsWithExpressionTree: 10 000 000 runs takes 1919 mSec
Sunday, 21 December 2008 15:09:32 (Central Europe Standard Time, UTC+01:00)
Vyborne, dekuji za zajimave testy. Ano reseni je pouze pro NF 3.5.
Monday, 05 January 2009 16:18:48 (Central Europe Standard Time, UTC+01:00)
Zajímavý nápad.

Když už jsme u těch estetických záležitostí, přejmenoval bych metodu Contains na IsFlagSet nebo jen IsSet. Contains bych očekával u kontejneru, který má metody Add a Remove, kdežto hodnota [Flags] enumerace má bity, které jsou buď nastavené (1) nebo nenastavené (0), ale vždy jsou přítomné.
Monday, 05 January 2009 16:22:56 (Central Europe Standard Time, UTC+01:00)
Daniel Kolman: Jak bych take volil jine jmeno - tady jsem vysel jen ze zadani v diskuzi, na kterou odkazuji.

Cituji

"Rad bych napsal extension metodu *Contains()*, ktera by fungovala pro
vsechny enumy a vratila mi boolean, zda dany Enum obsahuje dany flag
nebo ne[...]"
Comments are closed.