Arbre d'expression - compiler le lambda interne dans le lambda externe - résolution de la portée

c# dynamic-language-runtime expression-trees scope

Question

Je crée un arbre d'expression et il y a une situation dans laquelle je dois créer un lambda dans un autre lambda et en stocker un dans une classe et ajouter cette classe dans l'arbre d'expression. Voici un exemple simple de ce que j'essaie de faire (ce code ne compile pas):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace SimpleTest {
    public class LambdaWrapper {
        private Delegate compiledLambda;
        public LambdaWrapper(Delegate compiledLambda) {
            this.compiledLambda = compiledLambda;
        }
        public dynamic Execute() {
            return compiledLambda.DynamicInvoke();
        }
    }

    public class ForSO {

        public ParameterExpression Param;

        public LambdaExpression GetOuterLambda() {
            IList<Expression> lambdaBody = new List<Expression>();
            Param = Expression.Parameter(typeof(object), "Param");
            lambdaBody.Add(Expression.Assign(
                            Param, 
                            Expression.Constant("Value of 'param' valiable"))
                          );

            lambdaBody.Add(Expression.Call(
                            null, 
                            typeof(ForSO).GetMethod("Write"), 
                            Param)
                          );

            Delegate compiledInnerLambda = GetInnerLambda().Compile();
            LambdaWrapper wrapper = new LambdaWrapper(compiledInnerLambda);
            lambdaBody.Add(Expression.Constant(wrapper));
            //lambdaBody.Add(GetInnerLambda());
            return Expression.Lambda(
                        Expression.Block(
                                new ParameterExpression[] { Param }, 
                                lambdaBody));
        }

        public LambdaExpression GetInnerLambda() {
            return Expression.Lambda(
                    Expression.Block(
                        Expression.Call(null, 
                                typeof(ForSO).GetMethod("Write"), 
                                Expression.Constant("Inner lambda start")),
                        Expression.Call(null, 
                                typeof(ForSO).GetMethod("Write"), 
                                Param),
                        Expression.Call(null, 
                                typeof(ForSO).GetMethod("Write"), 
                                Expression.Constant("Inner lambda end"))
                    )
                );
        }

        public static void Write(object toWrite) {
            Console.WriteLine(toWrite);
        }

        public static void Main(string[] args) {
            ForSO so = new ForSO();
            LambdaWrapper wrapper = so.GetOuterLambda().Compile()
                                      .DynamicInvoke() as LambdaWrapper;
            wrapper.Execute();
            //(so.GetOuterLambda().Compile().DynamicInvoke() as Delegate).DynamicInvoke();
        }
    }
}

Le problème est dans la ligne GetInnerLambda().Compile() dans la méthode GetOuterLambda . Je suis au courant d'une solution - c'est dans la partie commentée du code. Avec cela, tout fonctionne bien, mais j'ai besoin d'un wrapper comme valeur de retour, pas de sous-arbre d'expression (il serait peut-être correct de stocker le sous-arbre lambda interne dans LambdaWrapper et de le compiler ultérieurement, mais le même problème se produit).

L'erreur que Unhandled Exception: System.InvalidOperationException: variable 'Param' of type 'System.Object' referenced from scope '', but it is not defined est Unhandled Exception: System.InvalidOperationException: variable 'Param' of type 'System.Object' referenced from scope '', but it is not defined .

Si j'ajoute Param pour bloquer les variables dans lambda interne, le code est compilé, mais Param n'a pas de valeur affectée dans lambda externe (et cela a du sens).

Comment cela peut-il être résolu?

Réponse acceptée

Avec l'aide de Balazs Tihanyi, j'ai trouvé une solution qui me convient exactement comme j'en ai besoin. C'est un peu plus de travail parce que je devais créer des classeurs, mais comme je les avais déjà dans mon projet principal, j'ai donc créé des classeurs factices pour que cet exemple fonctionne.

Ceci est ma solution finale:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Reflection;
using System.Dynamic;


namespace SimpleTest {
    public class MyCreateBinder : CreateInstanceBinder {
        public MyCreateBinder(CallInfo info) : base(info) { }

