roslyn: Using null conditional with IEnumerable fails with a compiler error CS0023

Version Used: VS 2017 15.6 and 15.7 Preview 1

Steps to Reproduce:

Put the following code into a program.

Scenario 1

static T GetFirstItem<T> ( ) where T: new()
{
    var results = GetItems<T>();

    return results?.FirstOrDefault() ?? default(T);
}

static IEnumerable<T> GetItems<T> () where T: new()
{
    return new[] { new T(), new T() };
}

Scenario 2

static T GetFirstItem<T> ( ) where T: new()
{
    IEnumerable results = GetItems<T>();
                        
    return results?.OfType<T>().FirstOrDefault() ?? default(T);
}

Expected Behavior:

The type of results is IEnumerable<T> in Scenario 1. That would make it a reference type. Since the type is nullable then ?. should properly evaluate the value and call the subsequent member if not null.

In Scenario 2 the type is IEnumerable which eliminates the generic type altogether.

Actual Behavior:

The compiler reports an error with the ?. The actual error is

CS0023 Operator ‘?’ cannot be applied to operand of type ‘T’

For some reason the compiler is confused about the type of results. Switching from var to an explicit type doesn’t change anything.

Scenario 2 generates the same error but we’ve removed the generic type so the compiler is still seeing it as T.

If you change to a different interface (i.e. IFoo) and then try the null conditional then the code compiles correctly. There is something special about IEnumerable but I don’t know what.

About this issue

  • Original URL
  • State: open
  • Created 6 years ago
  • Comments: 17 (10 by maintainers)

Most upvoted comments

in this context it basically means: not constrained to a reference or value type.

as such ?. is not legal for it as it doesn’t know what T will finally be. If it knew it would be a value type, then it would know to create the “T?” type. If it knew it would be a reference type, it would just keep it as the “T” type. Because the type neither has the “class” or “struct” constraint, you get hte error. “unconstrained” is just a simple way of describing that.

Type parameters (i.e. T in void Foo<T>()) can have constraints on them. For example:

  1. InterfaceName/ClassName constraints: void Foo<T>() where T : IEnumerable. It means it can only be instantiated with a type that is or inherits from that class name.
  2. class/struct constraint: void Foo<T>() where T : class or void Foo<T>() where T : struct. It means this can only be instantiated with reference or value types respectively.
  3. ‘new’ constraint. void Foo<T>() where T : new(). It means the type must have a public no-arg constructor on it.
  4. ‘unmanaged’ constraint. We’ll ignore that for now. It’s very new.

“Unconstrained type parameter” simply means: “a type parameter without a constraint provided”.

is then you’ll probably find that it doesn’t mean anything to them.

The problem then becomes: what audience are you creating your error messages for? There are novices using the language. There are experts. There are people who come from all sorts of different language paths, and you have to come up with a single message that somehow is suitable for all of them.

My preference is that the language error messages speak in the terms of the language itself. It means there is something you can actually read in the spec related to these concepts and that all the error messages are consistent with themselves and hte actual specification.

On top of that, i think it’s enormously important that you be able to get from an error message to useful documentation explaining it better. That documentation can then fill in the gaps of that audience spectrum. In this case, it seems unfortunate that CS0023 is being used for all these messages. Having a dedicated error code would be beneficial as that could help lead people to a dedicated page discussing the error as it pertains to these types (unconstrained type parameters) and these operators (‘?’).

This puts a higher burden on the compiler. i.e. the need to internally break what is one error message into many. But it serves to better drive users toward understanding what is going wrong, which is ultimately the most important goal of errors in the first place. After all, we would never accept a compiler that quit out, saying ‘something is wrong with your program’. 😃