Проверка нулевого значения в иерархии объектов

У меня есть большая структура объектов С# (3.0), созданная из десериализованного XML-документа. Мне нужно знать, является ли переменная глубиной иерархии нулевой. То, как я делаю это сейчас, - проверить каждый родительский объект на пути вниз для null, но это приводит к длительному повторению операторов if.

Я пытаюсь избежать дорогостоящих блоков try-catch.

Есть ли более разумный способ сделать это?

изменить Например, после десериализации формы приложения XML в структуру иерархии объектов потенциально может быть значение зарплаты в

applicationForm.employeeInfo.workingConditions.salary

но чтобы безопасно узнать, мне нужно написать что-то вроде

if (applicationForm.employeeInfo != null)
 if (applicationForm.employeeInfo.workingConditions != null)
 if (applicationForm.employeeInfo.workingConditions.salary != null)

потому что просто использование последнего if-statement, конечно, завершится неудачно, если один из родительских объектов имеет значение null.

Итак, я ищу любой разумный способ справиться с этой ситуацией.

11 ответов

Во-первых, если вы повторяете логику в нескольких местах, инкапсулируйте ее в метод.

Во-вторых, вам не нужно много инструкций if, вам просто нужно много условий OR:

if(parent==null || 
 parent.Child == null || 
 parent.Child.GrandChild == null ...

В-третьих, "избегая дорогостоящих блоков try/catch" может быть преждевременная оптимизация, в зависимости от вашего сценария. Вы действительно пробовали это и профилировали его, и действительно ли это налагает большие накладные расходы?


Вы столкнулись с классической ситуацией, где каждый шаг в A.B.C.D может дать null. Хотя это общий сценарий, на удивление нет общей схемы его решения, кроме того, используя большой if-оператор с большим количеством или (||).

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

Обобщенный метод расширения не является фиксированным термином, но я использую его здесь для того, чтобы подчеркнуть, что ext. метод применим практически ко всем типам объектов, поэтому обобщается. Согласно Билл Вагнер в Эффективном С#, это плохой дизайн. Но в некоторых отдаленных случаях, как и у вас, вы можете использовать его, пока знаете, что делаете и почему.

Трюк прост: определите общий метод расширения и обобщите его для всех классов, которые имеют конструктор по умолчанию. Если тест завершился неудачно (объект имеет значение null), метод возвращает новый объект того же типа. В противном случае он вернет сам неизменный объект.

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

// extension method:
public static class SomeExtentionMethods
{
 public static T SelfOrDefault<t>(this T elem)
 where T : class, new() /* must be class, must have ctor */
 {
 return elem ?? new T(); /* return self or new instance of T if null */
 }
}
// your code now becomes very easily readable:
Obj someObj = getYourObjectFromDeserializing();
// this is it applied to your code:
var mySalary = applicationForm.SelfOrDefault().
 employeeInfo.SelfOrDefault().
 workingConditions.SelfOrDefault().
 salary;
// now test with one if-statement:
if(mySalary.IsEmpty())
 // something in the chain was empty
else
 // all well that ends well :)
</t>

Красота такого подхода заключается в том, что он работает со всеми типами классов (при условии, что у них есть ctor), включая коллекции и массивы. Если какой-либо шаг является шагом индекса, он все равно будет работать (в зависимости от коллекции недопустимый индекс может вернуть значение null, по умолчанию или вызвать исключение):

var x = 
 someObj.SelfOrDefault()
 .Collection.SelfOrDefault()
 .Items[1].SelfOrDefault()
 .Mother.SelfOrDefault()
 .Father.SelfOrDefault();

Обновление: расширено и добавлено более сложный пример Обновление: переименовано NotNull, что подразумевает логическое значение, SelfOrDefault, которое следует за соглашением об именах LINQ (FirstOrDefault и т.д.) и подразумевает, что он делает. Обновление: переписано и реорганизовано, код более применим, надеясь сделать его более понятным в целом:)


