Foreach loop usando árboles de expresiones

.net c# expression-trees lambda

Pregunta

He visto este problema al crear un árbol dinámico de expresiones y árboles de expresiones y expresiones y, como soy nuevo en los árboles de expresiones, todavía estoy luchando por entender cómo lograr lo que quiero.

Un objeto artificial está abajo.

    public class TestObject
    {
        public TestObject()
        {
            ClassList = new List<Class>();
        }
        public int Age { get; set; }
        public List<Class> ClassList { get; set; } 
    }

    public class Class
    {
        public string Name { get; set; }
        public int ClassId { get; set; }
    }

En el tiempo de ejecución, repito cada una de las propiedades y genero un Delegado que hace una conversión a la cadena de esa propiedad. Tengo todo eso funcionando. El problema con el que tengo que lidiar ahora es que, para el tipo de lista, necesito poder aplicar un conjunto de acciones a cada elemento en la propiedad ClassList, por lo que necesito un foreach que me permita hacer eso.

Actualmente tengo esto

//type==TestObject at runtime
//propertyName == "ClassList"
   ParameterExpression recordExpression = Expression.Parameter(type, "record");

   memberExpression = MemberExpression.Property(recordExpression, propertyName);

   Type getEnumerableDelegateType =
                typeof(Func<,>).MakeGenericType(new Type[] { type, memberExpression.Type}); 

   var getList = Expression.Lambda(getEnumerableDelegateType, memberExpression, recordExpression);

GetList cuando se compila e invoca devuelve la lista como se esperaba. Con lo que estoy luchando es cómo crear una expresión que usará el resultado de la expresión lambda e itere sobre ella aplicando el conjunto de acciones que ya he creado para cada elemento de la Clase.

En última instancia, estoy buscando una firma lambda que coincida con la firma globalAction a continuación

   var getListFunc = new Func<TestObject, List<Class>>((TestObject obj1) => obj1.ClassList);

   Action<List<Class>> listAction = delegate(List<Class> data)
                {
                    foreach (var dataChannelWithUnitse in data)
                    {
                        //Apply generated delegate
                    }
                };

     Action<TestObject> overallAction = delegate(TestObject data)
                {
                    var x = getListFunc.Invoke(data);
                    listAction.Invoke(x as List<Class>);
                };

Cualquier ayuda es apreciada para ayudarme a entender cómo hacer esto.

Actualmente tengo esto, que es una excepción con la variable 'Entrada' del tipo 'TestObject' referenciado desde el alcance '', pero no está definido

    var typeParam = Expression.Parameter(type, "Input");
    var listVariable = Expression.Variable(memberExpression.Type, "List");
    var enumerator = Expression.Variable(typeof(IEnumerator<>).MakeGenericType(dataType));


    var enumeratorType = typeof(IEnumerator<>).MakeGenericType(dataType);
    var enumerableType = typeof(IEnumerable<>).MakeGenericType(dataType);
    var enumerableParam = Expression.Parameter(enumerableType, "ExtractedCollection");

    var getEnumeratorFunc = Expression.Call(enumerableParam, enumerableType.GetMethod("GetEnumerator"));
    var getEnumeratorLambda = Expression.Lambda(getEnumeratorFunc, enumerableParam);

    var t1 = Expression.Assign(listVariable, Expression.Invoke(getListLambda, typeParam));
    var t2 = Expression.Assign(enumerator, Expression.Invoke(getEnumeratorLambda, listVariable));


    var @break = Expression.Label();

    var funcBlock = Expression.Block(
        new ParameterExpression[] { listVariable, enumerator},

   t1,
   t2,

    Expression.Loop(
        Expression.IfThenElse(

            Expression.NotEqual(Expression.Call(enumerator,typeof(IEnumerator).GetMethod("MoveNext")),Expression.Constant(false)),
                                Expression.Invoke(enumerableExpressions[0],Expression.Property(enumerator, "Current")),

                      Expression.Break(@break))
            , @break), typeParam);



    Expression<Action<TestObject>> lm = Expression.Lambda<Action<TestObject>>(funcBlock,recordExpression);
    var d = lm.Compile(); **//this is exceptioning with " variable 'Input' of type 'TestObject' referenced from scope '', but it is not defined**

Respuesta aceptada

Me perdí en algún lugar en medio de tu pregunta (y si lo he interpretado incorrectamente, por favor, dímelo y volveré a sumergirme), pero creo que esto es lo que buscas:

