2010-01-05 9 views
5

Pour un scénario à distance, le résultat serait très bon à recevoir sous la forme d'un tableau ou d'une liste d'objets Tuple (parmi les avantages de typage fort)..Net 4: moyen facile de créer dynamiquement Liste <Tuple<...>> résultats

Exemple: dynamiquement convertir SELECT Name, Age FROM Table =>List<Tuple<string,int>>

Question: y at-il des échantillons là-bas qui, compte tenu d'une table arbitraire de données (comme resultset SQL ou CSV), avec les types de chaque colonne connue seulement à l'exécution, pour générer du code qui créerait dynamiquement un objet List<Tuple<...>> fortement typé. Le code devrait être généré dynamiquement, sinon il serait extrêmement lent.

+0

Il existe une limite au nombre de membres dans un Tuple. Que devrait faire le code s'il y a trop de membres? – Eilon

+0

Il n'y a pas de limite - Tuple de 8 éléments est conçu de manière à avoir un 8ème élément comme un autre Tuple – Yurik

+1

Hmno, il n'y a aucun avantage à taper dynamiquement des données non typées. Vous choisirez dynamiquement le mauvais type. –

Répondre

9

Edit: Je changé le code pour utiliser le constructeur Tuple au lieu de Tuple.Create. Il ne fonctionne actuellement que pour 8 valeurs maximum, mais ajouter 'Tuple stacking' devrait être trivial.


Ceci est un peu délicat et l'implémentation dépend en quelque sorte de la source de données. Pour donner une impression, j'ai créé une solution en utilisant une liste de types anonymes comme source. Comme l'a dit Elion, nous devons créer dynamiquement un arbre d'expression pour l'appeler par la suite. La technique de base que nous utilisons est appelée projection.

Nous devons obtenir à l'exécution les informations de type et créer un ConstructorInfor du constructeur Tuple (...) en fonction du nombre de propriétés. Ceci est dynamique (bien qu'il soit nécessaire d'être le même par enregistrement) pour chaque appel.

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

class Program 
{ 
    static void Main(string[] args) 
    { 

     var list = new[] 
         { 
          //new {Name = "ABC", Id = 1}, 
          //new {Name = "Xyz", Id = 2} 
          new {Name = "ABC", Id = 1, Foo = 123.22}, 
          new {Name = "Xyz", Id = 2, Foo = 444.11} 
         }; 

     var resultList = DynamicNewTyple(list); 

     foreach (var item in resultList) 
     { 
      Console.WriteLine(item.ToString()); 
     } 

     Console.ReadLine(); 

    } 

    static IQueryable DynamicNewTyple<T>(IEnumerable<T> list) 
    { 
     // This is basically: list.Select(x=> new Tuple<string, int, ...>(x.Name, x.Id, ...); 
     Expression selector = GetTupleNewExpression<T>(); 

     var expressionType = selector.GetType(); 
     var funcType = expressionType.GetGenericArguments()[0]; // == Func< <>AnonType..., Tuple<String, int>> 
     var funcTypegenericArguments = funcType.GetGenericArguments(); 

     var inputType = funcTypegenericArguments[0]; // == <>AnonType... 
     var resultType = funcTypegenericArguments[1]; // == Tuple<String, int> 

     var selects = typeof (Queryable).GetMethods() 
      .AsQueryable() 
      .Where(x => x.Name == "Select" 
      ); 

     // This is hacky, we just hope the first method is correct, 
     // we should explicitly search the correct one 
     var genSelectMi = selects.First(); 
     var selectMi = genSelectMi.MakeGenericMethod(new[] {inputType, resultType}); 

     var result = selectMi.Invoke(null, new object[] {list.AsQueryable(), selector}); 
     return (IQueryable) result; 

    } 

