¿Cómo puedo reemplazar un parámetro de tipo en un árbol de expresiones?

c# expression-trees

Pregunta

Me gustaría poder escribir una expresión genérica que un usuario pueda usar para describir cómo quiere hacer una conversión a través de una familia de tipos.

La expresión puede parecer algo como:

Expression<Func<PlaceHolder,object>> sample = 
x=> (object)EqualityComparer<PlaceHolder>.GetHashCode(x)

Me gustaría convertirlo en:

Expression<Func<Foo,object>> sample = 
x=> (object)EqualityComparer<Foo>.GetHashCode(x)

Simplemente puedo visitar la expresión y reemplazar el parámetro PlaceHolder con x, pero luego no puedo resolver la llamada de tipo genérico.

La expresión es proporcionada por el usuario y no puede asignar un método genérico a una expresión.

El resultado final siempre es devolver un objeto y la expresión siempre será de T=>object . Compilaré una nueva expresión para cualquier objeto que reemplazará la regla predeterminada.

Aquí está mi código existente que funciona, pero parece muy complicado.

// ReSharper disable once InconsistentNaming
// By design this is supposed to look like a generic parameter.
public enum TEnum : long
{
}

internal sealed class EnumReplacer : ExpressionVisitor
{
    private Type ReplacePlaceHolder(Type type)
    {
        if (type.IsByRef)
        {
            return ReplacePlaceHolder(type.GetElementType()).MakeByRefType();
        }

        if (type.IsArray)
        {
            // expressionTrees can only deal with 1d arrays.
            return ReplacePlaceHolder(type.GetElementType()).MakeArrayType();
        }

        if (type.IsGenericType)
        {
            var typeDef = type.GetGenericTypeDefinition();
            var args = Array.ConvertAll(type.GetGenericArguments(), t => ReplacePlaceHolder(t));
            return typeDef.MakeGenericType(args);
        }

        if (type == typeof(TEnum))
        {
            return _enumParam.Type;
        }

        return type;
    }

    private MethodBase ReplacePlaceHolder(MethodBase method)
    {
        var newCandidate = method;
        var currentParams = method.IsGenericMethod ? ((MethodInfo)method).GetGenericMethodDefinition().GetParameters() : method.GetParameters();
        // ReSharper disable once PossibleNullReferenceException
        if (method.DeclaringType.IsGenericType)
        {
            var newType = ReplacePlaceHolder(method.DeclaringType);
            var methodCandidates = newType.GetMembers()
                .OfType<MethodBase>()
                .Where(x => x.Name == method.Name
                            && x.IsStatic == method.IsStatic
                            && x.IsGenericMethod == method.IsGenericMethod).ToArray();

            // grab the first method that wins. Not 100% correct, but close enough. 
            // yes an evil person could define a class like this::
            // class C<T>{
            //     public object Foo<T>(T b){return null;}
            //     public object Foo(PlaceHolderEnum b){return new object();}
            // }
            // my code would prefer the former, where as C#6 likes the later.
            newCandidate = methodCandidates.First(m => TestParameters(m, currentParams));
        }

        if (method.IsGenericMethod)
        {
            var genericArgs = method.GetGenericArguments();
            genericArgs = Array.ConvertAll(genericArgs, temp => ReplacePlaceHolder(temp));
            newCandidate = ((MethodInfo)newCandidate).GetGenericMethodDefinition().MakeGenericMethod(genericArgs);
        }
        return newCandidate;
    }
    private Expression ReplacePlaceHolder(MethodBase method, Expression target, ReadOnlyCollection<Expression> arguments)
    {
        // no point in not doing this.
        var newArgs = Visit(arguments);

        if (target != null)
        {
            target = Visit(target);
        }

        var newCandidate = ReplacePlaceHolder(method);

        MethodInfo info = newCandidate as MethodInfo;
        if (info != null)
        {
            return Expression.Call(target, info, newArgs);
        }
        return Expression.New((ConstructorInfo)newCandidate, newArgs);
    }

    private bool TestParameters(MethodBase candidate, ParameterInfo[] currentParams)
    {
        var candidateParams = candidate.GetParameters();
        if (candidateParams.Length != currentParams.Length) return false;
        for (int i = 0; i < currentParams.Length; i++)
        {
            // the names should match.
            if (currentParams[i].Name != candidateParams[i].Name) return false;

            var curType = currentParams[i].ParameterType;
            var candidateType = candidateParams[i].ParameterType;

            // Either they are the same generic type arg, or they are the same type after replacements.
            if (!((curType.IsGenericParameter &&
                  curType.GenericParameterPosition == candidateType.GenericParameterPosition)
                  || ReplacePlaceHolder(curType) == candidateType))
            {
                return false;
            }
        }
        return true;
    }

    private readonly ParameterExpression _enumParam;

    public EnumReplacer(ParameterExpression enumParam)
    {
        _enumParam = enumParam;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        if (node.Type == typeof(TEnum))
        {
            return _enumParam;
        }

        if (node.Type == typeof(TypeCode))
        {
            return Expression.Constant(Type.GetTypeCode(_enumParam.Type));
        }

        return base.VisitParameter(node);
    }

