我有两个不同类型的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
一样起作用吗?
答案 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>
这里显然分为三类:
a
和b
是相同的。我们待会再看。c
和d
是彼此的镜像。请注意,只要涉及类型推断,将哪个用作this
参数都没有关系。更多稍后……e
很简单:T
以一种非常明显的方式被推断为BaseClass
现在a
和b
不起作用,因为BaseClass
从未作为候选类型存在。从我记得的类型推断算法(确实非常复杂)来看,当任何一种参数类型都不存在该类型参数时,永远不会将其推断为类型参数。因此,尽管BaseClass
和object
都是有效的 显式泛型类型参数,但是不会推论这两个参数。
c
和d
解析为T
是BaseClass
,因为存在推断,要求从IEnumerable<BaseClass>
到IEnumerable<T>
进行转换,并且从List<DerivedClass2>
或List<DerivedClass1>
(分别用于c
和d
)到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 : B
和D2 : B
。
IEnumerable<D1>
(或IEnumerable<D2>
)和IEnumerable<B>
的并集是IEnumerable<B>
的实例(顺序不相关)。
还可以说IEnumerable<D1>
和IEnumerable<D2>
的并集是IEnumerable<B>
的实例。但是在这种情况下,编译器需要找到D1
和D2
的共同祖先,显然这并不麻烦。