Reflect<TTarget> aneb “infoof”, “Compile time member info” nebo ”Strongly typed reflection”

Tomáš Holan       10. 1. 2011       LINQ, Reflexe       6605 zobrazení

Uznávám, že název dnešního příspěvku je trochu delší, komplikovaný a asi ne úplně průhledný, proto se radši hned pustím do vysvětlování o co jde.

V jazyku C# existuje všem asi dobře známý operátor typeof, který nám pro compile-time určený datový typ (nebo generický argument) vrací instanci objektu Type s runtime informacemi o daném typu. Jedna z uvažovaných, navrhovaných, ale nikdy neimplementovaných funkcí jazyka C# je operátor infoof (autoři tenkrát ze srandy navrhovali i to, aby výslovnost byla “in-foof”). Tento operátor by obdobně jako typeof umožňoval vrátit např. PropertyInfo nebo MethodInfo pro určenou vlastnost nebo metodu.

K čemu by taková věc mohla být dobrá? Obecně na platformě .NET lze najít odlišné a navzájem nezávislé případy, kdy musíme název něčeho určit v kódu jako argument typu string (např. název parametru při validaci argumentů, název vlastnosti při definici DependencyProperty, název vlastnosti při raisování události INotifyPropertyChanged.PropertyChanged, volání metody pomoci reflekce apod.). Nevýhodou tohoto přístupu je, že ve všech těchto případech přicházíme o možnost kontroly správnosti hodnoty stringu v době kompilace, možnosti refaktoringu např. při přejmenování dané vlastnosti a tím je umožněno vzniku úplně zbytečných chyb v našem kódu. Toto je pak zvláště nepříjemné, pokud je takových použití v našem projektu opravdu požehnaně jako např. při používání MVVM ve WPF/Silverlightu, které na implementaci interfacu INotifyPropertyChanged dost stojí.

