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