Jak rozbić łańcuch wyrażeń dostępu członków?

Wersja skrócona (TL;DR):

Załóżmy, że mam wyrażenie, które jest tylko łańcuchem operatorów dostępu do członków:

Expression<Func<Tx, Tbaz>> e = x => x.foo.bar.baz;

Można myśleć o tym wyrażeniu jako o kompozycji pod-wyrażeń, z których każde składa się z jednej operacji dostępu członowego:

Expression<Func<Tx, Tfoo>>   e1 = (Tx x) => x.foo;
Expression<Func<Tfoo, Tbar>> e2 = (Tfoo foo) => foo.bar;
Expression<Func<Tbar, Tbaz>> e3 = (Tbar bar) => bar.baz;

Chcę podzielić e NA pod - wyrażenia składowe, aby móc pracować z nimi indywidualnie.

Jeszcze Krótsza Wersja:

Jeśli mam wyrażenie x => x.foo.bar, to już wiem jak zerwać x => x.foo. Jak mogę wyciągnąć inne wyrażenie foo => foo.bar?

Dlaczego to robię:

Próbuję symulować "podnoszenie" operatora dostępu członka w C#, tak jak egzystencjalny operator dostępu CoffeeScript ?.. Eric Lippert stwierdził, że dla C#rozważano podobny operator, ale nie było budżetu na jego wdrożenie.

Gdyby taki operator istniał w C#, można by zrobić coś takiego:

value = target?.foo?.bar?.baz;

Jeśli jakaś część łańcuch target.foo.bar.baz okazał się być null, wtedy całość oceniałaby NA null, unikając w ten sposób Nullreferencexception.

Chcę Lift} metodę rozszerzenia, która może symulować tego rodzaju rzeczy:

value = target.Lift(x => x.foo.bar.baz); //returns target.foo.bar.baz or null

Co próbowałem:

Mam coś, co kompiluje, i to działa. Jest to jednak niekompletne, ponieważ Wiem tylko, jak zachować lewą stronę wyrażenia dostępu członka. Mogę zmienić x => x.foo.bar.baz w x => x.foo.bar, ale nie wiem, jak utrzymać bar => bar.baz.

Więc kończy się robiąc coś takiego (pseudokod):

return (x => x)(target) == null ? null
       : (x => x.foo)(target) == null ? null
       : (x => x.foo.bar)(target) == null ? null
       : (x => x.foo.bar.baz)(target);

Oznacza to, że ostatnie kroki w wyrażeniu są wielokrotnie oceniane. Może to nic wielkiego, jeśli są tylko właściwościami na obiektach POCO, ale zamień je w wywołania metod, a nieefektywność (i potencjalne skutki uboczne) staną się o wiele bardziej oczywiste: {]}

//still pseudocode
return (x => x())(target) == null ? null
       : (x => x().foo())(target) == null ? null
       : (x => x().foo().bar())(target) == null ? null
       : (x => x().foo().bar().baz())(target);

Kod:

static TResult Lift<T, TResult>(this T target, Expression<Func<T, TResult>> exp)
    where TResult : class
{
    //omitted: if target can be null && target == null, just return null

    var memberExpression = exp.Body as MemberExpression;
    if (memberExpression != null)
    {
        //if memberExpression is {x.foo.bar}, then innerExpression is {x.foo}
        var innerExpression = memberExpression.Expression;
        var innerLambda = Expression.Lambda<Func<T, object>>(
                              innerExpression, 
                              exp.Parameters
                          );  

        if (target.Lift(innerLambda) == null)
        {
            return null;
        }
        else
        {
            ////This is the part I'm stuck on. Possible pseudocode:
            //var member = memberExpression.Member;              
            //return GetValueOfMember(target.Lift(innerLambda), member);
        }
    }

    //For now, I'm stuck with this:
    return exp.Compile()(target);
}

To było luźno zainspirowane tą odpowiedzią .


Alternatywy dla Metoda Lift, i dlaczego nie mogę ich użyć:

The Maybe monad

value = x.ToMaybe()
         .Bind(y => y.foo)
         .Bind(f => f.bar)
         .Bind(b => b.baz)
         .Value;
Plusy:
  1. używa istniejącego wzorca, który jest popularny w programowaniu funkcyjnym
  2. ma inne zastosowania poza podnoszonym dostępem do członka
Wady:
    To zbyt gadatliwe. Nie chcę, żeby za każdym razem, gdy chcę wywiercić kilku członków. Nawet jeśli zaimplementuję SelectMany i użyję składni zapytań, IMHO będzie to wyglądało bardziej niechlujnie, nie mniej.
  1. muszę ręcznie przepisać x.foo.bar.baz jako jego poszczególne komponenty, co oznacza, że muszę wiedzieć, jakie są w czasie kompilacji. Nie mogę po prostu użyć wyrażenia ze zmiennej takiej jak result = Lift(expr, obj);.
  2. nie do końca zaprojektowane do tego, co próbuję zrobić, i nie czuję się idealnie dopasowany.

ExpressionVisitor

Zmodyfikowałem metodę LiftMemberAccessToNull Iana Griffitha do ogólnej metody rozszerzenia, która może być używana zgodnie z opisem. Kod też jest długo tu zawierać, ale wrzucę Gist, jeśli ktoś jest zainteresowany.

Plusy:
  1. wynika ze składniresult = target.Lift(x => x.foo.bar.baz)
  2. działa świetnie, jeśli każdy krok w łańcuchu zwraca typ referencyjny lub nie-nullable value type
