Wednesday, March 20, 2013

Safe Navigation Operator

C# is lacking a safe navigation operator, so we are forced to write this kind of code...

Console.WriteLine("Winner's zipcode: {0}", lottery.LastDrawnWinner.Address.Street.ZipCode.Value);


...to this code pattern:

string zipCode = null;

// Too much guard clause...
if (lottery != null &&
        lottery.LastDrawnWinner != null &&
        lottery.LastDrawnWinner.Address != null &&
        lottery.LastDrawnWinner.Address.Street != null &&
        lottery.LastDrawnWinner.Address.Street.ZipCode != null) 
// ...too much.
{
    zipCode = lottery.LastDrawnWinner.Address.Street.ZipCode.Value;
}
 
Console.WriteLine("Winner's zip code: {0}", zipCode)



C# team hinted safe navigation operator will be available on future version of C#, which didn't materialized in C# 5 though, otherwise it looks like the following, neat!

Console.WriteLine("Winner's zip code: {0}", lottery.?LastDrawnWinner.?Address.?Street.?ZipCode.?Value);


For now what we can do is introduce a safe navigation design pattern by using extension method:

Console.WriteLine("Winner's zip code: {0}", lottery.NullSafe(l => l.LastDrawnWinner).NullSafe(w => w.Address).NullSafe(a => a.Street).NullSafe(s => s.ZipCode).NullSafe(z => z.Value));


Not as pretty as the real one (safe navigation operator), but it's tidier than doing nested ifs. You'll see the importance of having safe navigation operator / design pattern when you have some deep object graph or you have some repeated code, e.g.

g.Include(x =>
                    {                                   
                        if (x.PayrollExcelExportItemVariableBovms == null) return null;
 
                        var varPay =
                            x.PayrollExcelExportItemVariableBovms.SingleOrDefault(v => v.VariablePayPeriodStepId == param.PeriodStepId);
 
                        if (varPay != null)
                            return varPay.VariablePayBonus;
                        else
                            return null;
                    }
    ).Label(i18nVarPay_VariablePayBudget + "\n(" + param.PeriodStepLabel + ")");


g.Include(x =>
                    {                                   
                        if (x.PayrollExcelExportItemVariableBovms == null) return null;
 
                        var varPay =
                            x.PayrollExcelExportItemVariableBovms.SingleOrDefault(v => v.VariablePayPeriodStepId == param.PeriodStepId);
 
                        if (varPay != null)
                            return varPay.VariablePayBonus;
                        else
                            return null;
                    }
    ).Label(i18nVarPay_EquityPayBonus + "\n(" + param.PeriodStepLabel + ")");



I'm not gonna write my code that way if I can write it in a more robust and conciser way:

g.Include(x => x.PayrollExcelExportItemVariableBovms.?SingleOrDefault(v => v.VariablePayPeriodStepId == param.PeriodStepId).?VariablePayBudget)
        .Label(i18nVarPay_VariablePayBudget + "\n(" + param.PeriodStepLabel + ")");

g.Include(x => x.PayrollExcelExportItemVariableBovms.?SingleOrDefault(v => v.VariablePayPeriodStepId == param.PeriodStepId).?VariablePayBonus)
        .Label(i18nVarPay_VariablePayBonus + "\n(" + param.PeriodStepLabel + ")");



Oops, that's not possible in C# 5 yet, NullSafe extension method would do the trick for now:

g.Include(x => x.PayrollExcelExportItemVariableBovms.NullSafe(x => x.SingleOrDefault(y => y.VariablePayPeriodStepId == param.PeriodStepId)).NullSafe(z => z.VariablePayBudget))
        .Label(i18nVarPay_VariablePayBudget + "\n(" + param.PeriodStepLabel + ")");

g.Include(x => x.PayrollExcelExportItemVariableBovms.NullSafe(x => x.SingleOrDefault(y => y.VariablePayPeriodStepId == param.PeriodStepId)).NullSafe(z => z.VariablePayBonus))
        .Label(i18nVarPay_VariablePayBonus + "\n(" + param.PeriodStepLabel + ")");



NullSafe extension method:

namespace NullSafeExtension
{
     
    public static class NullSafeHelper
    {
        public static U NullSafe<T, U>(this T t, Func<T, U> fn)
        {
            return t != null ? fn(t) : default(U);
        }
    }
}


Technique source: http://qualityofdata.com/2011/01/27/nullsafe-dereference-operator-in-c/



Happy Coding! ツ


UPDATE

Why using safe navigation is better than doing try-catch

When using a try catch, the code won't be able to run the next lines after of the line that has a null. Using a guard clause can prevent nulls from interfering on running all the lines in your code, but that will bloat or increase the noise in code.

I would rather use safe navigation than to use guarded clause. If we rather use catching null reference exception, it will prevent the code from running the next lines.

An example, a lottery winner won on a specific year, yet he has no address:

// Let's say a lottery winner has no registered address,
// hence this line will cause a null exception. So if we are catching exception...
Console.WriteLine("Winner's zipcode: {0}", lottery.LastDrawnWinner.Address.Street.ZipCode.Value); 

// ...this line won't execute:
Console.WriteLine("Year won: {0}", lottery.LastDrawnWinner.YearWon); 

2 comments:

  1. just do a try catch on nullreferenceexception.

    ReplyDelete
    Replies
    1. See my update why safe navigation is better than if-statements and try-catch approach

      Delete