Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

null-conditional operator doesn't work with Func<T> inside a generic method

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.

like image 986
Bauss Avatar asked Jan 13 '17 14:01

Bauss


People also ask

How to use null conditional operator in c#?

A null-conditional operator applies a member access, ?. , or element access, ?[] , operation to its operand only if that operand evaluates to non-null; otherwise, it returns null . That is, If a evaluates to null , the result of a?. x or a?[x] is null .

Is null c# operator?

operator is known as Null-coalescing operator. It will return the value of its left-hand operand if it is not null. If it is null, then it will evaluate the right-hand operand and returns its result. Or if the left-hand operand evaluates to non-null, then it does not evaluate its right-hand operand.


2 Answers

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.
    }
like image 143
Scott Chamberlain Avatar answered Oct 10 '22 23:10

Scott Chamberlain


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'

like image 25
Jeroen van Langen Avatar answered Oct 10 '22 23:10

Jeroen van Langen