A dnes se právě podíváme na jedno z možných řešení těchto problémů. Ještě předem upozorním, že podobných řešení jako to zde mnou předkládané existuje a lze na “netu” najít více (např. zde, zde, zde, zde a ještě třeba zde) a to i pod různými názvy jako třeba “Compile-time member info”, “Strongly typed reflection” či jen “Static reflection”. Některé tyto řešení jsou více obecné, některé o něco méně, ale většinou mají všechny společné to, že jsou založeny (stejně jako i řešení moje) na Expression Trees (funkce uvedená v C# 3.0).

Nebudu to zbytečně protahovat, pro většinu běžných případů by měla posloužit tato generická statická třída Reflect<TTarget>, kde TTarget označuje třídu, na jejíž členy (podporované jsou vlastnosti nebo metody) se potřebujeme odkazovat:

using System;
using System.Linq.Expressions;

namespace IMP.Shared
{
    /// <summary>
    /// Provides strong-typed reflection
    /// </summary>
    internal static class Reflect<TTarget>
    {
        #region action methods
        /// <summary>
        /// Gets the method represented by the lambda expression.
        /// </summary>
        /// <exception cref="ArgumentNullException">The <paramref name="method"/> is null.</exception>
        /// <exception cref="ArgumentException">The <paramref name="method"/> is not a lambda expression or it does not represent a method.</exception>
        public static System.Reflection.MethodInfo Infoof(Expression<Func<TTarget, Action>> method)
        {
            return GetMethodInfo(method);
        }

        /// <summary>
        /// Gets the method represented by the lambda expression.
        /// </summary>
        /// <exception cref="ArgumentNullException">The <paramref name="method"/> is null.</exception>
        /// <exception cref="ArgumentException">The <paramref name="method"/> is not a lambda expression or it does not represent a method.</exception>
        public static System.Reflection.MethodInfo Infoof<T>(Expression<Func<TTarget, Action<T>>> method)
        {
            return GetMethodInfo(method);
        }

        /// <summary>
        /// Gets the method represented by the lambda expression.
        /// </summary>
        /// <exception cref="ArgumentNullException">The <paramref name="method"/> is null.</exception>
        /// <exception cref="ArgumentException">The <paramref name="method"/> is not a lambda expression or it does not represent a method.</exception>
        public static System.Reflection.MethodInfo Infoof<T1, T2>(Expression<Func<TTarget, Action<T1, T2>>> method)
        {
            return GetMethodInfo(method);
        }

        /// <summary>
        /// Gets the method represented by the lambda expression.
        /// </summary>
        /// <exception cref="ArgumentNullException">The <paramref name="method"/> is null.</exception>
        /// <exception cref="ArgumentException">The <paramref name="method"/> is not a lambda expression or it does not represent a method.</exception>
        public static System.Reflection.MethodInfo Infoof<T1, T2, T3>(Expression<Func<TTarget, Action<T1, T2, T3>>> method)
        {
            return GetMethodInfo(method);
        }

        /// <summary>
        /// Gets the method represented by the lambda expression.
        /// </summary>
        /// <exception cref="ArgumentNullException">The <paramref name="method"/> is null.</exception>
        /// <exception cref="ArgumentException">The <paramref name="method"/> is not a lambda expression or it does not represent a method.</exception>
        public static System.Reflection.MethodInfo Infoof<TResult>(Expression<Func<TTarget, Func<TResult>>> method)
        {
            return GetMethodInfo(method);
        }

        /// <summary>
        /// Gets the method represented by the lambda expression.
        /// </summary>
        /// <exception cref="ArgumentNullException">The <paramref name="method"/> is null.</exception>
        /// <exception cref="ArgumentException">The <paramref name="method"/> is not a lambda expression or it does not represent a method.</exception>
        public static System.Reflection.MethodInfo Infoof<T, TResult>(Expression<Func<TTarget, Func<T, TResult>>> method)
        {
            return GetMethodInfo(method);
        }

        /// <summary>
        /// Gets the method represented by the lambda expression.
        /// </summary>
        /// <exception cref="ArgumentNullException">The <paramref name="method"/> is null.</exception>
        /// <exception cref="ArgumentException">The <paramref name="method"/> is not a lambda expression or it does not represent a method.</exception>
        public static System.Reflection.MethodInfo Infoof<T1, T2, TResult>(Expression<Func<TTarget, Func<T1, T2, TResult>>> method)
        {
            return GetMethodInfo(method);
        }

        /// <summary>
        /// Gets the method represented by the lambda expression.
        /// </summary>
        /// <exception cref="ArgumentNullException">The <paramref name="method"/> is null.</exception>
        /// <exception cref="ArgumentException">The <paramref name="method"/> is not a lambda expression or it does not represent a method.</exception>
        public static System.Reflection.MethodInfo Infoof<T1, T2, T3, TResult>(Expression<Func<TTarget, Func<T1, T2, T3, TResult>>> method)
        {
            return GetMethodInfo(method);
        }

        /// <summary>
        /// Gets the property or field represented by the lambda expression.
        /// </summary>
        /// <exception cref="ArgumentNullException">The <paramref name="member"/> is null.</exception>
        /// <exception cref="ArgumentException">The <paramref name="member"/> is not a lambda expression or it does not represent a property or field access.</exception>
        public static System.Reflection.MemberInfo Infoof<T>(Expression<Func<TTarget, T>> member)
        {
            if (member == null)
            {
                throw new ArgumentNullException("member");
            }

            var me = member.Body as MemberExpression;
            if (me == null)
            {
                throw new ArgumentException("member must be a member access expression", "member");
            }
            return me.Member;
        }
        #endregion

        #region private member functions
        private static System.Reflection.MethodInfo GetMethodInfo<TMethod>(Expression<Func<TTarget, TMethod>> method)
        {
            if (method == null)
            {
                throw new ArgumentNullException("method");
            }
            var convert = method.Body as UnaryExpression;
            if (convert == null || convert.NodeType != ExpressionType.Convert)
            {
                throw new ArgumentException("method must be a convert expression", "method");
            }
            var methodCall = convert.Operand as MethodCallExpression;
            if (methodCall == null || !string.Equals(methodCall.Method.Name, "CreateDelegate", StringComparison.OrdinalIgnoreCase))
            {
                throw new ArgumentException("method must be a CreateDelegate method call", "method");
            }

            return (System.Reflection.MethodInfo)((ConstantExpression)methodCall.Arguments[2]).Value;
        }
        #endregion
    }
}

A samozřejmě si také ukážeme jak vypadá použití této třídy:

public class SelectJidelnaViewModel: INotifyPropertyChanged
{
    //...

    private Jidelna m_SelectedJidelna;

    public Jidelna SelectedJidelna
    {
        get { return m_SelectedJidelna; }
        private set
        {
            if (m_SelectedJidelna != value)
            {
                m_SelectedJidelna = value;
                OnPropertyChanged(Reflect<SelectJidelnaViewModel>.Infoof(o => o.SelectedJidelna).Name);
            }
        }
    }

    private void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Příklad ukazuje, jak získat název vlastnosti SelectedJidelna třídy SelectJidelnaViewModel (pro volání metody OnPropertyChanged(string)) v setru dané vlastnosti. Získání nějaké metody by bylo obdobné.

Konkrétně výše uvedený kód lze ale ještě zjednodušit a to následovně:

public class SelectJidelnaViewModel: INotifyPropertyChanged
{
    //...

    private Jidelna m_SelectedJidelna;

    public Jidelna SelectedJidelna
    {
        get { return m_SelectedJidelna; }
        private set
        {
            if (m_SelectedJidelna != value)
            {
                m_SelectedJidelna = value;
                OnPropertyChanged(o => o.SelectedJidelna);
            }
        }
    }

    private void OnPropertyChanged<T>(System.Linq.Expressions.Expression<Func<SelectJidelnaViewModel, T>> property)
    {
        if (PropertyChanged != null)
        {
            string propertyName = Reflect<SelectJidelnaViewModel>.Infoof(property).Name;
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Nyní má metoda OnPropertyChanged argument property což je přímo expression odkazující se na požadovanou vlastnost a volání třídy Reflect je přesunuto až to této metody (kterou případně můžeme celou přesunout do nějaké base třídy, ze které budeme dědit).

Závěrem ještě jednou upozorním, že třída, kterou jsme si ukázali, má samozřejmě své omezení a nepůjde jí použít v některých okrajových případech (nakonec z podobných důvodů byly příliš vysoké i odhadované náklady na implementaci operátoru infoof přímo do jazyku C#), ale v běžných případech jako je již zmiňované MVVM rozhodně význam a uplatnění najde.

 

hodnocení článku

0 bodů / 1 hlasů       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:  
Heslo:  

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback