116

I can't understand why the following C# code doesn't compile.

As you can see, I have a static generic method Something with an IEnumerable<T> parameter (and T is constrained to be an IA interface), and this parameter can't be implicitly converted to IEnumerable<IA>.

What is the explanation? (I don't search for a workaround, just to understand why it doesn't work).

public interface IA { }
public interface IB : IA { }
public class CIA : IA { }
public class CIAD : CIA { }
public class CIB : IB { }
public class CIBD : CIB { }

public static class Test
{
    public static IList<T> Something<T>(IEnumerable<T> foo) where T : IA
    {
        var bar = foo.ToList();

        // All those calls are legal
        Something2(new List<IA>());
        Something2(new List<IB>());
        Something2(new List<CIA>());
        Something2(new List<CIAD>());
        Something2(new List<CIB>());
        Something2(new List<CIBD>());
        Something2(bar.Cast<IA>());

        // This call is illegal
        Something2(bar);

        return bar;
    }

    private static void Something2(IEnumerable<IA> foo)
    {
    }
}

Error I get in Something2(bar) line:

Argument 1: cannot convert from 'System.Collections.Generic.List' to 'System.Collections.Generic.IEnumerable'

shA.t
  • 16,580
  • 5
  • 54
  • 111
BenLaz
  • 1,005
  • 1
  • 7
  • 8
  • 8
    Possible duplicate of [Why covariance and contravariance do not support value type](https://stackoverflow.com/questions/12454794/why-covariance-and-contravariance-do-not-support-value-type) – Dirk Nov 07 '18 at 08:40
  • 13
    You have not restricted `T` to reference types. If you use the condition `where T: class, IA` then it should work. The linked answer has more details. – Dirk Nov 07 '18 at 08:41
  • 2
    @Dirk I don't think this should be flagged as a duplicate. While it's true that the concept problem here is a covariance/contravariance problem in the face of value types, the specific case here is "what does this error message mean" as well as the author not realizing merely including "class" fixes his issue. I believe future users will search for this error message, find this post, and leave happy. (As I often do.) – Reginald Blue Nov 07 '18 at 13:46
  • You can also reproduce the situation by simply saying `Something2(foo);` directly. Going around `.ToList()` to get a `List` (`T` is your type parameter declared by the generic method) is not needed to understand this (a `List` is an `IEnumerable`). – Jeppe Stig Nielsen Nov 08 '18 at 12:18
  • @ReginaldBlue 100%, was going to post the same thing. Similar answers do not a duplicate question make. – StayOnTarget Nov 11 '18 at 13:35

2 Answers2

219

The error message is insufficiently informative, and that is my fault. Sorry about that.

The problem you are experiencing is a consequence of the fact that covariance only works on reference types.

You're probably saying "but IA is a reference type" right now. Yes, it is. But you didn't say that T is equal to IA. You said that T is a type which implements IA, and a value type can implement an interface. Therefore we do not know whether covariance will work, and we disallow it.

If you want covariance to work you have to tell the compiler that the type parameter is a reference type with the class constraint as well as the IA interface constraint.

The error message really should say that the conversion is not possible because covariance requires a guarantee of reference-type-ness, since that is the fundamental problem.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • 3
    Why you said it's your fault? – user4951 Nov 07 '18 at 15:04
  • 78
    @user4951: Because I implemented all of the conversion checking logic including the error messages. – Eric Lippert Nov 07 '18 at 15:06
  • @BurnsBA This is only a "fault" in the causal sense -- technically implementation as well as error message are perfectly correct. (It's just that the error statement of inconvertability could elaborate on the actual reasons. But producing good errors with generics is hard -- compared to C++ template error messages a few years ago this is lucid and concise.) – Peter - Reinstate Monica Nov 08 '18 at 17:25
  • 3
    @PeterA.Schneider: I appreciate that. But one of my primary goals for designing the error reporting logic in Roslyn was in particular to capture not just what rule was violated, but furthermore, to identify the "root cause" where possible. For example, what should the error message be for `customers.Select(c=>c.FristName)` ? The C# specification is very clear that this is an *overload resolution* error: the set of applicable methods named Select that can take that lambda is empty. But the root cause is that `FirstName` has a typo. – Eric Lippert Nov 08 '18 at 19:39
  • 3
    @PeterA.Schneider: I did a lot of work to ensure that scenarios involving generic type inference and lambdas used the appropriate heuristics to deduce what error message was likely to best help the developer. But I did a far less good job on the conversion error messages, particularly where variance was concerned. I've always regretted that. – Eric Lippert Nov 08 '18 at 19:40
26

I just wanted to complement Eric's excellent insider answer with a code example for those that may not be that familiar with generic constraints.

Change Something's signature like this: The class constraint has to come first.

public static IList<T> Something<T>(IEnumerable<T> foo) where T : class, IA
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Marcell Toth
  • 3,433
  • 1
  • 21
  • 36
  • 2
    I'm curious... what exactly is the reason behind the significance of the ordering? – Tom Wright Nov 08 '18 at 02:11
  • 6
    @TomWright - the spec doesn't, of course, include the answer to many "Why?" questions, but in [this case](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/classes#type-parameter-constraints) does make it clear that there are three distinct types of constraints, and when all three are used they have to be specifically `primary_constraint ',' secondary_constraints ',' constructor_constraint` – Damien_The_Unbeliever Nov 08 '18 at 07:05
  • 2
    @TomWright: Damien is correct; there is no particular reason I'm aware of other than the convenience of the author of the parser. If I had my druthers the syntax for type constraints would be considerably *more* verbose. `class` is bad because it means "reference type", not "class". I would have been happier with something verbose like `where T is not struct` – Eric Lippert Nov 10 '18 at 00:49