Wady:
  1. dławi się, jeśli jakikolwiek element w łańcuchu jest typem wartości nullable, co naprawdę ogranicza jego przydatność do mnie. Potrzebuję go do pracy dla Nullable<DateTime> członków.

Spróbuj / Złap

try 
{ 
    value = x.foo.bar.baz; 
}
catch (NullReferenceException ex) 
{ 
    value = null; 
}

Jest to najbardziej oczywisty sposób, i to, co Użyję, jeśli nie znajdę bardziej eleganckiego sposobu.

Plusy:
    To proste.
  1. to oczywiste, do czego służy kod.
  2. Nie muszę się martwić o sprawy na krawędzi.
Wady:
  1. jest brzydki i gadatliwy
  2. Nie jest to jednak żaden problem, ponieważ nie jest to możliwe.]}
  3. jest to blok instrukcji, więc nie mogę sprawić, aby emitował drzewo wyrażeń dla LINQ
  4. czuję się jak przyznanie się do porażki

Nie będę kłamać; " nie przyznając się do porażki " jest głównym powodem, dla którego jestem taki uparty. Mój instynkt mówi, że musi być na to elegancki sposób, ale znalezienie go było wyzwaniem. nie mogę uwierzyć, że tak łatwo jest uzyskać dostęp do lewej strony wyrażenia, ale prawa strona jest prawie nieosiągalna.

Naprawdę mam tutaj dwa problemy, więc zaakceptuję wszystko, co rozwiąże oba:

  • rozkład ekspresji, który zachowuje obie strony, ma rozsądną wydajność i działa na każdym Typ
  • null-propagujący dostęp członka

Aktualizacja:

Null-propagujący dostęp członka jest planowane dla zawarte w C # 6.0 . Chciałbym jednak znaleźć rozwiązanie rozkładu ekspresji.

Author: Community, 2012-06-19

1 answers

Jeśli jest to tylko prosty łańcuch wyrażeń dostępu członka, istnieje proste rozwiązanie:

public static TResult Lift<T, TResult>(this T target, Expression<Func<T, TResult>> exp)
    where TResult : class
{
    return (TResult) GetValueOfExpression(target, exp.Body);
}

private static object GetValueOfExpression<T>(T target, Expression exp)
{
    if (exp.NodeType == ExpressionType.Parameter)
    {
        return target;
    }
    else if (exp.NodeType == ExpressionType.MemberAccess)
    {
        var memberExpression = (MemberExpression) exp;
        var parentValue = GetValueOfExpression(target, memberExpression.Expression);

        if (parentValue == null)
        {
            return null;
        }
        else
        {
            if (memberExpression.Member is PropertyInfo)
                return ((PropertyInfo) memberExpression.Member).GetValue(parentValue, null);
            else
                return ((FieldInfo) memberExpression.Member).GetValue(parentValue);
        }
    }
    else
    {
        throw new ArgumentException("The expression must contain only member access calls.", "exp");
    }
}

EDIT

Jeśli chcesz dodać obsługę wywołań metod, użyj tej zaktualizowanej metody:

private static object GetValueOfExpression<T>(T target, Expression exp)
{
    if (exp == null)
    {
        return null;
    }
    else if (exp.NodeType == ExpressionType.Parameter)
    {
        return target;
    }
    else if (exp.NodeType == ExpressionType.Constant)
    {
        return ((ConstantExpression) exp).Value;
    }
    else if (exp.NodeType == ExpressionType.Lambda)
    {
        return exp;
    }
    else if (exp.NodeType == ExpressionType.MemberAccess)
    {
        var memberExpression = (MemberExpression) exp;
        var parentValue = GetValueOfExpression(target, memberExpression.Expression);

        if (parentValue == null)
        {
            return null;
        }
        else
        {
            if (memberExpression.Member is PropertyInfo)
                return ((PropertyInfo) memberExpression.Member).GetValue(parentValue, null);
            else
                return ((FieldInfo) memberExpression.Member).GetValue(parentValue);
        }
    }
    else if (exp.NodeType == ExpressionType.Call)
    {
        var methodCallExpression = (MethodCallExpression) exp;
        var parentValue = GetValueOfExpression(target, methodCallExpression.Object);

        if (parentValue == null && !methodCallExpression.Method.IsStatic)
        {
            return null;
        }
        else
        {
            var arguments = methodCallExpression.Arguments.Select(a => GetValueOfExpression(target, a)).ToArray();

            // Required for comverting expression parameters to delegate calls
            var parameters = methodCallExpression.Method.GetParameters();
            for (int i = 0; i < parameters.Length; i++)
            {
                if (typeof(Delegate).IsAssignableFrom(parameters[i].ParameterType))
                {
                    arguments[i] = ((LambdaExpression) arguments[i]).Compile();
                }
            }

            if (arguments.Length > 0 && arguments[0] == null && methodCallExpression.Method.IsStatic &&
                methodCallExpression.Method.IsDefined(typeof(ExtensionAttribute), false)) // extension method
            {
                return null;
            }
            else
            {
                return methodCallExpression.Method.Invoke(parentValue, arguments);
            }
        }
    }
    else
    {
        throw new ArgumentException(
            string.Format("Expression type '{0}' is invalid for member invoking.", exp.NodeType));
    }
}
 8
Author: Balazs Tihanyi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-06-22 14:33:44