        public override DynamicMetaObject FallbackCreateInstance(
                                        DynamicMetaObject target,
                                        DynamicMetaObject[] args,
                                        DynamicMetaObject errorSuggestion) {
            var param = args[0].Value;

            Type toCreate = target.Value as Type;
            var ctors = toCreate.GetConstructors()
                        .Where(c => c.GetParameters().Length == args.Length)
                        .ToArray();

            if (ctors.Length == 0)
                throw 
                    new Exception(
                      String.Format(
                      "Can not find constructor for '{0}' with {1} parameters",
                      toCreate, args.Length));
            ConstructorInfo ctorToUse = ctors[0];
            return new DynamicMetaObject(
                            Expression.New(
                                ctorToUse,
                                args.Select(a => a.Expression).ToList()),
                       BindingRestrictions.Empty);
        }
    }

    public class MySetMemberBinder : SetMemberBinder {

        public MySetMemberBinder(string name) : base(name, false) { }

        public override DynamicMetaObject FallbackSetMember(
                                DynamicMetaObject target,
                                DynamicMetaObject value,
                                DynamicMetaObject errorSuggestion) {

            throw new NotImplementedException();
        }
    }

    public class MyGetMemberBinder : GetMemberBinder {
        public MyGetMemberBinder(string name) : base(name, false) { }

        public override DynamicMetaObject FallbackGetMember(
                                        DynamicMetaObject target,
                                        DynamicMetaObject errorSuggestion) {
            throw new NotImplementedException();
        }
    }

    public class MyInvokeMemberBinder : InvokeMemberBinder {
        public MyInvokeMemberBinder(string name, CallInfo callInfo) 
            : base(name, false, callInfo) { }

        public override DynamicMetaObject FallbackInvokeMember(
                                    DynamicMetaObject target,
                                    DynamicMetaObject[] args,
                                    DynamicMetaObject errorSuggestion) {
            var a = this;
            throw new NotImplementedException();
        }

        public override DynamicMetaObject FallbackInvoke(
                                    DynamicMetaObject target,
                                    DynamicMetaObject[] args,
                                    DynamicMetaObject errorSuggestion) {
            throw new NotImplementedException();
        }
    }

    public class LambdaWrapper : IDynamicMetaObjectProvider {
        private Delegate compiledLambda;
        private LambdaExpression exp;

        public LambdaWrapper(LambdaExpression exp) {
            this.exp = exp;
            this.compiledLambda = exp.Compile();
        }
        public dynamic Execute(dynamic param) {
            return compiledLambda.DynamicInvoke(param);
        }

        public DynamicMetaObject GetMetaObject(Expression parameter) {
            return new MetaLambdaWrapper(parameter, this);
        }
    }

    public class MetaLambdaWrapper : DynamicMetaObject {
        public MetaLambdaWrapper(Expression parameter, object value) : 
            base(parameter, BindingRestrictions.Empty, value) { }

        public override DynamicMetaObject BindInvokeMember(
                                    InvokeMemberBinder binder,
                                    DynamicMetaObject[] args) {
            MethodInfo method = this.Value.GetType().GetMethod(binder.Name);
            return new DynamicMetaObject(
                        Expression.Call(
                            Expression.Constant(this.Value),
                                method,
                                    args.Select(a => a.Expression)),
                        BindingRestrictions.GetTypeRestriction(
                            this.Expression, 
                            typeof(LambdaWrapper)));
        }
    }


    public class ForSO {
        public ParameterExpression Param;
        public LambdaExpression GetOuterLambda() {
            Expression wrapper;
            IList<Expression> lambdaBody = new List<Expression>();
            Param = Expression.Parameter(typeof(object), "Param");
            lambdaBody.Add(Expression.Assign(
                            Param,
                            Expression.Constant("Value of 'param' variable"))
                          );
            lambdaBody.Add(Expression.Call(
                            null,
                            typeof(ForSO).GetMethod("Write"),
                            Param)
                          );

            wrapper = Expression.Dynamic(
                                new MyCreateBinder(new CallInfo(1)),
                                typeof(object),
                                Expression.Constant(typeof(LambdaWrapper)),
                                Expression.Quote(GetInnerLambda()));


            lambdaBody.Add(
                Expression.Dynamic(
                    new MyInvokeMemberBinder("Execute", new CallInfo(1)),
                    typeof(object),
                    wrapper,
                Expression.Constant("calling inner lambda from outer")));

            lambdaBody.Add(wrapper);

            return Expression.Lambda(
                        Expression.Block(
                                new ParameterExpression[] { Param },
                                lambdaBody));
        }

