Question

I discovered a strange behaviour of my program and after futher analysis, I was able to find that there is probably something wrong either in my C# knowledge or somewhere else. I beleive it's my mistake but I cannot find an answer anywhere...

public class B
{
    public static implicit operator B(A values) 
    {
        return null; 
    }
}
public class A { }

public class Program
{
    static void Main(string[] args)
    {
        A a = new A();
        B b = a ?? new B();
        //b = null ... is it wrong that I expect b to be B() ?
    }
}

The variable "b" in this code is evaluated to null. I don't get why is it null.

I googled and found a response in this question - Implicit casting of Null-Coalescing operator result - with the official specification.

But following this specification, I can't find the reason why "b" is null :( Maybe I'm reading it wrong in which case I apologize for spamming.

If A exists and is not a nullable type or a reference type, a compile-time error occurs.

...that's not the case.

If b is a dynamic expression, the result type is dynamic. At run-time, a is first evaluated. If a is not null, a is converted to dynamic, and this becomes the result. Otherwise, b is evaluated, and this becomes the result.

...that's also not the case.

Otherwise, if A exists and is a nullable type and an implicit conversion exists from b to A0, the result type is A0. At run-time, a is first evaluated. If a is not null, a is unwrapped to type A0, and this becomes the result. Otherwise, b is evaluated and converted to type A0, and this becomes the result.

...A exists, implicit conversion from b to A0 does not exist.

Otherwise, if A exists and an implicit conversion exists from b to A, the result type is A. At run-time, a is first evaluated. If a is not null, a becomes the result. Otherwise, b is evaluated and converted to type A, and this becomes the result.

...A exists, implicit conversion from b to A does not exist.

Otherwise, if b has a type B and an implicit conversion exists from a to B, the result type is B. At run-time, a is first evaluated. If a is not null, a is unwrapped to type A0 (if A exists and is nullable) and converted to type B, and this becomes the result. Otherwise, b is evaluated and becomes the result.

...b has a type B and implicit conversion exists from a to B. a is evaluated into null. Therefore, b should be evaluated and b should be the result.

Otherwise, a and b are incompatible, and a compile-time error occurs. Does not happen

Am I missing something please?

Was it helpful?

Solution 3

Well, the specification says (I change to x and y for less confusion here):

• Otherwise, if y has a type Y and an implicit conversion exists from x to Y, the result type is Y. At run-time, x is first evaluated. If x is not null, x is unwrapped to type X0 (if X exists and is nullable) and converted to type Y, and this becomes the result. Otherwise, y is evaluated and becomes the result.

This happens. First, the left-hand side x, which is just a, is checked for null. But it is not null in itself. Then the left-hand side is to be used. The implicit conversion is then run. Its result of type B is ... null.

Note that this is different from:

    A a = new A();
    B b = (B)a ?? new B();

In this case the left operand is an expression (x) which is null in itself, and the result becomes the right-hand side (y).

Maybe implicit conversions between reference types should return null (if and) only if the original is null, as a good practice?


I guess the guys who wrote the spec could have done it like this (but did not):

• Otherwise, if y has a type Y and an implicit conversion exists from x to Y, the result type is Y. At run-time, x is first evaluated and converted to type Y. If the output of that conversion is not null, that output becomes the result. Otherwise, y is evaluated and becomes the result.

Maybe that would have been more intuitive? It would have forced the runtime to call your implicit conversion no matter if the input to the conversion were null or not. That should not be too expensive if typical implementations quickly determined that null → null.

OTHER TIPS

Why did you expect the null-coalescing operator to return new B()? a is not null, so a ?? new B() evaluates to a.

Now that we know that a will be returned, we need to determine the type of the result (T) and whether we need to cast a to T.

• Otherwise, if b has a type B and an implicit conversion exists from a to B, the result type is B. At run-time, a is first evaluated. If a is not null, a is unwrapped to type A0 (if A exists and is nullable) and converted to type B, and this becomes the result. Otherwise, b is evaluated and becomes the result.

An implicit conversion exists from A to B, so B is the result type of the expression. Which means a will be implicitly casted to B. And your implicit operator returns null.

In fact, if you write var b = a ?? new B(); (notice the var), you'll see that the compiler infers B to be the type returned by the expression.

Otherwise, if b has a type B and an implicit conversion exists from a to B, the result type is B. At run-time, a is first evaluated. If a is not null, a is unwrapped to type A0 (if A exists and is nullable) and converted to type B, and this becomes the result. Otherwise, b is evaluated and becomes the result.

...b has a type B and implicit conversion exists from a to B. a is evaluated into null. Therefore, b should be evaluated and b should be the result.

You're interpreting this one wrong. Nothing says that a to B conversion is done before null check is performed. It states that null check is done before the conversion!

Your case fits to that:

If a is not null, a is unwrapped to type A0 (if A exists and is nullable) and converted to type B, and this becomes the result.

The part we need to look at is the compile-time type of the null-coalescing expression.

Otherwise, if b has a type B and an implicit conversion exists from a to B, the result type is B. At run-time, a is first evaluated. If a is not null, a is unwrapped to type A0 (if A exists and is nullable) and converted to type B, and this becomes the result. Otherwise, b is evaluated and becomes the result.

To put this into pseudocode:

public Tuple<Type, object> NullCoalesce<TA, TB>(TA a, TB b)
{
    ...
    else if (a is TB) // pseudocode alert, this won't work in actual C#
    {
        Type type = typeof(TB);
        object result;
        if (a != null)
        {
            result = (TB)a; // in your example, this resolves to null
        }
        else
        {
            result = b;
        }
        return new Tuple<Type, object>(type, result);
    }
    ...
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top