3 способа использовать оператор?.. неправильно в C# 6

Наверняка вы уже знаете об операторе безопасной навигации ("?." операторе) в C# 6. В то время как это довольно хороший синтаксический сахар, я хотел бы отметить варианты злоупотребления этим оператором.

Вариант 1


Я помню как я читал на одном из форумов как кто-то высказывался, что команда, разрабатывающая C#, должна сделать поведение при "?." дефолтным поведением при навигации внутрь объектов вместо того, что сейчас происходит при ".". Давайте порассуждаем немного об этом. Действительно, почему бы и не изменить имплементацию для "."? Другими словами, почему не использовать "?." оператор везде, просто на всякий случай?

Одно из основных назначений вашего кода — раскрывать ваши намерения другим разработчикам. Повсеместное использование оператора "?." вместо "." приводит к ситуации, в которой разработчики не способны понять, действительно ли код ожидает null или кто-то поместил эту проверку просто потому что это было легко сделать.

public void DoSomething(Order order)
{
    string customerName = order?.Customer?.Name;
}


Действительно ли этот метод ожидает, что параметр order может быть null? Может ли свойство Customer также возвращать null? Этот код сообщает, что да: и параметр order, и свойство Customer могут обращаться в null. Но это перестает быть таковым если автор поместил эти проверки просто так, без понимания того, что он делает.

Использование оператора "?." в коде без действительной необходимости в нем приводит к путанице и ухудшает читаемость кода.

Эта проблема тесно связана с отсутствием ненулевых ссылочных типов в C#. Было бы гораздо проще читать код если они были бы добавлены на уровне языка. К тому же, подобный код приводил бы к ошибке компиляции:

public void DoSomething(Order! order)
{
    Customer customer = order?.Customer; // Compiler error: order can’t be null
}


Вариант 2


Еще один способ злоупотребления оператором "?." — полагаться на null там, где это не требуется. Рассмотрим пример:

List<string> list = null;
int count;
if (list != null)
{
    count = list.Count;
}
else
{
    count = 0;
}


Этот код имеет очевидный «запах». Вместо использования null, в данном случае лучше использовать паттерн Null object:

// Пустой список - пример использования паттерна Null object
List<string> list = new List<string>();
int count = list.Count;


Теперь же с новым "?." оператором, «запах» уже не так очевиден:

List<string> list = null;
int count = list?.Count ?? 0;


В большинстве случаев, паттерн Null object — намного более предпочтительный выбор чем null. Он не только позволяет устранить проверки на нал, но также помогает лучше выразить доменную модель в коде. Использование оператора "?." может помочь с устранением проверок на нал, но никогда не сможет заменить необходимость построения качественной модели предметной области в коде.

С новым оператором очень легко писать подобный код:

public void Process()
{
    int result = DoProcess(new Order(), null);
}
 
private int DoProcess(Order order, Processor processor)
{
    return processor?.Process(order) ?? 0;
}


В то время как было бы правильней выразить эту логику с использованием Null object:

public void Process()
{
    var processor = new EmptyProcessor();
    int result = DoProcess(new Order(), processor);
}
 
private int DoProcess(Order order, Processor processor)
{
    return processor.Process(order);
}


Вариант 3


Часто подобный код показывается как хороший пример применения нового оператора:

public void DoSomething(Customer customer)
{
    string address = customer?.Employees
        ?.SingleOrDefault(x => x.IsAdmin)?.Address?.ToString();
    SendPackage(address);
}


В то время как подобный подход действительно позволяет сократить число строк в методе, он подразумевает, что сам по себе такой код — это нечно приемлемое.

Код выше нарушает принципы инкапсуляции. С точки зрения доменной модели, намного более правильным было бы добавить отдельный метод:

public void DoSomething(Customer customer)
{
    Contract.Requires(customer != null);
 
    string address = customer.GetAdminAddress();
    SendPackage(address);
}


Сохраняя таким образом инкапсуляцию и устраняя необходимость в проверках на нал. Использование оператора "?." может замаскировать проблемы с инкапсуляцией. Лучше не поддаваться искушению использовать оператор "?." в подобных случаях, даже если «сцеплять» вызовы методов друг за другом может казаться очень простой задачей.

Валидные примеры использования


В каких же случаях использование оператора "?." допустимо? В первую очередь, это легаси код. Если вы работаете с кодом или библиотекой, к которой не имеете доступа (или просто не хотите трогать исходники), у вас может не остаться другого выхода кроме как подстраиваться под этот код. В этом случае, оператор "?." может помочь уменьшить количество кода, необходимого для работы с подобной code base.

Другой хороший пример — вызов ивента:

protected void OnNameChanged(string name)
{
    NameChanged?.Invoke(name);
}


Остальные примеры сводятся к тем, которые не подпадают под три варианта невалидного использования, описанных выше.

Заключение


В то время как оператор "?." может помочь уменьшить количество кода в некоторых случаях, он также может замаскировать признаки плохого дизайна (design smells), которые могли бы быть более очевидными без него.

Для того чтобы решить, нужно или нет использовать этот оператор в каком-то конкретном случае, просто подумайте будет ли код, написанный «по старинке» допустимым. Если да, то смело используйте этот оператор. Иначе, постарайтесь отрефакторить код и убрать недостатки в дизайне.

Английская версия статьи: 3 misuses of "?." operator in C# 6

© Habrahabr.ru