I have two IEnumerable
s of different types, both of which derive from a common base class. Now I try to union the enumerables to get an enumerable of the base class. I have to explicitly cast one of the enumerables to the base class for this to work.
I would have guessed that the Compiler would have automatically chosen the closest common base type for the resulting enumerable, but it does not.
Here is an example of that behavior:
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
seems to be easy to explain, as the first enumerable is now of type BaseClass
, so a union with the second list, which contains elements which are derived from BaseClass also is easy to understand.
var d
is not so easy to understand for me, because we start with an enumerable of DerivedClass1
and union that with BaseClass
elements. I am surprised that this works. Is it because a union operation is kind of an commutative operation and so it has to work as c
also works?
It's worth remembering that Union
is an extension method. Here's the method signature you're calling:
public static IEnumerable<TSource> Union<TSource> (
this IEnumerable<TSource> first,
IEnumerable<TSource> second);
So your calls are effectively:
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>());
The argument types involved in those calls are:
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>
There are clearly three categories here:
a
and b
are the same. We'll look at that later.c
and d
are mirror images of each other. Note how it doesn't matter which is used as the this
parameter, as far as type inference is involved. More later...e
is simple: T
is inferred as BaseClass
in a really obvious wayNow a
and b
don't work because BaseClass
never exists as a candidate type. From what I remember of the type inference algorithm (and it really is very complicated), a type will never be inferred as a type argument when that type isn't present in any of the argument types. So while both BaseClass
and object
would be valid explicit generic type arguments, neither of those will be inferred.
c
and d
resolve to T
being BaseClass
, because there are inferences that require that there's a conversion from IEnumerable<BaseClass>
to IEnumerable<T>
, and that there's a conversion from List<DerivedClass2>
or List<DerivedClass1>
(respectively for c
and d
) to IEnumerable<T>
. That's only true for T=BaseClass
out of the types being considered, so that's what's inferred.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With