Sunday, 11 February 2024

Apex Null Coalescing Operator

Image generated by DALL-E 3 based on about 10 prompts by Bob Buzzard

Introduction

The Spring 24 release of Salesforce bring a new operator to Apex - the null coalescing operator. It makes me very happy when these kinds of operators make their way to Apex, as it feels like we're a real programming language!

The null coalescing operator consists of an operand, two question marks, and another operand:

  operand1 ?? operand2

When the expressions is evaluated, if operand 1 is not null that will be the result, otherwise operand2 will be the result. Note that this isn't a mechanism for avoiding a null result, as if operand1 is null, operand2 will be the result even if it is null.

Using the Operator

The key use case for the null coalescing operator is removing explicit, and possibly lengthy, null checks. The obvious one is falling back on a default value, so instead of :

public List<Contact> getContactsForAccount(Account account)
{
    List<Contact> contacts=contactsForAccount.get(account.Id);

    if (null==contacts)
    {
        contacts=new List<Contact>();
    }

    return contacts;
}

we can write:

public List<Contact> getContactsForAccountNew(Account account)
{
    List<Contact> contacts=contactsForAccount.get(account.Id) ??
                            new List<Contact>();

    return contacts;
}

Much more compact, but that's not all - there's another aspect to this that is good and bad in equal measure. As there is now a single line evaluating the list of contacts, a single test will give 100% coverage of the line regardless of whether the first or second operand is as the result. For some developers this is good - one less scenario needed for test coverage. For others (including me) this is bad - if both scenarios aren't tested then we don't know how it will behave when the missing scenario happens in production.

Actually there's a third aspect to this too - the right hand operand isn't evaluated if the left hand is non-null, so it's efficient and devoid of unexpected side effects.

You can also chain them. so if you have five potential candidates for an opportunity value to return, you can write:

   Opportunity opp=opp1 ?? opp2 ?? opp3 ?? opp4 ?? opp5;

Safe in the knowledge that the first non-null value will be used. 

You also don't have to use objects or primitives as the operands, but watch out for the loss of clarity if you start to put too much working into a single expression.  For example, I don't think this is the clearest way to extract a title given an Id that might refer to a Contact or a Lead:

return [select Title from Contact where Email=:email][0].Title ??
       [select Title from Lead where Email=:email limit 1].Title ??
       'No contact/lead found matching email';Al

Always think about those that come after you!

Apples with Apples

The operands must both be the same type, so you can't have the first operand being a Lead and the second being a Contact unless they can be converted to a common type  - e.g. an sObject. By the time you introduce casting though, I'd say you've lost most of the benefit due to some ugly code:

Contact contactCand=new Contact();
Lead leadCand;

String name = (String) ((sobject) leadCand ?? (sobject) contactCand).get('Name');

You can also compare operands that can be promoted to a common type by the compiler, for example if you have some inheritance at play:

public virtual class First
{
    public String name {get; set;}
}

public class Second extends First
{
}

   ----------
   
First first=new First();
Second second=new Second();
String name=(first ?? second).name;

More Information

2 comments:

  1. Thanks for writing about this.
    The label on this post is "bull coalescing" instead of "null coalescing".
    Also, just before your "Apples with Apples" section, you have an unfinished part: "Use to pull a collection from a map or add the new collection in if it's not present. Instead of"

    ReplyDelete
    Replies
    1. Thanks - I've corrected the label and removed the half-finished sentence. I think that was a use case that didn't pan out but I didn't remove all of it.

      Delete