.Net 4:动态创建List<Tuple<...>>结果的简便方法

5

对于远程调用的场景,最好接收一个元组对象的数组或列表作为结果(其中强类型是其中的一个优点)。

例如:动态转换 SELECT Name, Age FROM Table => List<Tuple<string,int>>

问题:是否有任何示例可以在运行时仅知道每个列的类型的任意数据表(如SQL结果集或CSV文件),生成动态创建强类型 List<Tuple<...>> 对象的代码。代码应该是动态生成的,否则会非常慢。


元组中成员数量是有限制的。如果成员数量过多,代码应该怎么处理? - Eilon
没有限制 - 8个元素的元组被设计成第8个元素是另一个元组。 - Yuri Astrakhan
1
哦,动态类型未经过类型定义的数据并没有任何好处。你会动态地选择错误的类型。 - Hans Passant
不,类型是由数据存储知道的 - 问题在于代码生成。 - Yuri Astrakhan
1
祝你好运。这肯定是可行的,但编写这种代码真的很痛苦。你将不得不编写一堆基于表达式的代码,然后将其编译并保存到某种字典中。我不羡慕最终编写它的人 :) 如果这个代码超过100行,那么看到此处的回复(至少没有完整的代码示例)就不要抱太大希望了。 - Eilon
呵呵 :) 下面的Dominik自告奋勇地承担了这个不感谢的任务,并且进展顺利 :) 不知怎么的,我认为随着v4中元组的普及,这将对许多人非常有用。 - Yuri Astrakhan
2个回答

11

编辑:我更改了代码,使用元组构造函数代替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
Dominik,非常好的帖子,谢谢!关于创建:.NET 4+元组允许超过8个参数-Tuple<8>是一个特殊情况-您可以将第8个值设置为Tuple,并且Tuple<8>将正确处理GetHashCode和比较。不过需要注意的是-我们应该使用Tuple构造函数,而不是静态方法才能使上述内容正常工作。我应该修复代码还是你想要做这个荣誉? :) - Yuri Astrakhan
另外,出于性能考虑,最好有一个预编译和缓存的函数,它接受一个 IEnumerable<T> 并输出 List<Tuple<?>>(更容易),或者是 IEnumerable<Tuple<?>> - 更难,因为我怀疑你无法使用表达式来表示 yield return,所以可能需要一个状态类。 - Yuri Astrakhan
Yurik,我已经更改了代码以使用构造函数。该代码仅支持8个值,因此元组的堆叠留给您 :) - Dominik Fretz

0
我对Dominik的表达式构建印象深刻,可以在我们迭代IEnumerable时懒惰地创建Tuple,但我的情况要求我以不同的方式使用他的一些概念。
我想将数据从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返回后立即将其强制转换为强类型元组。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接