Unique Ways to Use the Null Coalescing Operator

Unique ways to use the null coalescing operator

Well, first of all, it's much easier to chain than the standard ternary operator:

string anybody = parm1 ?? localDefault ?? globalDefault;

vs.

string anyboby = (parm1 != null) ? parm1
: ((localDefault != null) ? localDefault
: globalDefault);

It also works well if a null-possible object isn't a variable:

string anybody = Parameters["Name"]
?? Settings["Name"]
?? GlobalSetting["Name"];

vs.

string anybody = (Parameters["Name"] != null ? Parameters["Name"]
: (Settings["Name"] != null) ? Settings["Name"]
: GlobalSetting["Name"];

Is there a way to utilize the nullish coalescing operator (`??`) in object property destructuring?

I see two possible options:

  1. Do the nullish coalescing property-by-property, or

  2. Use a utility function

Property by property

Fairly plodding (I'm a plodding developer):

// Default `ExampleProps` here −−−−−−−−−−−−−−−vvvvv
export default function Example({ ExampleProps = {} }) {
// Then do the nullish coalescing per-item
const content = ExampleProps.content ?? "";
const title = ExampleProps.title ?? "Missing Title";
const date = ExampleProps.date ?? "";
const featuredImage = ExampleProps.featuredImage ?? {},
const author = ExampleProps.author ?? {},
const tags = ExampleProps.tags ?? [];
// ...

Utility function

Alternatively, use a utility function along these lines to convert null values (both compile-time and runtime) to undefined, so you can use destructuring defaults when destructuring the result. The type part is fairly straightforward:

type NullToUndefined<Type> = {
[key in keyof Type]: Exclude<Type[key], null>;
}

Then the utility function could be something like this:

function nullToUndefined<
SourceType extends object,
ResultType = NullToUndefined<SourceType>
>(object: SourceType) {
return Object.fromEntries(
Object.entries(object).map(([key, value]) => [key, value ?? undefined])
) as ResultType;
}

or like this (probably more efficient in runtime terms):

function nullToUndefined<
SourceType extends object,
ResultType = NullToUndefined<SourceType>
>(object: SourceType) {
const source = object as {[key: string]: any};
const result: {[key: string]: any} = {};
for (const key in object) {
if (Object.hasOwn(object, key)) {
result[key] = source[key] ?? undefined;
}
}
return result as ResultType;
}

Note that Object.hasOwn is very new, but easily polyfilled. Or you could use Object.prototype.hasOwn.call(object, key) instead.

(In both cases within nullToUndefined I'm playing a bit fast and loose with type assertions. For a small utility function like that, I think that's a reasonable compromise provided the inputs and outputs are well-defined.)

Then:

export default function Example({ ExampleProps }) {
const {
content = "",
title = "Missing Title",
date = "",
featuredImage = {},
author = {},
tags = [],
} = nullToUndefined(ExampleProps || {});
// ^^^^^^^^^^^^^^^^−−−−−−−−−−−−−−−−−−^
// ...

Playground link

C# coalescing operator with 3 possible return values?

Absolutely - you could use ?? in the same way as any other binary operator, meaning that you could have any expression on its left and/or on its right.

For example, you could do something like this:

int? a = ...
int? b = ...
int? c = ...
int? d = ...
int? res = (condition_1 ? a : b) ?? (condition_2 ? c : d);

This expression will evaluate (condition_1 ? a : b) first, check if it's null, and then either use the non-null value as the result, or evaluate the right-hand side, and us it as the result.

You can also "chain" the null coalesce operators ??, like this:

int? res =a ?? b ?? c ?? d;

Evaluation of this expression goes left to right.

The null-coalescing operator doesn't accepts different types

When you call NotFound(), you are creating a NotFoundResult. Your method has a return type of ActionResult<Round> but NotFoundResult doesn’t actually inherit from ActionResult<Round>, so you cannot return the NotFoundResult object directly.

When you do type return NotFound() then what actually happens is that the compiler will use the implicit operator ActionResult<T> (ActionResult) to transform the NotFoundResult into a ActionResult<Round>.

This works fine when you just return the value directly but it will not work when used within a ternary conditional or null-coalescing expression. Instead, you would have to do the conversion yourself:

public ActionResult<Round> Get(string id) =>
this._roundsService.Get(id) ?? new ActionResult<Round>(NotFound());

Because the constructor of ActionResult<T> accepts any ActionResult, you can just pass the NotFoundResult to it to make sure that it gets converted properly.

Of course, you can also split this up again and have the compiler do the conversion for you:

public ActionResult<Round> Get(string id)
{
var result = this._roundsService.Get(id);
if (result != null)
return result;
return NotFound();
}

How the right associative of null coalescing operator behaves?

The spec is actually self-contradictory on this one.

Section 7.13 of the C# 4 spec states:

The null coalescing operator is right-associative, meaning that operations are grouped from right to left. For example, an expression of the form a ?? b ?? c is evaluated as a ?? (b ?? c).

On the other hand, as has been pointed out, 7.3.1 claims that:

Except for the assignment operators, all binary operators are left-associative

I entirely agree that for simple cases it doesn't matter how you do the grouping... but there may be cases where it really matters due to implicit type conversions doing interesting things if the operands have different types.

I'll consider it further, ping Mads and Eric, and add an erratum for the relevant section of C# in Depth (which inspired this question).

EDIT: Okay, I've now got an example where it does matter... and the null coalescing operator is definitely right-associative, at least in the MS C# 4 compiler. Code:

using System;

public struct Foo
{
public static implicit operator Bar(Foo input)
{
Console.WriteLine("Foo to Bar");
return new Bar();
}

public static implicit operator Baz(Foo input)
{
Console.WriteLine("Foo to Baz");
return new Baz();
}
}

public struct Bar
{
public static implicit operator Baz(Bar input)
{
Console.WriteLine("Bar to Baz");
return new Baz();
}
}

public struct Baz
{
}

class Test
{
static void Main()
{
Foo? x = new Foo();
Bar? y = new Bar();
Baz? z = new Baz();

Console.WriteLine("Unbracketed:");
Baz? a = x ?? y ?? z;
Console.WriteLine("Grouped to the left:");
Baz? b = (x ?? y) ?? z;
Console.WriteLine("Grouped to the right:");
Baz? c = x ?? (y ?? z);
}
}

Output:

Unbracketed:
Foo to Baz
Grouped to the left:
Foo to Bar
Foo to Bar
Bar to Baz
Grouped to the right:
Foo to Baz

In other words,

x ?? y ?? z

behaves the same as

x ?? (y ?? z)

but not the same as

(x ?? y) ?? z

I'm not currently sure why there are two conversions from Foo to Bar when using (x ?? y) ?? z - I need to check that out more carefully...

EDIT: I now have another question to cover the double conversion...

Is there a way to implement and make use of a NOT null coalescing operator?

Mads Torgersen has publicly said that a null-propagating operator is under consideration for the next version of C# (but also emphasised that this doesn't mean it will be there). This would allow code like:

var value = someValue?.Method()?.AnotherMethod();

where the ?. returns null if the operand (on the left) is null, else will evaluate the right hand side. I suspect that would get you a lot of the way here, especially if combined with (say) extension methods; for example:

DateTime? dtStartDate = strStartDate?.MyParse();

where:

static DateTime MyParse(this string value) {
return DateTime.ParseExact(value, "dd.MM.yyyy",
System.Globalization.CultureInfo.InvariantCulture
);

However! You could do the same thing right now just using extension methods:

DateTime? dtStartDate = strStartDate.MyParse();

static DateTime? MyParse(this string value) {
if(value == null) return null;
return DateTime.ParseExact(value, "dd.MM.yyyy",
System.Globalization.CultureInfo.InvariantCulture
);

Null-coalescing operators with operands of different types

Does anyone have a reason why this isn't allowed?

This is how the language is designed. What should it do? How'd you convert an int, which is a 32-bit number, into a GUID, which is a structure composed of several number portions, or vice-versa?

This code:

var ID3 = ID ?? ID2

translated into types means (pseudocode):

(inferred type_) = Guid? ?? int;

and the ?? operator naturally requires the types of its arguments to be compatible. The actual rules are fairly complex and can be found in the C# language specification here.

Anyway, the null-coalescing operator returns a single value and both its left and right arguments must be evaluated into a value of the same type. Hence, the result could be either a Guid, or an int. The (inferred type) would then be this type. However, the ?? cannot convert the values of Guid and int operands into a value of such a single type.

Note: You could argue that the compiler could choose object as the closes type compatible with both Guid and int and just box those values into an object. However, that would undermine the very sense of strong type checking. It would be just 'too relaxed'.

In your particular example, sticking with ?? seems artificial; a good old ?: and a little bit more expressiveness in your code would do better and maybe even help readability:

var parameters = new Parameters() 
{
new Parameter("id", FKGuid.HasValue ? ToTypeParameterExpects(FKGuid.Value) : ToTypeParameterExpects(id))
};

Obviously, the ToTypeParameterExpects is a conversion to whatever type the Parameter's constructor expects for the value.

C# Null Coalescing Operator Behaves Differently in Conjunction with OR Operator Between Null and Empty Collection

According to operators precedence, conditional OR operator || has higher priority than null-coalescing operator ??.

According to null-coalescing operator specs:

The ?? operator doesn't evaluate its right-hand operand if the
left-hand operand evaluates to non-null.

So, in your code adding a parenthesis is needed to make the correct order of execution and ?? operator evaluation.

Without parenthesis this nullCollection?.Any() returns null, therefore ?? operator returns result of false || true (because of priority, mentioned above), which is always true.

emptyCollection?.Any() result is not null and returns false, therefore right side false || true isn't evaluated, compiler is smart enough to understand it.

Adding a parenthesis force the compiler to perform the expected execution of operators:

  • (nullCollection?.Any() ?? false) returns false because left-side
    operand is null and right side operand is executed.
  • (emptyCollection?.Any() ?? false) also returns false, left-side
    operand isn't null and right side isn't executed.

Logical OR for false || true returns true in both cases and you'll see a desired behavior



Related Topics



Leave a reply



Submit