Tuesday 4 November 2014

Apex Ternary Operator and Types

Recently I was refactoring some of my code that in a Visualforce controller that generated a list of select options to produce a custom picklist of records.  Depending on the use case, I needed the selectoption values to be either the id of the record or the name.  Initially I had two separate methods that did pretty much the same thing:

public List<SelectOption> getOptionsByName(List<Account> accs)
{
    List<SelectOption> results=new List<SelectOption>();
	for (Account acc : accs)
    {
        results.add(new SelectOption(acc.Name, acc.Name));
    }

    return results;
}

public List<SelectOption> getOptionsById(List<Account> accs)
{
    List<SelectOption> results=new List<SelectOption>();
	for (Account acc : accs)
    {
        results.add(new SelectOption(acc.Id, acc.Name));
    }

    return results;
}

as you can see, this isn’t a very good example of the principle of DRY, so it seemed a trivial task to alter these functions to delegate to a single optionBuilder that could handle either scenario:

public List<SelectOption> getOptionsByName(List<Account> accs)
{
	return buildOptions(accs, false);
}

public List<SelectOption> getOptionsById(List<Account> accs)
{
	return buildOptions(accs, true);
}

private List<SelectOption> buildOptions(List<Account> accs, boolean byId)
{
    List<SelectOption> results=new List<SelectOption>();
    for (Account acc : accs)
    {
        results.add(new SelectOption(byId?acc.Id:acc.Name, acc.Name));
    }

    return results;
}

This refactored code worked fine when I executed the getOptionsById method, but when I tried getOptionsByName, I got the following, unexpected error:

15:50:59.047 (47206263)|FATAL_ERROR|System.StringException: Invalid id: test

which indicated that the ternary operator:

byId?acc.Id:acc.Name

was trying to promote the acc.Name String to an Id. Looking in the Apex Developer’s Guide didn’t throw a whole lot of light on the issue: 

Screen Shot 2014 07 27 at 15 58 41

as always, when presented with behaviour in Apex which isn’t documented, I reverted to the Java documentation (Apex being based on Java), to see if there was any further information.  There is, but its not the most enthralling read - if you are interested in the fine detail, check out http://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.25. If that’s TL and you DR, what it boils down to is that the ternary operator has a single type and the operands are converted to this single type as required, which in my case appeared to mean that as the Id was the first type encountered, that was taken as the type of the ternary operator, even though the parameter type for the method call is a String. So while the ternary operator is a short-hand for an if-then-else statement, it is one that converts:

byId?acc.Id:acc.Name

to 

Id result;
if (byId)
{
    result=acc.id;
}
else
{
    result=acc.Name;
}

Swapping the operands round fixes the problem nicely - the type of the first operand encountered is a String and as the Id primitive supports conversion to a String, everything works as expected:

!byId?acc.Name:acc.Id, acc.Name

For the sake of completeness, this is now shorthand for:

String result;
if (!byId)
{
    result=acc.Name;
}
else
{
    result=acc.id;
}

4 comments:

  1. Hi Bob,

    Great post! There is a typo in the second code snippet.

    return buildOptions(accs, false); -> should be (accs, true) for getOptionsById.

    ReplyDelete
  2. Alternatively, you could have:

    byId? (String)acc.Id:acc.Name;

    By casting to a String, you'd avoid the conflict, as both operands would now be strings. Of course, the solution here is perfectly valid. I just thought it'd be nice for people to have an alternative.

    ReplyDelete