Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why cant the compiler resolve the resulting type?

Tags:

c#

I have two IEnumerables 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 dis 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?

like image 547
Stefan Illner Avatar asked Dec 12 '18 16:12

Stefan Illner


1 Answers

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 way

Now 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.

like image 103
Jon Skeet Avatar answered Oct 04 '22 22:10

Jon Skeet