.Net 4:动态创建List <tuple <...>&gt;的简便方法结果</元组<...>

时间:2010-01-05 21:56:09

标签: c# .net expression tuples

对于远程处理场景,结果非常适合作为数组或Tuple对象列表接收(在强类型的好处中)。

示例:动态转换SELECT Name, Age FROM Table =&gt; List<Tuple<string,int>>

问题:是否存在任何样本,给定任意数据表(如SQL结果集或CSV文件),每个列的类型仅在运行时知道,以生成将动态创建强类型{的代码{1}}对象。代码应该动态生成,否则会非常慢。

2 个答案:

答案 0 :(得分:11)

编辑:我更改了代码以使用Tuple构造函数而不是 Tuple.Create 。它目前仅适用于最多8个值,但添加“元组堆叠”应该是微不足道的。


这有点棘手,实现依赖于数据源。 为了给人留下印象,我使用匿名类型列表作为源创建了一个解决方案。

正如Elion所说,我们需要动态创建一个表达式树,然后再调用它。我们采用的基本技术称为投影

我们必须在运行时获取类型信息,并根据属性计数创建 Tuple(...)构造函数的ConstructorInfor。每次调用都是动态的(尽管每条记录需要相同)。

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

答案 1 :(得分:0)

当我们遍历IEnumerable时,Dominik建立了一个懒洋洋地创建元组的表达式给我留下了深刻的印象,但是我的情况要求我以不同的方式使用他的一些概念。

我想将DataReader中的数据加载到Tuple中,只知道运行时的数据类型。为此,我创建了以下类:

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

与Dominik的解决方案存在一些差异:

1)没有延迟加载。由于我们一次只能从IDataReader使用IDataRecord的一条记录,所以我没有看到延迟加载的优势。

2)没有IQueryable,而是输出一个Object。这可能被视为一个缺点,因为你正在失去类型安全,但我发现我如何使用它并不会对你造成不利影响。如果您执行查询以获取DataRecord,您可能知道类型的模式是什么,因此您可以在Object返回后立即将其直接强制转换为强类型的元组。

对于我正在处理的另一个用例(代码未发布,因为它仍处于不稳定状态),我想要一些返回的元组来表示由具有多个连接的select查询构建的多个对象。有时将多行查询结果处理为不可变对象具有阻抗不匹配,因为在迭代DataReader时填充子类型数组。我在过去通过在构建时使用私有可变类来解决这个问题,然后在填充完成时创建一个不可变对象。这个DynamicTuple允许我将我在几个不同查询上使用的概念抽象为通用函数来读取任意连接查询,将其构建为List(DynamicTuples)而不是专用私有类,然后使用它来构造不可变数据对象。