Вы можете вложить тройные операторы. Еще боль, но не так плохо, как вложенные ifs.

string salary = (applicationForm.employeeInfo == null) ? null :
 (applicationForm.employeeInfo.workingConditions == null) ? null :
 applicationForm.employeeInfo.workingConditions.salary;

Если вы просто хотите узнать, имеет ли он значение null:

bool hasSalary = (applicationForm.employeeInfo == null) ? false :
 (applicationForm.employeeInfo.workingConditions == null) ? false :
 (applicationForm.employeeInfo.workingConditions.salary != null);


Не можете ли вы итерации?

for (SomeObject obj = someInstance; obj != null; obj = obj.Parent) {
 //do something
}


Поскольку вы не представили слишком много деталей, мне пришлось заполнить много пробелов. Вот пример psuo-codeish, как вы могли бы выполнить это через рекурсию

public bool doesHierarchyContainANull(MyObject ParentObject)
 {
 if (ParentObject.getMemberToCheckForNull() == null)
 return true;
 else if (ParentObject.isLastInHierarchy())
 return false;
 return doesHierarchyContainANull(ParentObject.getNextInHierarchy());
 }


Используйте отражение.

Создайте вспомогательный метод, который берет родительский объект и строку с иерархической точкой, обозначающую ваши имена свойств. Затем используйте ************ и используйте рекурсию, чтобы спускаться по одному свойству каждый раз, проверяя значение null и возвращая null, если это так, иначе продолжайте иерархию.


CheckForNull(MyType element)
{
 if(element.ChildElement != null)
 {
 CheckForNull(element.ChildElement);
 }
 else
 {
 Console.WriteLine("Null Found");
 }
}


Мое решение будет выглядеть примерно так:

public static TResult SafeGet<tsource, tresult="">(this TSource source, Func<tsource, tresult=""> getResult) {
 if (source == null)
 return default(TResult);
 try {
 return getResult(source);
 }
 catch {
 return default(TResult);
 }
}
</tsource,></tsource,>

Использование:

Test myTestObject = null;
var myStringOrNull = myTestObject.SafeGet(x => x.test.test.test.mySring);


Вам нужна рекурсивная функция для итерации по структуре и проверки каждого node и его дочерних элементов для нулевого. Я работал над образцом, но IE разбился (типичный!!). Будет опубликован позже.

Пример

Вы можете сделать что-то простое (предполагая, что вы когда-либо хотите просто проверить, действительно ли структура):

public void ValidateStructure()
{
 Node root = // get the root node
 try
 {
 ValidateChildren(root);
 Console.WriteLine("All nodes are valid");
 }
 catch (NullReferenceException)
 {
 Console.WriteLine("Structure contained a null node.");
 }
}
public void ValidateChildren(Node parent)
{
 // an NullReferenceException would be raised here since parent would be null 
 foreach (var child in parent.Children)
 {
 ValidateChildren(child);
 }
}


Используйте Null monad. Он может быть в том же файле или в другом файле, если вы using его.

public static class NullMonad {
 public static TResult SelectMany<tin, tout,="" tresult="">(this TIn @in, Func<tin, tout=""> remainder, Func<tin, tout,="" tresult=""> resultSelector)
 where TIn : class
 where TOut : class
 where TResult : class {
 var @out = @in != null ? remainder(@in) : null;
 return @out != null ? resultSelector(@in, @out) : null;
 }
}
</tin,></tin,></tin,>

Затем вы можете использовать LINQ:

var salary = from form in applicationForm
 from info in form.employeeInfo
 from cond in info.workingConditions
 select cond.salary

Это вернет зарплату, если она существует, или null, если какой-либо из предыдущих утверждений приведет к null, не вызывая исключения.

Это намного лучше? Нет, просто сэкономит вам хоть немного повторения, но выглядит круто. Это также позволяет избежать накладных расходов на создание всех неиспользуемых объектов "OrDefault" в принятом ответе.