    static Expression GetTupleNewExpression<T>() 
    { 
     Type paramType = typeof (T); 
     string tupleTyneName = typeof (Tuple).AssemblyQualifiedName; 
     int propertiesCount = paramType.GetProperties().Length; 

     if (propertiesCount > 8) 
     { 
      throw new ApplicationException(
       "Currently only Tuples of up to 8 entries are alowed. You could change this code to allow stacking of Tuples!"); 
     } 

     // So far we have the non generic Tuple type. 
     // Now we need to create select the correct geneeric of Tuple. 
     // There might be a cleaner way ... you could get all types with the name 'Tuple' and 
     // select the one with the correct number of arguments ... that exercise is left to you! 
     // We employ the way of getting the AssemblyQualifiedTypeName and add the genric information 
     tupleTyneName = tupleTyneName.Replace("Tuple,", "Tuple`" + propertiesCount + ","); 
     var genericTupleType = Type.GetType(tupleTyneName); 

     var argument = Expression.Parameter(paramType, "x"); 

     var parmList = new List<Expression>(); 
     List<Type> tupleTypes = new List<Type>(); 

     //we add all the properties to the tuples, this only will work for up to 8 properties (in C#4) 
     // We probably should use our own implementation. 
     // We could use a dictionary as well, but then we would need to rewrite this function 
     // more or less completly as we would need to call the 'Add' function of a dictionary. 
     foreach (var param in paramType.GetProperties()) 
     { 
      parmList.Add(Expression.Property(argument, param)); 
      tupleTypes.Add(param.PropertyType); 
     } 

     // Create a type of the discovered tuples 
     var tupleType = genericTupleType.MakeGenericType(tupleTypes.ToArray()); 

     var tuplConstructor = 
      tupleType.GetConstructors().First(); 

     var res = 
      Expression.Lambda(
       Expression.New(tuplConstructor, parmList.ToArray()), 
       argument); 

     return res; 
    } 
} 

Si vous souhaitez utiliser un DataReader ou une entrée CVS, vous devrez réécrire la fonction GetTupleNewExpression. Je ne peux pas parler de la performance, bien qu'elle ne devrait pas être beaucoup plus lente qu'une implémentation native de LINQ car la génération de l'expression LINQ ne se produit qu'une seule fois par appel. Si c'est trop lent, vous pouvez aller sur la route de générer du code (et le garder dans un fichier) par exemple en utilisant Mono.Cecil.

Je ne pouvais pas encore tester cela en C# 4.0 et cela devrait fonctionner. Si vous voulez l'essayer dans 3,5 C# vous avez besoin le code suivant ainsi:

public static class Tuple 
{ 

    public static Tuple<T1, T2> Create<T1, T2>(T1 item1, T2 item2) 
    { 
     return new Tuple<T1, T2>(item1, item2); 
    } 

    public static Tuple<T1, T2, T3> Create<T1, T2, T3>(T1 item1, T2 item2, T3 item3) 
    { 
     return new Tuple<T1, T2, T3>(item1, item2, item3); 
    } 
} 

public class Tuple<T1, T2> 
{ 

    public Tuple(T1 item1, T2 item2) 
    { 
     Item1 = item1; 
     Item2 = item2; 
    } 

    public T1 Item1 { get; set;} 
    public T2 Item2 { get; set;} 

    public override string ToString() 
    { 
     return string.Format("Item1: {0}, Item2: {1}", Item1, Item2); 
    } 

} 

public class Tuple<T1, T2, T3> : Tuple<T1, T2> 
{ 
    public T3 Item3 { get; set; } 

    public Tuple(T1 item1, T2 item2, T3 item3) : base(item1, item2) 
    { 
     Item3 = item3; 
    } 

    public override string ToString() 
    { 
     return string.Format(base.ToString() + ", Item3: {0}", Item3); 
    } 
} 
+0

Dominik, excellent poste, merci! En ce qui concerne la création: .NET 4+ Tuples permettent plus de 8 paramètres - le Tuple <8> est un cas spécial - vous pouvez définir la 8ème valeur à être aussi un Tuple, et Tuple <8> va gérer correctement GetHashCode et la comparaison. Une note cependant - nous devrions utiliser le constructeur Tuple, pas les méthodes statiques pour que le dessus fonctionne. Dois-je corriger le code, ou voulez-vous faire les honneurs? :) – Yurik

+0

Autre chose - pour des raisons de performances, il serait idéal d'avoir une fonction précompilée et mise en cache qui prend un 'IEnumerable ' et crache soit 'List >' (beaucoup plus facile), soit 'IEnumerable >' - plus difficile parce que je doute que vous puissiez exprimer le «rendement de rendement» en utilisant des expressions, ainsi une classe d'état peut être exigée. – Yurik

+0

Yurik, j'ai changé le code pour utiliser le constructeur. Le code ne supporte que 8 valeurs, donc l'empilement de Tuples est laissé à vous :) –

0

J'ai été très impressionné par la construction de Dominik une expression pour créer paresseusement la Tuple que nous parcourons IEnumerable, mais ma situation appelée pour moi d'utiliser certains de ses concepts d'une manière différente.

Je souhaite charger les données d'un DataReader dans un Tuple en ne connaissant que les types de données lors de l'exécution. A cette fin, j'ai créé la classe suivante:

