为什么这是可查询的.Where Call更改可查询';s类型参数

本文关键字:查询 类型参数 Call Where 为什么 | 更新日期: 2023-09-27 18:21:31

再现问题的代码

我遇到了这样一种情况:IQueryable.Where<TSource>调用返回IQueryable<TOther>,其中TOther != TSource。我整理了一些样本代码来复制它:

using System;
using System.Collections.Generic;
using System.Linq;
namespace IQueryableWhereTypeChange {
    class Program {
        static void Main( string[] args ) {
            var ints = new List<ChildQueryElement>();
            for( int i = 0; i < 10; i++ ) {
                ints.Add( new ChildQueryElement() { Num = i, Value = i.ToString() } );
            }
            IQueryable<ChildQueryElement> theIQ = ints.AsQueryable();
            Object theObj = theIQ;
            Type theObjElementType = ( (IQueryable<ParentQueryElement>) theObj ).ElementType;
            Type theObjGenericType = ( (IQueryable<ParentQueryElement>) theObj ).GetType().GetGenericArguments()[ 0 ];
            var iQ = ( (IQueryable<ParentQueryElement>) theObj );
            var copy = iQ;
            Type copyType1 = copy.GetType().GetGenericArguments()[ 0 ];
            Type elementType1 = copy.ElementType;
            copy = copy.Where( qe1 => true );
            Type copyType2 = copy.GetType().GetGenericArguments()[ 0 ];
            Type elementType2 = copy.ElementType;
            Console.WriteLine( "theObjElementType : " + theObjElementType.ToString() );
            Console.WriteLine( "theObjGenericType : " + theObjGenericType.ToString() );
            Console.WriteLine( "copyType1 : " + copyType1.ToString() );
            Console.WriteLine( "elementType1 : " + elementType1.ToString() );
            Console.WriteLine( "copyType2 : " + copyType2.ToString() );
            Console.WriteLine( "elementType2 : " + elementType2.ToString() );
        }
    }
    public class ParentQueryElement {
        public int Num { get; set; }
    }
    public class ChildQueryElement : ParentQueryElement {
        public string Value { get; set; }
    }
}

该程序的输出为:

theObjElementType : IQueryableWhereTypeChange.ChildQueryElement    
theObjGenericType : IQueryableWhereTypeChange.ChildQueryElement    
copyType1 : IQueryableWhereTypeChange.ChildQueryElement  
elementType1 : IQueryableWhereTypeChange.ChildQueryElement  
copyType2 : IQueryableWhereTypeChange.ParentQueryElement  
elementType2 : IQueryableWhereTypeChange.ParentQueryElement 

代码结果摘要

因此,我们将IQueryable<ChildQueryElement>存储在Object中,然后将对象强制转换为IQueryable<ParentQueryElement>,其中子类型继承自父类型。此时,存储在Object变量中的对象仍然知道它是子类型的集合。然后我们对它调用Queryable.Where,但返回的对象不再知道它包含子类型,而是认为它只包含父类型。

问题

为什么会发生这种情况?除了跳过将其存储在对象中的步骤之外,还有什么方法可以避免这种情况吗?我问这个问题是因为我要处理一个第三方API,它要求我向它传递Object,我不想重写一堆第三方代码。

更新的示例代码

在得到Jon Skeet的一些建议后,我尝试了这个示例代码,它使用了一个动态变量进行复制。将Main的机身更换为以下部件:

var ints = new List<ChildQueryElement>();
for( int i = 0; i < 10; i++ ) {
    ints.Add( new ChildQueryElement() { Num = i, Value = i.ToString() } );
}
IQueryable<ChildQueryElement> theIQ = ints.AsQueryable();
Object theObj = theIQ;
Type theObjElementType = ( (IQueryable<ParentQueryElement>) theObj ).ElementType;
Type theObjGenericType = ( (IQueryable<ParentQueryElement>) theObj ).GetType().GetGenericArguments()[ 0 ];
var iQ = ( (IQueryable<ParentQueryElement>) theObj );
dynamic copy = iQ;
Type copyType1 = copy.GetType().GetGenericArguments()[ 0 ];
Type elementType1 = ((IQueryable)copy).ElementType;
Expression<Func<ParentQueryElement, bool>> del = qe => true;
copy = Queryable.Where( copy, del );
Type copyType2 = copy.GetType().GetGenericArguments()[ 0 ];
Type elementType2 = ((IQueryable)copy).ElementType;
Console.WriteLine( "theObjElementType : " + theObjElementType.ToString() );
Console.WriteLine( "theObjGenericType : " + theObjGenericType.ToString() );
Console.WriteLine( "copyType1 : " + copyType1.ToString() );
Console.WriteLine( "elementType1 : " + elementType1.ToString() );
Console.WriteLine( "copyType2 : " + copyType2.ToString() );
Console.WriteLine( "elementType2 : " + elementType2.ToString() );

不幸的是,产出保持不变。

为什么这是可查询的.Where Call更改可查询';s类型参数

为什么会发生这种情况?

因为Where调用正在接收ParentQueryElement的类型自变量作为TSource。它创建了一个基于TSource的结果作为一个新的对象。。。所以你最终得到了一些"知道"ParentQueryElement而不是ChildQueryElement的东西。

在不进入LINQ的情况下演示这一点很容易:

using System;
public interface IWrapper<out T>
{
    T Value { get; }
}
public class Wrapper<T> : IWrapper<T>
{
    private readonly T value;
    public Wrapper(T value)
    {
        this.value = value;
    }
    public T Value { get { return value; } }
}
class Program
{
    static void Main(string[] args)
    {
        IWrapper<string> original = new Wrapper<string>("foo");
        IWrapper<object> original2 = original;        
        IWrapper<object> rewrapped = Rewrap(original2);
        Console.WriteLine(original2.GetType()); // Wrapper<string>
        Console.WriteLine(rewrapped.GetType()); // Wrapper<object>
    }
    static IWrapper<T> Rewrap<T>(IWrapper<T> wrapper)
    {
        return new Wrapper<T>(wrapper.Value);
    }    
}

除了跳过将其存储在对象中的步骤之外,还有什么方法可以避免这种情况吗?

好吧,您可以动态调用Where,在这一点上,类型参数将在执行时推断出来:

dynamic copy = ...;
Expression<Func<ChildQueryElement, bool>> filter = qe1 => true;
// Can't call an extension method "on" dynamic; call it statically instead
copy = Queryable.Where(copy, filter);

注意,表达式树类型也需要是Func<ChildQueryElement, bool>。。。我不清楚这对你来说是否是个问题。

如果您确定查询中的元素类型为ChildQueryElement,那么您可以简单地使用Cast方法?

copy = copy.Where(qe1 => true); // IQueryable<ParentQueryElement>
var copyCasted = copy.Cast<ChildQueryElement>(); // IQueryable<ChildQueryElement>