        public LambdaExpression GetInnerLambda() {
            ParameterExpression innerParam = Expression.Parameter(
                                                typeof(object), 
                                                "innerParam");
            return Expression.Lambda(
                    Expression.Block(
                        Expression.Call(null,
                                typeof(ForSO).GetMethod("Write"),
                                Expression.Constant("Inner lambda start")),
                        Expression.Call(null,
                                typeof(ForSO).GetMethod("Write"),
                                innerParam),
                        Expression.Call(null,
                                typeof(ForSO).GetMethod("Write"),
                                Param),
                        Expression.Call(null,
                                typeof(ForSO).GetMethod("Write"),
                                Expression.Constant("Inner lambda end"))
                    ),
                    innerParam
                );
        }

        public static void Write(object toWrite) {
            Console.WriteLine(toWrite);
        }

        public static void Main(string[] args) {
            Console.WriteLine("-----------------------------------");
            ForSO so = new ForSO();

            LambdaWrapper wrapper = (LambdaWrapper) so.GetOuterLambda()
                                                    .Compile()
                                                    .DynamicInvoke();
            Console.WriteLine("-----------------------------------");
            wrapper.Execute("Calling from main");
        }
    }

}

Réponse populaire

Eh bien, puisque vous ne pouvez pas utiliser Param comme valeur constante dans votre expression lambda interne, je vous suggère d'ajouter un paramètre lambda à votre expression:

public LambdaExpression GetInnerLambda()
{
    var param = Expression.Parameter(typeof(object));
    return Expression.Lambda(
        Expression.Block(
            Expression.Call(null,
                typeof(ForSO).GetMethod("Write"),
                Expression.Constant("Inner lambda start")),
            Expression.Call(null,
                typeof(ForSO).GetMethod("Write"),
                param),
            Expression.Call(null,
                typeof(ForSO).GetMethod("Write"),
                Expression.Constant("Inner lambda end"))
        ),
        param
    );
}

LambdaWrapper ensuite la valeur du paramètre dans votre classe LambdaWrapper et utilisez-la ultérieurement en tant qu'argument dans l'appel DynamicInvoke :

public LambdaExpression GetInnerLambda()
{
    var param = Expression.Parameter(typeof(object));
    return Expression.Lambda(
        Expression.Block(
            Expression.Call(null,
                typeof(ForSO).GetMethod("Write"),
                Expression.Constant("Inner lambda start")),
            Expression.Call(null,
                typeof(ForSO).GetMethod("Write"),
                param),
            Expression.Call(null,
                typeof(ForSO).GetMethod("Write"),
                Expression.Constant("Inner lambda end"))
        ),
        param
    );
}

Cela fonctionne, mais le seul problème est qu’il appellera WriteLine on Param , qui est un objet ParameterExpression. Pour résoudre ce problème, vous devez créer la classe wrapper de manière dynamique dans votre arbre d'expression:

public LambdaExpression GetInnerLambda()
{
    var param = Expression.Parameter(typeof(object));
    return Expression.Lambda(
        Expression.Block(
            Expression.Call(null,
                typeof(ForSO).GetMethod("Write"),
                Expression.Constant("Inner lambda start")),
            Expression.Call(null,
                typeof(ForSO).GetMethod("Write"),
                param),
            Expression.Call(null,
                typeof(ForSO).GetMethod("Write"),
                Expression.Constant("Inner lambda end"))
        ),
        param
    );
}

Ensuite, il utilisera la valeur assignée de Param . Et comme vous n'utilisez pas Param dehors de GetOuterLambda , vous pouvez maintenant l'utiliser comme variable locale.

MODIFIER:

Voici ma deuxième tentative pour résoudre ce problème:

public LambdaExpression GetInnerLambda()
{
    var param = Expression.Parameter(typeof(object));
    return Expression.Lambda(
        Expression.Block(
            Expression.Call(null,
                typeof(ForSO).GetMethod("Write"),
                Expression.Constant("Inner lambda start")),
            Expression.Call(null,
                typeof(ForSO).GetMethod("Write"),
                param),
            Expression.Call(null,
                typeof(ForSO).GetMethod("Write"),
                Expression.Constant("Inner lambda end"))
        ),
        param
    );
}

Cette approche compile ce lambda interne lorsque vous exécutez le délégué externe. En faisant cela, Param sera assigné avant que le lambda interne soit compilé.




Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi