【问题标题】:Why Does This Queryable.Where Call Change the Queryable's Type Parameter?为什么这个 Queryable.Where 调用会改变 Queryable 的类型参数?
【发布时间】:2014-11-04 21:16:23
【问题描述】:

重现问题的代码

我遇到了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&lt;ChildQueryElement&gt; 存储在Object 中,然后将对象转换为IQueryable&lt;ParentQueryElement&gt;,其中子类型继承自父类型。此时存储在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() );

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

【问题讨论】:

  • 我猜这对新用户来说有点敏感。将非代码突出显示为代码是愚蠢的。在这里,进行编辑。
  • @Will 谢谢你。你是怎么做到的?它只是没有给你警告吗?因为如果是这样,那么这就回答了我的元问题。
  • 不,它不会在某个代表阈值之后进行检查。不知道那是什么。或者,您可以关闭所选代码块的语法突出显示(搜索 Meta Stack Exchange 了解详细信息),或者将其标记为代码并在您的 q/a 下方留下评论以寻求帮助:/

标签: c# linq types iqueryable


【解决方案1】:

为什么会这样?

因为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&lt;ChildQueryElement, bool&gt;...我不清楚这是否会给您带来问题。

【讨论】:

  • 感谢斯基特先生的帮助。我不熟悉您在答案末尾提出的 Queryable.Where 语法。我试过这样做:动态副本= iQ;表达式> del = qe => true;复制 = Queryable.Where(复制,删除);但是.ElementType 调用会引发错误,因为副本现在是EnumerableQuery 而不是IQueryable
  • @DCShannon:究竟是什么错误,这是在编译时还是执行时?我用于 Where 的语法只是普通的静态方法语法,而不是使用它是扩展方法这一事实。
  • 我指的是动态变量的使用,而不是 Where 调用本身,抱歉。错误发生在执行时。直接从控制台窗口复制:未处理的异常:Microsoft.CSharp.RuntimeBinder.RuntimeBinderException:“Sys tem.Linq.EnumerableQuery”在 CallSite.Target(Closure, CallSite, Object) 不包含“ElementType”的定义) 在 System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,Tret](CallSite 站点,T 0 arg0) 在 IQueryableWhereTypeChange.Program.Main(String[] args)
  • @DCShannon:好的,它可能明确地实现了ElementType。您可以使用:var type = ((IQueryable) copy).ElementType;
  • 呃,我早该想到的。它现在编译并运行,但控制台的输出是相同的。 copyType2elementType2 都是 ParentQueryElement。时间不早了,我要回家了。再次感谢您的帮助。
【解决方案2】:

如果您确定查询中的元素将属于ChildQueryElement 类型,也许您可​​以简单地使用Cast 方法?

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

【讨论】:

  • 如果我确定这些元素将是那个特定的子类,那么我肯定会这样做。然而,提示这个问题的代码被设置为处理许多不同的子类,它们都继承自 ParentQueryElement。
猜你喜欢
  • 2010-09-25
  • 2020-05-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-03-11
  • 2017-10-05
  • 2017-08-15
  • 2020-06-14
相关资源
最近更新 更多