What Are Lifted Operators

What are lifted operators?

Lifted operators are operators which work over nullable types by "lifting" the operators which already exist on the non-nullable form. So for example, if you do:

int? x = 10;
int? y = 10;
int? z = x + y;

That "+" operator is lifted. It doesn't actually exist on Nullable<int> but the C# compiler acts as if it does, generating code to do the right thing. (For the most case, that's a matter of checking whether either operand is null; if so, the result is null. Otherwise, unwrap both operands to their non-nullable values, use the normal operator, and then wrap the result back into a nullable value. There are a few special cases around comparisons though.)

See section 6.4.2 (lifted conversion operators) and 7.3.7 (lifted operators) of the C# spec for more information.

Why doesn't incrementing Nullableint throw an exception?

You're observing the effects of a lifted operator.

From section 7.3.7 of the C# 5 specification:

Lifted operators permit predefined and user-defined operators that operate on non-nullable value types to also be used with nullable forms of those types. Lifted operators are constructed from predefined and user-defined operators that meet certain requirements, as described in the following:

  • For the unary operators + ++ - -- ! ~
    a lifted form of an operator exists if the operand and result types are both non-nullable value types. The lifted form is constructed by adding a single ? modifier to the operand and result types. The lifted operator produces a null value if the operand is null. Otherwise, the lifted operator unwraps the operand, applies the underlying operator, and wraps the result.

So basically, a++ in this case is an expression with a result of null (as an int?) and the variable is left untouched.

When you call

Console.WriteLine(a);

that's being boxed into object, which converts it to a null reference, which is printed as an empty line.

ulong == ulong? evaluated as ulong == ulong works correctly

From MSDN:

Lifted operators permit predefined and user-defined operators that operate on non-nullable value types to also be used with nullable forms of those types. Lifted operators are constructed from predefined and user-defined operators that meet certain requirements, as described in the following:

...

  • For the equality operators

    ==  !=

a lifted form of an operator exists if the operand types are both non-nullable value types and if the result type is bool. The lifted form is constructed by adding a single ? modifier to each operand type. The lifted operator considers two null values equal, and a null value unequal to any non-null value. If both operands are non-null, the lifted operator unwraps the operands and applies the underlying operator to produce the bool result.

You're not using the operator:

bool ==(ulong left, ulong right)

You're using the lifted operator:

bool ==(ulong? left, ulong? right)

This operator takes two ulong? parameters, and returns true if both are null, or if both are non-null and have the same value.


You're probably looking at Visual Studio, which does show you something confusing in this case:

Visual Studio screenshot

Don't be confused by this -- as @mjwills pointed out in the comments, this is a known issue.


If you write this:

public bool M(ulong a, ulong? b) {
return a == b;
}

Then the compiler produces the following code:

public bool M(ulong a, ulong? b)
{
ulong? num = b;
return (a == num.GetValueOrDefault()) & num.HasValue;
}

num.GetValueOrDefault() returns 0 if b is null, otherwise the value of b. So M returns true if and only if b is not null, and has the same value as a.

SharpLab

Is Nullableint a Predefined value type - Or how does Equals() and == work here?

In C#, there's a concept called "Lifted Operators", described in section 7.3.7 of the language specification (Version 5 download):

Lifted operators permit predefined and user-defined operators that operate on non-nullable value types to also be used with nullable forms of those types. Lifted operators are constructed from predefined and user-defined operators that meet certain requirements, as described in the following

And specifically:

For the equality operators

==  !=

a lifted form of an operator exists if the operand types are both non-nullable value types and if the result type is bool. The lifted form is constructed by adding a single ? modifier to each operand type. The lifted operator considers two null values equal, and a null value unequal to any non-null value. If both operands are non-null, the lifted operator unwraps the operands and applies the underlying operator to produce the bool result.

So, since there's an == operator defined between ints, there's also one defined for int?s

Negation of null for Nullablebool

See Section 7.3.7 of the spec:

Lifted operators permit predefined and user-defined operators that operate on non-nullable value types to also be used with nullable forms of those types. Lifted operators are constructed from predefined and user-defined operators that meet certain requirements, as described in the following:

  • For the unary operators

        + ++ - -- ! ~
    a lifted form of an operator exists if the operand and result types are both non-nullable value types. The lifted form is constructed by adding a single ? modifier to the operand and result types. The lifted operator produces a null value if the operand is null. Otherwise, the lifted operator unwraps the operand, applies the underlying operator, and wraps the result.

(Emphasis mine)

So:

bool? x = null;
bool? y = !x;

Follows this rule. We're using the lifted form of the unary ! operator, which results in null if the value it's applied to is null.

!null isn't allowed, because null isn't of type Nullable<T>. !(bool?)null works, however (although it produces a compiler warning).

! indeed has higher precedence than ??, see Section 7.3.1

Why NullableT doesn't overload == opertator?

Operators on nullable value types are actually "hardcoded" in the language spec. They are known as Lifted Operators.

Lifted operators permit predefined and user-defined operators that operate on non-nullable value types to also be used with nullable forms of those types.

For the equality operators,

The lifted form is constructed by adding a single ? modifier to each operand type. The lifted operator considers two null values equal, and a null value unequal to any non-null value. If both operands are non-null, the lifted operator unwraps the operands and applies the underlying operator to produce the bool result.

This is specified in the language spec so the operators are all implemented by "compiler magic", which is why you don't see a declaration in the Nullable struct.

Exactly what the compiler magic does is implementation detail, but I'm pretty sure it wouldn't do a cast to object here, because you are using the lifted form of ==(int, int). According to the spec, this would apply ==(int, int) ("the underlying operator") if both operands are not null, not object.Equals.

On sharplab.io, you can see that that version of the compiler compiles this:

public bool F(int? a, int? b) {
return a == b;
}

to:

public bool F(Nullable<int> a, Nullable<int> b)
{
Nullable<int> num = a;
Nullable<int> num2 = b;
return (num.GetValueOrDefault() == num2.GetValueOrDefault()) & (num.HasValue == num2.HasValue);
}

A generic implementation of this would be:

public static bool operator == (Nullable<T> a, Nullable<T> b) {
return (a.GetValueOrDefault() == b.GetValueOrDefault()) && (a.HasValue == b.HasValue);
}

But note that the above is not going to compile in normal C#, because == is not guaranteed to be defined on T. However, since the lifted form ==(T?, T?) exists if and only if ==(T, T) exists, and because this is implemented by compiler magic, we're fine.



Related Topics



Leave a reply



Submit