Public Class DynamicTuple 

Public Shared Function CreateTupleAtRuntime(ParamArray types As Type()) As Object 
    If types Is Nothing Then Throw New ArgumentNullException(NameOf(types)) 
    If types.Length < 1 Then Throw New ArgumentNullException(NameOf(types)) 
    If types.Contains(Nothing) Then Throw New ArgumentNullException(NameOf(types)) 

    Return CreateTupleAtRuntime(types, types.Select(Function(typ) typ.GetDefault).ToArray) 
End Function 

Public Shared Function CreateTupleAtRuntime(types As Type(), values As Object()) As Object 
    If types Is Nothing Then Throw New ArgumentNullException(NameOf(types)) 
    If values Is Nothing Then Throw New ArgumentNullException(NameOf(values)) 
    If types.Length < 1 Then Throw New ArgumentNullException(NameOf(types)) 
    If values.Length < 1 Then Throw New ArgumentNullException(NameOf(values)) 
    If types.Length <> values.Length Then Throw New ArgumentException("Both the type and the value array must be of equal length.") 

    Dim tupleNested As Object = Nothing 
    If types.Length > 7 Then 
     tupleNested = CreateTupleAtRuntime(types.Skip(7).ToArray, values.Skip(7).ToArray) 
     types(7) = tupleNested.GetType 
     ReDim Preserve types(0 To 7) 
     ReDim Preserve values(0 To 7) 
    End If 
    Dim typeCount As Integer = types.Length 

    Dim tupleTypeName As String = GetType(Tuple).AssemblyQualifiedName.Replace("Tuple,", "Tuple`" & typeCount & ",") 
    Dim genericTupleType = Type.[GetType](tupleTypeName) 
    Dim constructedTupleType = genericTupleType.MakeGenericType(types) 

    Dim args = types.Select(Function(typ, index) 
           If index = 7 Then 
            Return tupleNested 
           Else 
            Return values(index) 
           End If 
          End Function) 
    Try 
     Return constructedTupleType.GetConstructors().First.Invoke(args.ToArray) 
    Catch ex As Exception 
     Throw New ArgumentException("Could not map the supplied values to the supplied types.", ex) 
    End Try 
End Function 

Public Shared Function CreateFromIDataRecord(dataRecord As IDataRecord) As Object 
    If dataRecord Is Nothing Then Throw New ArgumentNullException(NameOf(dataRecord)) 
    If dataRecord.FieldCount < 1 Then Throw New InvalidOperationException("DataRecord must have at least one field.") 

    Dim fieldCount = dataRecord.FieldCount 
    Dim types(0 To fieldCount - 1) As Type 
    Dim values(0 To fieldCount - 1) As Object 
    For I = 0 To fieldCount - 1 
     types(I) = dataRecord.GetFieldType(I) 
    Next 
    dataRecord.GetValues(values) 

    Return CreateTupleAtRuntime(types, values) 
End Function 

End Class 

Certaines différences de la solution de Dominik:

1) Pas de chargement paresseux. Puisque nous utiliserions un enregistrement d'un IDataRecord à partir d'un IDataReader à la fois, je n'ai pas constaté d'avantage en chargement paresseux.

2) Non IQueryable, à la place, il génère un objet. Cela pourrait être considéré comme un inconvénient puisque vous perdez la sécurité de type, mais j'ai trouvé que la façon dont je l'utilise ne vous désavantage pas vraiment. Si vous avez exécuté une requête pour obtenir le DataRecord, vous savez peut-être quel est le motif des types et vous pouvez donc le transtyper directement dans un Tuple fortement typé immédiatement après le retour de l'Objet. Pour un autre cas d'utilisation sur lequel je travaille (code non affiché parce qu'il est toujours en flux), je voulais que quelques tuples retournés représentent plusieurs objets construits à partir d'une requête select avec plusieurs jointures. Parfois, le traitement d'un résultat de requête multiligne dans un objet immuable présente une incompatibilité d'impédance car vous remplissez un tableau de sous-types lorsque vous parcourez le DataReader. J'ai résolu ceci dans le passé en ayant une classe mutable privée en construisant, puis en créant un objet immuable quand le peuplement est fait. Cet DynamicTuple me permet de rendre abstrait ce concept que j'utilise sur plusieurs requêtes différentes pour une fonction polyvalente afin de lire une requête jointe arbitraire, de la construire en List (de DynamicTuples) au lieu de classes privées dédiées, puis de l'utiliser pour construire l'immutable. objet de données.