    protected override Expression VisitUnary(UnaryExpression node)
    {
        if (node.NodeType == ExpressionType.Convert || node.NodeType == ExpressionType.ConvertChecked)
        {
            var t = ReplacePlaceHolder(node.Type);
            // this isn't perfect. The compiler loves inserting random casts. To be protective and offer the most range, TEnum should be a long.
            var method = node.Method == null ? null : ReplacePlaceHolder(node.Method);
            return node.NodeType == ExpressionType.ConvertChecked
                ? Expression.ConvertChecked(Visit(node.Operand), t, (MethodInfo) method)
                : Expression.Convert(Visit(node.Operand), t, (MethodInfo) method);
        }
        if (node.Operand.Type == typeof(TEnum))
        {
            var operand = Visit(node.Operand);

            return node.Update(operand);
        }

        return base.VisitUnary(node);
    }

    private MemberInfo ReplacePlaceHolder(MemberInfo member)
    {
        if (member.MemberType == MemberTypes.Method || member.MemberType == MemberTypes.Constructor)
        {
            return ReplacePlaceHolder((MethodBase) member);
        }
        var newType = ReplacePlaceHolder(member.DeclaringType);
        var newMember = newType.GetMembers().First(x => x.Name == member.Name);
        return newMember;
    }

    protected override Expression VisitNewArray(NewArrayExpression node)
    {
        var children = Visit(node.Expressions);
        // Despite returning T[], it expects T.
        var type = ReplacePlaceHolder(node.Type.GetElementType());
        return Expression.NewArrayInit(type, children);
    }

    protected override MemberMemberBinding VisitMemberMemberBinding(MemberMemberBinding node)
    {
        var newMember = ReplacePlaceHolder(node.Member);
        var bindings = node.Bindings.Select(x => VisitMemberBinding(x));
        return Expression.MemberBind(newMember, bindings);
    }

    protected override MemberListBinding VisitMemberListBinding(MemberListBinding node)
    {
        var prop = ReplacePlaceHolder(node.Member);
        var inits = node.Initializers.Select(x => VisitElementInit(x));
        return Expression.ListBind(prop, inits);
    }

    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        return ReplacePlaceHolder(node.Method, node.Object, node.Arguments);
    }

    protected override MemberAssignment VisitMemberAssignment(MemberAssignment node)
    {
        var expr = Visit(node.Expression);
        var prop = ReplacePlaceHolder(node.Member);
        return Expression.Bind(prop, expr);
    }

    protected override ElementInit VisitElementInit(ElementInit node)
    {
        var method = ReplacePlaceHolder(node.AddMethod);
        var args = Visit(node.Arguments);
        return Expression.ElementInit((MethodInfo)method, args);
    }
    protected override Expression VisitNew(NewExpression node)
    {
        return ReplacePlaceHolder(node.Constructor, null, node.Arguments);
    }

    protected override Expression VisitConstant(ConstantExpression node)
    {
        // replace typeof expression
        if (node.Type == typeof(Type) && (Type)node.Value == typeof(TEnum))
        {
            return Expression.Constant(_enumParam.Type);
        }
        // explicit usage of default(TEnum) or (TEnum)456
        if (node.Type == typeof(TEnum))
        {
            return Expression.Constant(Enum.ToObject(_enumParam.Type, node.Value));
        }

        return base.VisitConstant(node);
    }
}

El uso es así:

class Program
{
    public class Holder
    {
        public int Foo { get; set; }
    }
    public class Foo<T1,T2> : IEnumerable
    {
        public object GenericMethod<TM, TM2>(TM2 blarg) => blarg.ToString();

        public IList<Foo<T1, T2>> T { get; set; } = new List<Foo<T1, T2>>();

        public T1 Prop { get; set; }
        public void Add(int i) { }
        public Holder Holder { get; set; } = new Holder {};

        public IEnumerator GetEnumerator()
        {
            throw new NotImplementedException();
        }
    }

    public enum LongEnum:ulong
    {
    }

    static void Main(string[] args)
    {
        Expression<Func<TEnum, TypeCode, object>> evilTest = (x,t) =>
                TypeCode.UInt64 == t
                    ? (object)new Dictionary<TEnum, TypeCode>().TryGetValue(checked((x - 407)), out t)
                    : new Foo<string, TEnum> { Holder = {Foo =6}, T = new []
                    {
                        new Foo<string, TEnum>
                        {
                            T = {
                                new Foo<string, TEnum>{1,2,3,4,5,6,7,8,9,10,11,12}
                            }
                        },
                        new Foo<string, TEnum>
                        {
                            Prop = $"What up hello? {args}"
                        }
                    }}.GenericMethod<string, TEnum>(x);
        Console.WriteLine(evilTest);
        var p = Expression.Parameter(typeof(LongEnum), "long");
        var expressionBody = new EnumReplacer(p).Visit(evilTest.Body);

        var q = Expression.Lambda<Func<LongEnum, object>>(expressionBody, p);
        var func =q.Compile();
        var res = func.Invoke((LongEnum)1234567890123Ul);

Respuesta popular

Modificar un árbol de expresión existente solo para cambiar un tipo usado dentro de él parece una tarea de tontos. Va a tener más problemas con eso, especialmente si alguna vez intenta realizar operaciones en los objetos de ese tipo.

Pero, ¿por qué te encuentras atrapado en cambiar un árbol existente? Estás intentando parametrizar un tipo que requiere genéricos. Simplemente cree un método genérico (donde el tipo es su parámetro) que devuelve la expresión deseada.

Expression<Func<T, object>> CreateConverter<T>() =>
    (T x) => EqualityComparer<T>.Default.GetHashCode(x);

No es necesario crear tipos de marcadores de posición falsos, el parámetro de tipo genérico es su marcador de posición.

Si necesita que esto sea conectable, coloque el método en una interfaz y los usuarios proporcionarán implementaciones que hagan la conversión.



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