Мне понравился ответ Понта Бремдаля, но добавил несколько подробностей для моих целей. Код:

/// <summary>
 /// Get a member in an object hierarchy that might contain null references.
 /// </summary>
 /// 
 /// 
 /// Base object to get member from.
 /// Member path.
 /// Returned object if object hierarchy is null.
 /// <returns>Default of requested member type.</returns>
 public TResult SafeGet<tsource, tresult="">(TSource source, Func<tsource, tresult=""> getResult, TResult defaultResult)
 {
 // Use EqualityComparer because TSource could by a primitive type.
 if (EqualityComparer<tsource>.Default.Equals(source, default(TSource)))
 return defaultResult;
 try
 {
 return getResult(source);
 }
 catch
 {
 return defaultResult;
 }
 }
 /// <summary>
 /// Get a member in an object hierarchy that might contain null references.
 /// </summary>
 /// 
 /// 
 /// Base object to get member from.
 /// Member path.
 /// <returns>Default of requested member type.</returns>
 public TResult SafeGet<tsource, tresult="">(TSource source, Func<tsource, tresult=""> getResult)
 {
 // Use EqualityComparer because TSource could by a primitive type.
 if (EqualityComparer<tsource>.Default.Equals(source, default(TSource)))
 return default(TResult);
 try
 {
 return getResult(source);
 }
 catch
 {
 return default(TResult);
 }
 }
</tsource></tsource,></tsource,></tsource></tsource,></tsource,>

Использование:

// Only authenticated users can run this code
if (!HttpContext.Current.SafeGet(s => s.User.Identity.IsAuthenticated))
 return;
// Get count limit from app.config
var countLimit = int.Parse(ConfigurationManager.AppSettings.SafeGet(
 s => s.Get("countLimit"),
 "100" // Default 100 if no value is present
 ));
// Is int 6 a class? Always no, but just to show primitive type usage.
var is6AClass = 6.SafeGet(i => i.GetType().IsClass);

Update

Теперь версия CSharp версии 6 встроена в нее. https://github.com/dotnet/roslyn/wiki/New-Language-Features-in-C%23-6#null-conditional-operators

Операторы с нулевым условием

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

int? length = customers?.Length; // null if customers is null
Customer first = customers?[0]; // null if customers is null

Оператор с нулевым условием удобно использовать вместе с нулевым коалесцирующим оператором:

int length = customers?.Length ?? 0; // 0 if customers is null

Оператор с нулевым условием демонстрирует поведение короткого замыкания, когда сразу следующая цепочка доступа к элементам, обращения к элементу и вызовы будут выполняться только в том случае, если исходный приемник не был нулевым:

int? first = customers?[0].Orders.Count();

Этот пример по существу эквивалентен:

int? first = (customers != null) ? customers[0].Orders.Count() : null;

За исключением того, что customers оценивается только один раз. Ни один из доступа к члену, обращения к элементу и вызовы, следующие непосредственно за ?, выполняются, если customers имеет ненулевое значение.

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

int? first = customers?[0].Orders?.Count();

Обратите внимание, что вызов (список аргументов в скобках) не может сразу следовать за оператором ?, что приведет к слишком большому количеству синтаксических двусмысленностей. Таким образом, простой способ вызова делегата только в том случае, если он там не работает. Однако вы можете сделать это с помощью метода Invoke для делегата:

if (predicate?.Invoke(e) ?? false) { … }

Мы ожидаем, что очень часто использование этого шаблона будет для запуска событий:

PropertyChanged?.Invoke(this, args);

Это простой и потокобезопасный способ проверить значение null перед запуском события. Причина, по которой его потокобезопасность заключается в том, что функция оценивает левую сторону только один раз и сохраняет ее во временной переменной.

licensed under cc by-sa 3.0 with attribution.