public static Expression ForEach(Expression collection, ParameterExpression loopVar, Expression loopContent)
{
    var elementType = loopVar.Type;
    var enumerableType = typeof(IEnumerable<>).MakeGenericType(elementType);
    var enumeratorType = typeof(IEnumerator<>).MakeGenericType(elementType);

    var enumeratorVar = Expression.Variable(enumeratorType, "enumerator");
    var getEnumeratorCall = Expression.Call(collection, enumerableType.GetMethod("GetEnumerator"));
    var enumeratorAssign = Expression.Assign(enumeratorVar, getEnumeratorCall);

    // The MoveNext method's actually on IEnumerator, not IEnumerator<T>
    var moveNextCall = Expression.Call(enumeratorVar, typeof(IEnumerator).GetMethod("MoveNext"));

    var breakLabel = Expression.Label("LoopBreak");

    var loop = Expression.Block(new[] { enumeratorVar },
        enumeratorAssign,
        Expression.Loop(
            Expression.IfThenElse(
                Expression.Equal(moveNextCall, Expression.Constant(true)),
                Expression.Block(new[] { loopVar },
                    Expression.Assign(loopVar, Expression.Property(enumeratorVar, "Current")),
                    loopContent
                ),
                Expression.Break(breakLabel)
            ),
        breakLabel)
    );

    return loop;
}

Para usarlo, debe proporcionar una colección para iterar, una expresión para sustituir en el cuerpo del bucle y una expresión de parámetro que se utiliza en la expresión del cuerpo del bucle, que se asignará a la variable de bucle en cada iteración del bucle.

Creo que a veces los ejemplos hablan más que las palabras ...

var collection = Expression.Parameter(typeof(List<string>), "collection");
var loopVar = Expression.Parameter(typeof(string), "loopVar");
var loopBody = Expression.Call(typeof(Console).GetMethod("WriteLine", new[] { typeof(string) }), loopVar);
var loop = ForEach(collection, loopVar, loopBody);
var compiled = Expression.Lambda<Action<List<string>>>(loop, collection).Compile();
compiled(new List<string>() { "a", "b", "c" });

EDITAR: Como Jeroem Mostert señala correctamente en los comentarios, esto no refleja exactamente el comportamiento "real" de un bucle foreach: esto aseguraría que elimine el enumerador. (También crearía una nueva instancia de la variable de bucle para cada iteración, pero eso no tiene sentido con las expresiones). ¡Implementar esto es solo una cuestión de girar la manija si te sientes lo suficientemente motivado!


Para cualquiera que esté mirando en casa, tengo un método similar para generar bucles 'for':

public static Expression For(ParameterExpression loopVar, Expression initValue, Expression condition, Expression increment, Expression loopContent)
{
    var initAssign = Expression.Assign(loopVar, initValue);

    var breakLabel = Expression.Label("LoopBreak");

    var loop = Expression.Block(new[] { loopVar },
        initAssign,
        Expression.Loop(
            Expression.IfThenElse(
                condition,
                Expression.Block(
                    loopContent,
                    increment
                ),
                Expression.Break(breakLabel)
            ),
        breakLabel)
    );

    return loop;
}

Esto es equivalente a la siguiente declaración, donde las pseudo-variables coinciden con las expresiones en el método anterior:

for (loopVar = initValue; condition; increment)
{
    loopContent
}

Nuevamente, loopContent, condición e incremento son expresiones que usan loopVar, y loopVar se asigna en cada iteración.


Respuesta popular

Aquí hay una versión ligeramente ampliada de la excelente solución de canton7 , teniendo en cuenta las observaciones sobre la eliminación del enumerador:

public static Expression ForEach(Expression enumerable, ParameterExpression loopVar, Expression loopContent)
{
    var elementType = loopVar.Type;
    var enumerableType = typeof(IEnumerable<>).MakeGenericType(elementType);
    var enumeratorType = typeof(IEnumerator<>).MakeGenericType(elementType);

    var enumeratorVar = Expression.Variable(enumeratorType, "enumerator");
    var getEnumeratorCall = Expression.Call(enumerable, enumerableType.GetMethod("GetEnumerator"));
    var enumeratorAssign = Expression.Assign(enumeratorVar, getEnumeratorCall);
    var enumeratorDispose = Expression.Call(enumeratorVar, typeof(IDisposable).GetMethod("Dispose"));

    // The MoveNext method's actually on IEnumerator, not IEnumerator<T>
    var moveNextCall = Expression.Call(enumeratorVar, typeof(IEnumerator).GetMethod("MoveNext"));

    var breakLabel = Expression.Label("LoopBreak");

    var trueConstant = Expression.Constant(true);

    var loop =
        Expression.Loop(
            Expression.IfThenElse(
                Expression.Equal(moveNextCall, trueConstant),
                Expression.Block(
                    new[] { loopVar },
                    Expression.Assign(loopVar, Expression.Property(enumeratorVar, "Current")),
                    loopContent),
                Expression.Break(breakLabel)),
            breakLabel);

    var tryFinally =
        Expression.TryFinally(
            loop,
            enumeratorDispose);

    var body =
        Expression.Block(
            new[] { enumeratorVar },
            enumeratorAssign,
            tryFinally);

    return body;
}


Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow