2010-01-05 3 views
5

Per uno scenario remoto, il risultato sarebbe molto buono da ricevere come array o elenco di oggetti Tuple (tra i vantaggi di una forte digitazione)..Net 4: Un modo semplice per creare dinamicamente List <Tuple<...>> risultati

Esempio: dinamicamente convertire SELECT Name, Age FROM Table =>List<Tuple<string,int>>

Domanda: ci sono dei campioni là fuori che, dato un tavolo arbitrario di dati (come resultset SQL o CSV), con tipi di ciascuna colonna nota solo in fase di runtime, per generare codice che crea dinamicamente un oggetto List<Tuple<...>> fortemente tipizzato. Il codice dovrebbe essere generato dinamicamente, altrimenti sarebbe estremamente lento.

+0

C'è un limite al numero di membri in una tupla. Cosa dovrebbe fare il codice se ci sono troppi membri? – Eilon

+0

Non c'è limite - La tupla di 8 elementi è progettata in modo tale da avere l'ottavo elemento come un'altra tupla – Yurik

+1

Hmno, non vi è alcun beneficio dalla digitazione dinamica dei dati non tipizzati. Sceglierai in modo dinamico il tipo sbagliato. –

risposta

9

Edit: Ho cambiato il codice per utilizzare il costruttore tupla invece di Tuple.Create. Attualmente funziona solo per un massimo di 8 valori, ma per aggiungere lo 'stacking Tuple' dovrebbe essere banale.


Questo è un po 'complicato e l'implementazione è in genere dipendente dall'origine dati. Per dare un'impressione, ho creato una soluzione utilizzando un elenco di tipi anonimi come fonte.

Come diceva Elion, abbiamo bisogno di creare dinamicamente un albero di espressioni per chiamarlo in seguito. La tecnica di base che impieghiamo è chiamata proiezione.

Dobbiamo ottenere in fase di esecuzione le informazioni sul tipo e creare un CostruttoreInfor del costruttore Tupla (...) in base al conteggio delle proprietà. Questo è dinamico (anche se deve essere lo stesso per record) per ogni chiamata.

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

Se si desidera utilizzare un DataReader o alcuni input CVS, si avrebbe bisogno di riscrivere la funzione di GetTupleNewExpression.

Non parlo delle prestazioni, anche se non dovrebbe essere molto più lento di un'implementazione LINQ nativa poiché la generazione dell'espressione LINQ avviene solo una volta per chiamata. Se è troppo lento si può andare giù per la strada di generare codice (e tenerlo memorizzato in un file) per esempio usando Mono.Cecil.

Non ho ancora potuto testarlo in C# 4.0 ma dovrebbe funzionare. Se volete provare in C# 3.5 è necessario il seguente codice così:

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, post eccellente, grazie! Per quanto riguarda la creazione: .NET 4+ Tuple consente più di 8 parametri: la tupla <8> è un caso speciale: puoi impostare l'ottavo valore anche come tupla e la tupla <8> gestirà correttamente il codice GetHash e il confronto. Una nota però - dovremmo usare il costruttore Tuple, non i metodi statici per il funzionamento di sopra. Devo correggere il codice o vuoi fare gli onori? :) – Yurik

+0

Un'altra cosa: per motivi di prestazioni, sarebbe ideale avere una funzione precompilata e memorizzata nella cache che prende un 'IEnumerable ' e sputa "Elenco >' (molto più semplice) o un 'IEnumerable >' - più difficile perché dubito che tu possa esprimere il 'yield return 'usando le espressioni, quindi potrebbe essere richiesta una classe statale. – Yurik

+0

Yurik, ho cambiato il codice per usare il costruttore. Il codice supporta solo 8 valori, quindi l'impilamento di Tuples è lasciato a te :) –

0

Sono stato molto impressionato con Dominik di costruzione di un'espressione per creare pigramente tupla come abbiamo iterare l'IEnumerable, ma la mia situazione richiedeva per me usare alcuni dei suoi concetti in un modo diverso.

Voglio caricare i dati da un DataReader in una tupla solo conoscendo i tipi di dati in fase di esecuzione. A tal fine, ho creato la seguente classe:

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 

Alcune delle differenze dalla soluzione di Dominik:

1) n lazy loading. Dal momento che utilizzeremmo un record di IDataRecord da un IDataReader alla volta, non ho riscontrato alcun vantaggio nel caricamento lento.

2) No IQueryable, invece emette un oggetto. Questo potrebbe essere visto come uno svantaggio dal momento che stai perdendo la sicurezza del tipo, ma ho scoperto che il modo in cui lo sto usando non ti svantaggia davvero. Se hai eseguito una query per ottenere il DataRecord, potresti sapere qual è il modello dei tipi e quindi puoi eseguire il cast direttamente in una tupla fortemente tipizzata immediatamente dopo il ritorno dell'oggetto.

Per un altro caso di utilizzo su cui sto lavorando (codice non pubblicato perché è ancora in flusso), volevo che alcune tuple restituite rappresentassero più oggetti creati da una query di selezione con più join. A volte l'elaborazione di un risultato di una query su più righe in un oggetto immutabile presenta un'impedenza di mancata corrispondenza poiché si sta compilando un array di sottotipi mentre si esegue l'iterazione su DataReader. Ho risolto questo problema in passato avendo una classe mutabile privata durante la costruzione, quindi creando un oggetto immutabile quando il popolamento è terminato. Questo DynamicTuple mi permette di astrarre il concetto che uso su diverse query per una funzione generica per leggere una query arbitraria unita, costruirla in una lista (di DynamicTuples) invece di private classes dedicate, quindi usarla per costruire l'immutabile oggetto dati.