2016-09-07 2 views
4

Ich möchte in der Lage sein, einen generischen Ausdruck zu schreiben, den ein Benutzer verwenden kann, um zu beschreiben, wie er eine Konvertierung über eine Familie von Typen vornehmen möchte.Wie kann ich einen Typparameter in einem Ausdrucksbaum ersetzen?

Der Ausdruck könnte etwas wie folgt aussehen:

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

ich es in umwandeln möchte ::

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

Ich kann nur den Ausdruck besuchen, und PlaceHolder Parameter mit x ersetzen, aber dann Ich kann den generischen Typenaufruf nicht auflösen.

Der Ausdruck wird vom Benutzer bereitgestellt, und Sie können einem Ausdruck keine generische Methode zuweisen.

Das Endergebnis ist immer ein Objekt zurückgeben und der Ausdruck wird immer von T=>object sein. Ich werde einen neuen Ausdruck für jedes Objekt kompilieren, das die Standardregel ersetzen wird.

Hier ist mein bestehender Code, der funktioniert, aber es scheint sehr kompliziert.

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

Verwendung ist wie so ::

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); 
+1

Dies wird unmöglich sein. Es wird keine Möglichkeit geben zu unterscheiden, welche Verwendungen dieses Typs geändert werden müssen und welche nicht. – Servy

+0

Der Typparameter einer generischen Klasse/Methode muss während der Kompilierzeit bekannt sein. Ich glaube also nicht, dass dies funktionieren wird, wenn Sie nicht zur Laufzeit einen neuen Typ erstellen. –

+0

Der Code wird vor der Verwendung kompiliert. –

Antwort

1

einen vorhandenen Ausdrucksbaum ändern nur eine Art zu ändern, können verwendet werden, wie ein Dummköpfe Besorgung scheint. Sie werden damit mehr Probleme bekommen, besonders wenn Sie versuchen, Operationen an den Objekten dieses Typs durchzuführen.

Aber warum werden Sie alle damit beschäftigt, einen bestehenden Baum zu ändern? Sie versuchen, einen Typ zu parametrisieren, der Generika aufruft. Erstellen Sie einfach eine generische Methode (wobei der Typ Ihr ​​Parameter ist), die den gewünschten Ausdruck zurückgibt.

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

Keine Notwendigkeit für gefälschte Platzhalter Typen erstellen, die generischen Typparameter ist Ihre Platzhalter.

Wenn Sie dies als Pluggable benötigen, platzieren Sie die Methode in einer Schnittstelle, und Benutzer stellen Implementierungen bereit, die die Konvertierung durchführen.

+0

Das Problem damit ist, dass ich einen Ausdruck von einem Benutzer nehmen musste. Sie können keinen generischen Delegaten übergeben, und das einzige, was hätte funktionieren können, wäre eine Klasse mit einer einzigen Schnittstellenmethode gewesen. Der Code müsste dann auch super generisch sein und Sie würden die Möglichkeit verlieren, arithmetische Operatoren zu verwenden. Meine neuartige Lösung funktionierte ein bisschen besser für das, was ich brauchte. –

+0

Ehrlich gesagt, denke ich, Ihre Argumentation ist hier fehlerhaft. Sie benötigen einen Ausdruck vom Benutzer, warum würde eine Factory-Methode, die zum Erzeugen des besagten Ausdrucks verwendet wird, nicht unerwünscht sein? Auch hier parametrisieren Sie den Typ, für den Generika genau ausgelegt sind. Sure C# 's Implementierung erlaubt nicht die Verwendung bestimmter Operatoren, aber ist das wirklich ein Problem?Es gibt andere Möglichkeiten, die Addition in der Ausdrucksbaumstruktur darzustellen. Mit einer "Platzhalter" -Funktion könnten Sie den Ausdruck einfach neu schreiben, um den tatsächlichen Zusatz zu verwenden, was schon Abfrage-Provider tun. –

+0

Für meinen Anwendungsfall war die Beschränkung des generischen Codes so ziemlich unmöglich, die interessanten Dinge zu tun, die die Benutzer tun wollten. Etwas zu haben, das wie ein Enum "roch", erlaubte einige ziemlich interessante und neuartige Anwendungen. Es erforderte auch weniger Substitutionen, da ich ohnehin einen Ausdrucksbaum benötigte. Einfach ausgedrückt sind einfache Ausdrücke wie 'x & ~ y' oder' x & y == y' viel einfacher zu lesen und zu schreiben als 'And (X, Not (y))' oder 'Equal (And (x, y), Y) '. –

Verwandte Themen