.Net 4: Простой способ динамически создавать результаты List <Tuple <... >> - PullRequest
5 голосов
/ 06 января 2010

Для сценария удаленного взаимодействия результат будет очень хорошо получить в виде массива или списка объектов Tuple (среди преимуществ - строгая типизация).

Пример: динамически конвертировать SELECT Name, Age FROM Table => List<Tuple<string,int>>

Вопрос: есть ли примеры, которые при произвольной таблице данных (например, набор результатов SQL или файл CSV) с типами каждого столбца, известными только во время выполнения, генерируют код, который динамически создает строго типизированный List<Tuple<...>> объект. Код должен генерироваться динамически, иначе он будет очень медленным.

Ответы [ 2 ]

11 голосов
/ 06 января 2010

Редактировать: Я изменил код для использования конструктора Tuple вместо Tuple.Create . В настоящее время он работает только для 8 значений, но добавить «укладку кортежа» должно быть тривиально.


Это немного сложно, и реализация зависит от источника данных. Чтобы создать впечатление, я создал решение, используя список анонимных типов в качестве источника.

Как сказал Элион, нам нужно динамически создать дерево выражений для его последующего вызова. Основная техника, которую мы используем, называется проекция .

Мы должны получить во время выполнения информацию о типе и создать ConstructorInfor конструктора Tuple (...) в соответствии с количеством свойств. Это является динамическим (хотя должно быть одинаковым для каждой записи) для каждого вызова.

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

Если вы хотите использовать DataReader или какой-либо ввод CVS, вам нужно переписать функцию GetTupleNewExpression .

Я не могу говорить о производительности, хотя она не должна быть намного медленнее, чем собственная реализация LINQ, поскольку генерация выражения LINQ происходит только один раз за вызов. Если он слишком медленный, вы можете пойти по пути генерации кода (и сохранить его в файле), например, используя Mono.Cecil.

Я еще не мог проверить это в C # 4.0, но он должен работать. Если вы хотите попробовать это в C # 3.5, вам также понадобится следующий код:

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 голосов
/ 08 апреля 2016

Я был весьма впечатлен тем, что Доминик построил выражение для ленивого создания кортежа, когда мы перебираем IEnumerable, но моя ситуация потребовала, чтобы я использовал некоторые из его концепций по-другому.

Я хочу загрузить данные из DataReader в кортеж, зная только типы данных во время выполнения. Для этого я создал следующий класс:

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

Некоторые отличия от решения Доминика:

1) Нет ленивой загрузки. Поскольку мы будем использовать одну запись IDataRecord из IDataReader за раз, я не вижу преимущества в отложенной загрузке.

2) Нет IQueryable, вместо этого он выводит объект. Это может рассматриваться как недостаток, так как вы теряете безопасность типов, но я обнаружил, что то, как я его использую, на самом деле не ставит вас в невыгодное положение. Если вы выполнили запрос для получения DataRecord, вы могли бы знать, каков шаблон типов, и поэтому вы можете преобразовать его непосредственно в строго типизированный кортеж сразу после возврата объекта.

Для другого варианта использования, над которым я работаю (код не опубликован, потому что он все еще в движении), я хотел, чтобы несколько возвращаемых кортежей представляли несколько объектов, построенных из запроса select с несколькими объединениями. Иногда обработка результата многострочного запроса в неизменяемом объекте имеет несоответствие импеданса, потому что вы заполняете массив подтипов, когда вы перебираете DataReader. Я решил эту проблему в прошлом, создав частный изменяемый класс при создании, а затем создав неизменный объект после завершения заполнения. Этот DynamicTuple позволяет мне абстрагировать эту концепцию, которую я использую в нескольких различных запросах, от функции общего назначения для чтения произвольного объединенного запроса, встроить его в список (из DynamicTuples) вместо выделенных частных классов, а затем использовать его для создания неизменяемого объект данных.

...