为什么编译器无法解析结果类型?

时间:2018-12-12 16:30:40

标签: c#

我有两个不同类型的IEnumerable,它们都来自一个共同的基类。现在,我尝试将可枚举合并以获取基类的可枚举。我必须将其中的一种枚举类型显式转换为基类,才能正常工作。

我猜想编译器会自动为结果可枚举选择最接近的通用基类型,但事实并非如此。

以下是该行为的示例:

namespace ConsoleApp1
{
    public class BaseClass { }
    public class DerivedClass1 : BaseClass { }
    public class DerivedClass2 : BaseClass { }

    class Program
    {
        static void Main(string[] args)
        {
            List<DerivedClass1> list1 = new List<DerivedClass1>();
            List<DerivedClass2> list2 = new List<DerivedClass2>();
            var a = list1.Union(list2); // Compiler Error
            IEnumerable<BaseClass> b = list1.Union(list2); // Compiler Error
            var c = list1.Cast<BaseClass>().Union(list2); // This works
            var d = list1.Union(list2.Cast<BaseClass>()); // This works
            var e = list1.Cast<BaseClass>().Union(list2.Cast<BaseClass>()); // This works, but ReSharper wants to remove one of the casts
        }
    }
}

var c似乎很容易解释,因为第一个可枚举的类型现在为BaseClass,因此与第二个列表的并集(包含从BaseClass派生的元素)也很容易理解

var d对我来说并不那么容易理解,因为我们从DerivedClass1的可枚举开始,然后将其与BaseClass元素进行合并。我对此工作感到惊讶。难道是因为联合运算是一种交换运算,所以它必须像c一样起作用吗?

4 个答案:

答案 0 :(得分:6)

值得记住的是Union是扩展方法。这是您要调用的方法签名:

public static IEnumerable<TSource> Union<TSource> (
    this IEnumerable<TSource> first, 
    IEnumerable<TSource> second);

所以您的通话有效:

var a = Enumerable.Union(list1, list2);
IEnumerable<BaseClass> b = Enumerable.Union(list1, list2); 
var c = Enumerable.Union(list1.Cast<BaseClass>(), list2);
var d = Enumerable.Union(list1, list2.Cast<BaseClass>());
var e = Enumerable.Union(list1.Cast<BaseClass>(), list2.Cast<BaseClass>());

这些调用所涉及的参数类型为:

a: List<DerivedClass1>, List<DerivedClass2>
b: List<DerivedClass1>, List<DerivedClass2> // Variable being assigned to doesn't matter
c: IEnumerable<BaseClass>, List<DerivedClass2>
d: List<DerivedClass1>, IEnumerable<BaseClass>
e: IEnumerable<BaseClass>, IEnumerable<BaseClass>

这里显然分为三类:

  • ab是相同的。我们待会再看。
  • cd是彼此的镜像。请注意,只要涉及类型推断,将哪个用作this参数都没有关系。更多稍后……
  • e很简单:T以一种非常明显的方式被推断为BaseClass

现在ab不起作用,因为BaseClass从未作为候选类型存在。从我记得的类型推断算法(确实非常复杂)来看,当任何一种参数类型都不存在该类型参数时,永远不会将其推断为类型参数。因此,尽管BaseClassobject都是有效的 显式泛型类型参数,但是不会推论这两个参数。

cd解析为TBaseClass,因为存在推断,要求从IEnumerable<BaseClass>IEnumerable<T>进行转换,并且从List<DerivedClass2>List<DerivedClass1>(分别用于cd)到IEnumerable<T>的转换。仅在考虑的类型中,T=BaseClass才是正确的,因此可以得出结论。

答案 1 :(得分:4)

  

为什么编译器无法解析结果类型?

该问题基于错误的信念;编译器无法找出通用类型。编译器可以很容易地发现这一点,它只是选择不这样做,这是有充分理由的。

当编译器对此做出解释时,它将始终使用表达式中涉及的类型。如果没有,那么应该在哪里停止?一切都可以简化为IEnumerable<object>并使其起作用,但这可能掩盖了意外行为和错误:

//ee is implicitly typed as IEnumerable<object>
var ee = someEnumerableOfString.Union(someEnumerableOfFoo);

?运算符也会发生类似的情况。编译器不会去寻找共同的祖先,无论是共同的基本类型,接口还是简单的对象。

这是一个好功能,因为它并不总是清楚您想要什么通用性。想象一下:

class A: IBar { }
class B: A, IFoo
class C: A, IFoo

var aa = someEnumerableOfB.Union(someEnumerableOfC);

为什么aa的类型为IEnumerable<A>?为什么不IEnumerable<IFoo>呢?为什么不IEnumerable<IBar>呢?它可能永远持续下去...

要考虑的另一个可能导致混乱的重要因素是,在以下假设的法律声明中:

IEnumerable<A> aa = someEnumerableOfB.Union(someEnumerableOfC);

有人可能会争辩说您是在明确IEnumerable<A>中指定所需的通用性。

但这不是编译器解释类型的方法;编译器将不会尝试根据赋值左侧的类型信息来找出右侧的类型。它的工作方式是尝试找出右侧的类型,如果成功,则 then 会查看其是否可分配给左侧。您收到的错误是因为第一步失败。

答案 2 :(得分:3)

它不能将DerivedClass2与DerivedClass1合并,它们共享一个基类,但是无论如何它们都不是同一类型

var c = list1.Cast<BaseClass>().Union(list2); // This works

这将起作用,因为现在在BaseClass和DerivedClass2之间建立了联合,并且由于baseClass.GetType()。IsAssigneableFrom(derivedClass2),可以将DerivedClass2转换为BaseClass

答案 3 :(得分:2)

您基本上有D1 : BD2 : B

IEnumerable<D1>(或IEnumerable<D2>)和IEnumerable<B>的并集是IEnumerable<B>的实例(顺序不相关)。

还可以说IEnumerable<D1>IEnumerable<D2>的并集是IEnumerable<B>的实例。但是在这种情况下,编译器需要找到D1D2的共同祖先,显然这并不麻烦。