2010-01-05 16 views
5

Para un escenario remoto, el resultado sería muy bueno para recibirlo como una matriz o lista de objetos Tuple (entre los beneficios se encuentra la tipificación fuerte)..Net 4: forma fácil de crear dinámicamente List <Tuple<...>> resultados

Ejemplo: dinámicamente convertir SELECT Name, Age FROM Table =>List<Tuple<string,int>>

Pregunta: ¿existen muestras por ahí que, dada una tabla arbitraria de datos (como resultados búsqueda SQL o archivo CSV), con tipos de cada columna sólo se conoce en tiempo de ejecución, para generar código que crearía dinámicamente un objeto List<Tuple<...>> fuertemente tipado. El código debe generarse dinámicamente, de lo contrario sería extremadamente lento.

+0

Hay un límite para la cantidad de miembros en un Tuple. ¿Qué debería hacer el código si hay demasiados miembros? – Eilon

+0

No hay límite - Tuple de 8 elementos está diseñado de tal manera que tiene 8vo elemento como otro Tuple – Yurik

+1

Hmno, no hay beneficio de tipear dinámicamente datos sin tipo. Elegirá dinámicamente el tipo incorrecto. –

Respuesta

9

Editar: he cambiado el código para utilizar el constructor tupla en lugar de Tuple.Create. Actualmente funciona solo para hasta 8 valores, pero agregar el 'apilamiento Tuple' debe ser trivial.


Esto es un poco complicado y la implementación depende de la fuente de datos. Para dar una impresión, creé una solución usando una lista de tipos anónimos como fuente.

Como dijo Elion, necesitamos crear dinámicamente un árbol de expresiones para llamarlo después. La técnica básica que empleamos se llama proyección.

Tenemos que obtener, en tiempo de ejecución, la información del tipo y crear un ConstructorInfor del constructor Tuple (...) de acuerdo con el recuento de propiedades. Esto es dinámico (aunque debe ser el mismo por registro) por cada llamada.

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 desea utilizar un DataReader o alguna entrada de CVS, que tendría que volver a escribir la función GetTupleNewExpression.

No puedo hablar sobre el rendimiento, aunque no debería ser mucho más lento como una implementación LINQ nativa, ya que la generación de la expresión LINQ solo ocurre una vez por llamada. Si es demasiado lento, podría ir por el camino de generar código (y mantenerlo almacenado en un archivo), por ejemplo, usando Mono.Cecil.

No he podido probar esto en C# 4.0 todavía pero debería funcionar. Si desea probarlo en C# 3.5 necesita el siguiente código así:

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, excelente publicación, gracias! En cuanto a la creación: .NET 4+ Tuples permiten más de 8 parámetros: el Tuple <8> es un caso especial: puede establecer que el 8vo valor también sea un Tuple, y Tuple <8> manejará correctamente GetHashCode y la comparación. Sin embargo, una nota: debemos usar el constructor Tuple, no los métodos estáticos para que lo anterior funcione. ¿Debo arreglar el código, o quieres hacer los honores? :) – Yurik

+0

Otra cosa: por razones de rendimiento, sería ideal tener una función precompilada y almacenada en caché que lleva 'IEnumerable ' y escupe cualquiera de' List > '(mucho más fácil), o un' IEnumerable > '- más difícil porque dudo que puedas expresar 'rendimiento rendimiento' usando expresiones, entonces una clase de estado puede ser requerida. – Yurik

+0

Yurik, he cambiado el código para usar el constructor. El código solo admite 8 valores, por lo que el apilamiento de Tuples te queda a ti :) –

0

me quedé bastante impresionado con Dominik de la construcción de una expresión para crear perezosamente la tupla como iteramos sobre el IEnumerable, pero mi situación requería para mí usar algunos de sus conceptos de una manera diferente.

Quiero cargar los datos de un DataReader en un Tuple con solo conocer los tipos de datos en tiempo de ejecución. Con este fin, he creado la clase siguiente:

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 

Algunas de las diferencias con respecto a la solución de Dominik:

1) No se carga diferida. Ya que estaríamos usando un registro de un IDataRecord de un IDataReader a la vez, no vi una ventaja en la carga diferida.

2) No IQueryable, sino que genera un Objeto. Esto podría ser visto como una desventaja ya que está perdiendo seguridad de tipo, pero he descubierto que la forma en que lo estoy usando no lo pone en desventaja. Si ejecutó una consulta para obtener el Registro de datos, puede saber cuál es el patrón de tipos y, por lo tanto, puede convertirlo directamente en una Tupla fuertemente tipada inmediatamente después de la devolución del Objeto.

Para otro caso de uso en el que estoy trabajando (código no publicado porque todavía está en flujo), quería unas cuantas tuplas devueltas para representar varios objetos formados a partir de una consulta de selección con varias combinaciones. A veces, el procesamiento de un resultado de consulta de varias líneas en un objeto inmutable tiene una falta de coincidencia de impedancia porque está rellenando una matriz de subtipos a medida que itera sobre el lector de datos. Ya resolví esto en el pasado al tener una clase privada mutable mientras construía, y luego creé un objeto inmutable cuando terminaba el llenado. Esta DynamicTuple me permite abstraer ese concepto que utilizo en varias consultas diferentes a una función de propósito general para leer una consulta combinada arbitraria, compilarla en una Lista (de DynamicTuples) en lugar de clases privadas dedicadas, luego usar eso para construir lo inmutable objeto de datos.

Cuestiones relacionadas