Comment décomposer une chaîne d'expressions d'accès de membre?

c# expression-trees lambda lifting nullreferenceexception

Question

La version courte (TL; DR):

Supposons que j'ai une expression qui ne soit qu'une chaîne d'opérateurs d'accès membres:

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

Vous pouvez considérer cette expression comme une composition de sous-expressions, chacune comprenant une opération d'accès membre:

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;

Ce que je veux faire est de briser e vers le bas dans ces sous-expressions composantes afin que je puisse travailler avec eux individuellement.

La version encore plus courte:

Si j'ai l'expression x => x.foo.bar , je sais déjà comment rompre x => x.foo . Comment puis-je extraire l'autre sous-expression, foo => foo.bar ?

Pourquoi je fais ça:

J'essaie de simuler la "levée" de l'opérateur d'accès membre en C #, comme l'opérateur d'accès existentiel de CoffeeScript ?. . Eric Lippert a déclaré qu'un opérateur similaire avait été envisagé pour C #, mais qu'il n'y avait pas de budget pour le mettre en œuvre.

Si un tel opérateur existait en C #, vous pourriez faire quelque chose comme ceci:

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

Si une partie quelconque de la chaîne target.foo.bar.baz se target.foo.bar.baz nulle, alors tout le target.foo.bar.baz serait évalué à null, évitant ainsi une exception NullReferenceException.

Je veux une méthode d'extension Lift qui puisse simuler ce genre de chose:

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

Ce que j'ai essayé:

J'ai quelque chose qui compile, et ça marche en quelque sorte. Cependant, c'est incomplet parce que je sais seulement comment garder le côté gauche d'une expression d'accès membre. Je peux transformer x => x.foo.bar.baz en x => x.foo.bar , mais je ne sais pas comment garder bar => bar.baz .

Donc, il finit par faire quelque chose comme ça (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);

Cela signifie que les étapes les plus à gauche de l'expression sont évaluées maintes et maintes fois. Peut-être que ce n’est pas grave si ce ne sont que des propriétés sur des objets POCO, mais si vous les transformez en appels de méthodes et que l’inefficacité (et les effets secondaires potentiels) deviennent bien plus évidents:

//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);

Le code:

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);
}

Cela a été vaguement inspiré par cette réponse .


Alternatives à une méthode de levage et pourquoi je ne peux pas les utiliser:

La monade peut-être

value = x.ToMaybe()
         .Bind(y => y.foo)
         .Bind(f => f.bar)
         .Bind(b => b.baz)
         .Value;
Avantages:
  1. Utilise un modèle existant qui est populaire dans la programmation fonctionnelle
  2. A d'autres utilisations en plus de l'accès des membres
Les inconvénients:
  1. C'est trop verbeux. Je ne veux pas d'une chaîne massive d'appels de fonctions à chaque fois que je veux explorer quelques membres. Même si SelectMany et utilise la syntaxe de requête, mon SelectMany sera plus compliqué, pas moins.
  2. Je dois réécrire manuellement x.foo.bar.baz tant que composants individuels, ce qui signifie que je dois savoir ce qu'ils sont au moment de la compilation. Je ne peux pas simplement utiliser une expression d'une variable telle que result = Lift(expr, obj); .
  3. Pas vraiment conçu pour ce que j'essaie de faire, et ne me semble pas un ajustement parfait.

ExpressionVisitor

J'ai modifié la méthode LiftMemberAccessToNull de Ian Griffith en une méthode d'extension générique qui peut être utilisée comme je l'ai décrit. Le code est trop long pour être inclus ici, mais je posterai un résumé si quelqu'un est intéressé.

Avantages:
  1. Suit la result = target.Lift(x => x.foo.bar.baz)
  2. Fonctionne très bien si chaque étape de la chaîne retourne un type de référence ou un type de valeur non nullable
Les inconvénients:
  1. Cela s'étouffe si un membre de la chaîne est un type de valeur nullable, ce qui me limite vraiment son utilité. J'en ai besoin pour les membres Nullable<DateTime> .

Essayer / attraper

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

C’est le moyen le plus évident, et c’est ce que je vais utiliser si je ne trouve pas un moyen plus élégant.

Avantages:
  1. C'est simple.
  2. Il est évident que le code est pour.
  3. Je n'ai pas à m'inquiéter des cas de bord.
Les inconvénients:
  1. C'est moche et prolixe
  2. Le bloc try / catch est un succès non négligeable *
  3. C'est un bloc d'instructions, je ne peux donc pas lui faire émettre un arbre d'expression pour LINQ
  4. C'est comme admettre sa défaite

Je ne vais pas mentir; "ne pas admettre sa défaite" est la raison principale pour laquelle je suis si têtu. Mon instinct dit qu'il doit y avoir un moyen élégant de le faire, mais le trouver a été un défi. Je n'arrive pas à croire qu'il soit si facile d'accéder au côté gauche d'une expression, pourtant le côté droit est pratiquement inaccessible.

J'ai vraiment deux problèmes ici, donc je vais accepter tout ce qui résout l'un ou l'autre:

  • Décomposition d'expression qui préserve les deux côtés, a des performances raisonnables et fonctionne sur tout type
  • Accès membre à propagation nulle

Mettre à jour:

Un accès de membre à propagation nulle est prévu pour être inclus dans C # 6.0 . J'aimerais quand même une solution à la décomposition d'expression, cependant.

Réponse acceptée

S'il ne s'agit que d'une simple chaîne d'expressions d'accès aux membres, il existe une solution simple:

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");
    }
}

MODIFIER

Si vous souhaitez ajouter un support pour les appels de méthode, utilisez cette méthode mise à jour:

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));
    }
}


Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow