\


 Wednesday, 24 February 2010
Podivné? chování při explicitním přetypování typu dynamic ve Visual Studiu 2010 RC

Na twitteru jsem psal, že si pohraju s implementací rozhraní ve třídě přes automatickou delegaci na privátní  proměnnou s využitím nového typu dynamic v C# 4.0. Jestliže se dobře pamatuji, většinou se po nějakém takovém řešení pídí Delphisté. Z příkladu níže bude asi jasné i pro ostatni, co mám předchozími hutnými větami na mysli .

Při hraní si s typem dynamic jsem ale narazil na zvláštní chování při explicitním přetypování a chtěl bych poprosit někoho dalšího z mých čtenářů o vyzkoušení stejného chování ve Visual Studiu 2010 (nejlépe nejen na RC, ale i na starší Betě 2, kterou jsem už smazal). Příklad níže je jen jednoduchý “jednosměrný” prototyp, na kterém vynikne problém s explicitním přetypováním.

Zde je mnou zmiňovaná podivnost (problém):

Mějme rozhraní IWorker:

   public interface IWorker
    {
        void DoWork();
    }

A třídu Worker, která toto rozhraní implementuje.

 class Worker : IWorker
    {
        #region Implementation of IWorker

        public void DoWork()
        {
            Console.WriteLine(GetType().ToString());
        }

        #endregion
    }

Dále máme  třídu Order, která rozhraní IWorker neimplementuje, ale má privátní proměnnou m_worker implementující toto rozhraní, kterou předá své bázové třídě DirtyCastBase. DirtyCastBase je třída, která zajistí, že bude-li klient přetypovávat instanci Order na rozhraní IWorker, tak toto přetypování projde a klient dostane jako implementora instanci m_worker.

 public class Order : DirtyCastBase
    {
        private IWorker m_worker;
        public Order() : base()
        {
            m_worker = new Worker();
            SetImplementors(m_worker);
        }
    }

Třída DirtyCastBase je potomkem třídy DynamicObject, která nám v .Net 4.0 dovoluje reagovat na “dynamická volání” a přidat jednoduše “dynamické chování” přepsáním metod začínajících písmeny Try (TryGetMember, TrySetMember ) apod. Já jsem přepsal metodu TryConvert, která se s využitím chráněné virtuální metody TryFindImplementor pokusí nalézt objekt, který podporuje rozhraní vyžadované uživatelem. Deskriptor rozhraní je předán ve vlastnosti Type argumentu binder.

public class DirtyCastBase : DynamicObject
    {
        private IEnumerable<Object> m_implementors;
        private Dictionary<Type, object> m_castContext;

        public DirtyCastBase()
        {
            m_castContext = new Dictionary<Type, object>();
        }
        

        public override bool TryConvert(ConvertBinder binder, out object result)
        {
            Type requestedType = binder.Type;            

            Tuple<bool, Object> FindResult =  TryFindImplementor(m_implementors, m_castContext, requestedType);
            
            if (FindResult.Item1)
            {
                result = FindResult.Item2;
                return true;
            }

            return base.TryConvert(binder, out result);
        }

        
        protected virtual Tuple<bool, Object> TryFindImplementor(IEnumerable<Object> implementors, Dictionary<Type, object> currentCastContext, Type requestedType)
        {
            if (implementors == null)
            {
                throw new ArgumentNullException("implementors");
            }
            if (currentCastContext == null)
            {
                throw new ArgumentNullException("currentCastContext");
            }
            if (requestedType == null)
            {
                throw new ArgumentNullException("requestedType");
            }

            object result = null;

            bool found = m_castContext.TryGetValue(requestedType, out result);
            
            
            if (!found)
            {
                
                result = (from implementor in implementors
                          where implementor != null
                          let type = implementor.GetType()
                          where requestedType.IsAssignableFrom(type)
                          select implementor).FirstOrDefault();

                found = result != null;
            }
            
            if (found)
            {
                m_castContext.Add(requestedType, result);
            }
            return new Tuple<bool, object>(found, result);

        }

        protected void SetImplementors(params object[] implementors)
        {
            SetImplementors(implementors.AsEnumerable());
        }

        protected void SetImplementors(IEnumerable<object> implementors)
        {
            if (m_implementors != null)
            {
                throw new InvalidOperationException();
                
            }
                        
            m_implementors = implementors ?? Enumerable.Empty<Object>();
        }
    }
 

V našem případě by tedy třída Order by měla dovolit přetypování na rozhraní IWorker, i když sama toto rozhraní neimplementuje. Zanedbejme nyní, že není zachována referenční identita při konverzi i že vydáváme jako implementora privátní objekt, protože pro demonstrovanou techniku to není příliš podstatné.

Tento kód ověří, že přetypování projde. Využíváme implicitní  (“bez závorek”) konverzi. Samozřejmě že je nutné instanci Order přiřadit do proměnné typu dynamic.

    class Program
    {
        static void Main(string[] args)
        {
            dynamic order = new Order();

            IWorker worker = order;

            worker.DoWork();
            Console.ReadLine();
        }
    }

Implicitní konverze projde, a jak jsem očekával, je vyvolána naše metoda TryConvert.

Když ale projde implicitní konverze, proč explicitní konverze selže a metoda TryConvert vyvolá výjimku?

 static void Main(string[] args)
        {
            dynamic order = new Order();

            IWorker worker =  (IWorker) order;

            worker.DoWork();
            Console.ReadLine();
        }

Ihned je vyvolána výjimka InvalidcastException {"Unable to cast object of type 'DynamicCastTest.Order' to type 'DynamicCastTest.IWorker'."}

Z call stacku výjimky (at CallSite.Target(Closure , CallSite , Object ) se dá odvodit, že výjimku vyhodil C# DLR binder, který se ale ani nepokusil zavolat metodu TryConvert. Metoda TryConvert by ve vlastnosti Explicit argumentu binder měla dostat příznak, že šlo o explicitní konverzi, ale tato metoda evidentně není volána.

Přitom z dokumentace metody TryConvert se dá usoudit, že by metoda TryConvert měla být při explicitní konverzi volána.

Ještě jsem zcela do detailu nestudoval všechna pravidla pro typ dynamic ve specifikaci C# 4.0, ale tohle na mě působí jako bug. Pokud projde implicitní konverze, proč by byla zcela zakázána explicitní? To vůbec není v souladu s pravidly kompilátoru pro konverze v C#:

“The set of explicit conversions includes all implicit conversions. This means that redundant cast expressions are allowed.” (C# specification sekce 6.2 Explicit conversions)

A C# DLR binder se i za běhu snaží podle mých dosavadních zkušeností vždy co co nejvěrněji napodobit chování C# kompilátoru.

Anebo už jsem dnes utahaný, nejde o žádnou anomálii a něco triviálního ve svém kódu přehlížím? :-)

Tedy znovu. Můžete někdo spustit projekt ve svém Visual Studiu (nejlépe i v Betě 2) a podělit se o výsledek?

Zde je projekt ke stažení.

Díky!



Wednesday, 24 February 2010 19:19:03 (Central Europe Standard Time, UTC+01:00)       
Comments [4]  .NET Framework | C#