8

Is this a compiler bug or is there a specific chosen reason why the null-conditional operator doesn't work with Func inside of generic methods?

To give an example the following doesn't compile

public static T Test<T>(Func<T> func)
{
    return func?.Invoke() ?? default(T);
}

The error the compiler produces is CS0023 Operator '?' cannot be applied to operand of type 'T'

I'm aware that you can achieve the same doing this however:

public static T Test<T>(Func<T> func)
{
    return func != null ? func() : default(T);
}

So why is it that it's not allowed?

To elaborate further Action<T> however works as expected.

public static void Test<T>(Action<T> action, T arg)
{
    action?.Invoke(arg);
}

Update (2017-01-17):

After some more research, it makes even less sense, even with the following:

Let's say we have a class (Reference-type)

public class Foo
{
    public int Bar { get; set; }
}

and let's say we have a Func<int>

Func<int> fun = () => 10;

The following works:

// This work
var nullableBar = foo?.Bar; // type of nullableBar is int?
var bar = nullableBar ?? default(int); // type of bar is int

// And this work
nullableBar = fun?.Invoke(); // ditto
bar = nullableBar ?? default(int); // ditto

Which means according to the logic applied there then a Func<T> of a value-type using null-conditional and null-coalescing operators should work.

However as soon the left-hand generic type of the null-conditional is generic with no constraints then it can't apply the same logic that it should be able to considering it can apply the same logic to both value-types and reference-types when the types are explicitly applied.

I'm aware of the compilers constraints, it just doesn't make sense to me why it doesn't allow it and why it wants the output to be different whether it's a reference or value type considering manually applying the types will yield expected results.

Bauss
  • 2,767
  • 24
  • 28
  • `var x = func?.Invoke()` will fail too. `x` can be null or have some value. compiler doesn't know that. besides that compiler doesn't know if `T` is reference type or not. note that `null` is not valid on value types. for example you cant write `int I = null`. thus the error you get. – M.kazem Akhgary Jan 13 '17 at 14:50
  • 1
    In a nutshell, the type of `Func?.Invoke()` must be `T` if `T` is a reference type, and `T?` if `T` is a value type. Since generics in .NET have one implementation (as opposed to templates in C++), this can't be done easily. In theory, the compiler could bend over backwards to make this work by clever code generation. In practice, the philosophy of the C# compiler is not to bend over backwards but to disallow things if they can't be done straightforwardly. – Jeroen Mostert Jan 13 '17 at 15:04

2 Answers2

13

Unfortunately I believe you have hit a edge case of the compiler. The ?. operator needs to return default(RetrunTypeOfRHS) for classes and default(Nullable<RetrunTypeOfRHS>) for structs. Because you have not constrained T to be classes or structs it can't tell which one to promote to.

The reason Action<T> works is because the return type of the right hand side is void for both cases so it does not need to decide which promotion to do.

You will need to use the long form you showed or have two methods with different constraints on T

    public static T TestStruct<T>(Func<T> func) where T : struct
    {
        return func?.Invoke() ?? default(T);
    }

    public static T TestClass<T>(Func<T> func) where T : class
    {
        return func?.Invoke(); // ?? default(T); -- This part is unnecessary, ?. already
                                                 // returns default(T) for classes.
    }
radarbob
  • 4,964
  • 2
  • 23
  • 36
Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431
  • `default(T);` when T is class is always null so it can be safely removed. resharper always reminds me xD – M.kazem Akhgary Jan 13 '17 at 14:58
  • 1
    @M.kazemAkhgary refresh, too slow :) – Scott Chamberlain Jan 13 '17 at 14:59
  • The compiler error is pretty terrible. It doesn't help at all to figure out what's wrong. +1 – InBetween Jan 13 '17 at 17:06
  • Going to accept this as answer, because it gives an explain as to how the operator functions. Just a weird behavior if you ask me. It should treat is values the same whether a reference or value type, especially if you define a default value. – Bauss Jan 13 '17 at 20:30
  • 2
    @Bauss: it can't treat the values the same, that's the whole point. The subexpression `func?.Invoke()` can't be assigned a single type. Realistically, the language would have to redefine the concept of nullability to make this work. That's a bit much for something that can easily be written in another way that avoids the issue. (In your alternative expression, there is no typing problem: `func()` is always of type `T`, and so is `default(T)`.) – Jeroen Mostert Jan 13 '17 at 23:23
  • This seems like ony of those times where the compiler should emit both and the JITer should optimize out the one it doesn't need. It already does something similar for other cases. – Mike Marynowski Nov 14 '17 at 07:10
6

You should set a constraint on the generic function:

public static T Test<T>(Func<T> func) where T: class
{
    return func?.Invoke() ?? default(T);
}

Because a struct cannot be null and the ?. requires a reference type.


Because of the comment from Jeroen Mostert, I had a look on what happens under the hood. A Func<T> is a delegate that is a reference type. Without any constraint on the T, the code will not compile. Error CS0023 Operator '?' cannot be applied to operand of type 'T'. When you add the constraint where T: struct or where T: class, the underlying code will be produced.

Code written:

    public static T TestStruct<T>(Func<T> func) where T : struct
    {
        return func?.Invoke() ?? default(T);
    }

    public static T TestClass<T>(Func<T> func) where T : class
    {
        return func?.Invoke() ?? default(T);
    }

Code produced and decompiled with ILSpy:

    public static T TestStruct<T>(Func<T> func) where T : struct
    {
        return (func != null) ? func.Invoke() : default(T);
    }

    public static T TestClass<T>(Func<T> func) where T : class
    {
        T arg_27_0;
        if ((arg_27_0 = ((func != null) ? func.Invoke() : default(T))) == null)
        {
            arg_27_0 = default(T);
        }
        return arg_27_0;
    }

As you can see, the code produced when T is a struct is different than when T is a class. So we fixed the ? error. BUT: The ?? operator doesn't make sense when T is a struct. I think the compiler should give a compileerror on this. Because using ?? on a struct isn't allowed. #BeMoreStrict

For example:

If I write:

var g = new MyStruct();
var p = g ?? default(MyStruct);

I get the compile error:

Error CS0019 Operator '??' cannot be applied to operands of type 'MainPage.MyStruct' and 'MainPage.MyStruct'

Jeroen van Langen
  • 21,446
  • 3
  • 42
  • 57
  • 1
    `default(T);` is always null so can be safely removed. – M.kazem Akhgary Jan 13 '17 at 14:57
  • 1
    "`?.` requires a reference type" is misleading at best and incorrect at worst. `Func` *is* a reference type, and `func?.Invoke()` would be legal if `Func` was of type `Func` (the result is of type `int?`). The problem is *specifically* that in the original code, a generic type `T` is involved that could be either a reference or a value type. – Jeroen Mostert Jan 13 '17 at 15:11
  • @JeroenMostert I added some info. Thanks for commenting. – Jeroen van Langen Jan 14 '17 at 15:55
  • The generated IL for classes doesn't make sense though. It seems like the issue is because the compiler generates that IL. Looking at the generated IL the logic is the following: If the result of either `func.Invoke` or `default(T)` is null then set the value to `default(T)`. As far as I'm aware there is no way to control the value of `default(T)` for classes, which means they will *always* be set null. Basically it says *If the result is null then set it to null* which makes no sense. It *should* have generated the same code that structs do if you ask me, which would have seem logical. – Bauss Jan 17 '17 at 07:43
  • Thats why I wrote ` BUT: The ?? operator doesn't make sense when T is a struct. I think the compiler should give a compileerror on this. Because using ?? on a struct isn't allowed.` – Jeroen van Langen Jan 17 '17 at 08:14
  • Updated my question with this ^ – Bauss Jan 17 '17 at 12:33