Глубокая нулевая проверка, есть ли лучший способ?

Примечание. Этот вопрос задавали перед введением .? оператора в C # 6 / Visual Studio 2015 .

Мы все были там, у нас есть некоторая глубокая собственность, такая как cake.frosting.berries.loader, что нам нужно проверить, является ли она нулевой, поэтому исключений нет. Способ состоит в том, чтобы использовать оператор short-circuiting if

 if (cake != null && cake.frosting != null && cake.frosting.berries != null) ... 

Это не совсем изящно, и, возможно, должен быть более простой способ проверить всю цепочку и посмотреть, сталкивается ли она с нулевой переменной / свойством.

Возможно ли использование какого-либо метода расширения или это будет языковая функция, или это просто плохая идея?

Мы рассмотрели возможность добавления новой операции «?». на язык, который имеет семантику, которую вы хотите. (И он был добавлен сейчас, см. Ниже.) То есть, вы сказали бы

 cake?.frosting?.berries?.loader 

и компилятор будет генерировать все короткозамкнутые проверки для вас.

Он не сделал планку для C # 4. Возможно, для гипотетической будущей версии языка.

Обновление (2014):. оператор теперь запланирован для следующего выпуска компилятора Roslyn. Обратите внимание, что все еще есть некоторые споры о точном синтаксическом и семантическом анализе оператора.

Обновление (июль 2015 г.): выпущена версия Visual Studio 2015 и поставляется с компилятором C #, который поддерживает операторы с нулевым условием ?. и ?[] .

Я был вдохновлен этим вопросом, чтобы попытаться выяснить, как эта глубокая проверка нуля может быть выполнена с помощью более простого / более симпатичного синтаксиса с использованием деревьев выражений. Хотя я согласен с ответами на то, что это может быть плохой дизайн, если вам часто приходится обращаться к экземплярам, ​​находящимся в глубине иерархии, я также думаю, что в некоторых случаях, например, в представлении данных, это может быть очень полезно.

Поэтому я создал метод расширения, который позволит вам написать:

 var berries = cake.IfNotNull(c => c.Frosting.Berries); 

Это вернет ягоды, если никакая часть выражения не равна нулю. Если null встречается, возвращается null. Есть некоторые предостережения, хотя в текущей версии он будет работать только с простым доступом к члену, и он работает только на .NET Framework 4, потому что он использует метод MemberExpression.Update, который является новым в v4. Это код для метода расширения IfNotNull:

 using System; using System.Collections.Generic; using System.Linq.Expressions; namespace dr.IfNotNullOperator.PoC { public static class ObjectExtensions { public static TResult IfNotNull(this TArg arg, Expression> expression) { if (expression == null) throw new ArgumentNullException("expression"); if (ReferenceEquals(arg, null)) return default(TResult); var stack = new Stack(); var expr = expression.Body as MemberExpression; while(expr != null) { stack.Push(expr); expr = expr.Expression as MemberExpression; } if (stack.Count == 0 || !(stack.Peek().Expression is ParameterExpression)) throw new ApplicationException(String.Format("The expression '{0}' contains unsupported constructs.", expression)); object a = arg; while(stack.Count > 0) { expr = stack.Pop(); var p = expr.Expression as ParameterExpression; if (p == null) { p = Expression.Parameter(a.GetType(), "x"); expr = expr.Update(p); } var lambda = Expression.Lambda(expr, p); Delegate t = lambda.Compile(); a = t.DynamicInvoke(a); if (ReferenceEquals(a, null)) return default(TResult); } return (TResult)a; } } } 

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

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

Я нашел это расширение весьма полезным для сценариев глубокого вложения.

 public static R Coal(this T obj, Func f) where T : class { return obj != null ? f(obj) : default(R); } 

Это идея, которую я получил от оператора нулевой коалесценции в C # и T-SQL. Приятно, что возвращаемый тип всегда является возвращаемым типом внутреннего свойства.

Таким образом вы можете сделать это:

 var berries = cake.Coal(x => x.frosting).Coal(x => x.berries); 

… или небольшое изменение вышеизложенного:

 var berries = cake.Coal(x => x.frosting, x => x.berries); 

Это не лучший синтаксис, который я знаю, но он работает.

Помимо нарушения Закона Деметры, как уже указывал Мехрдад Афшари, мне кажется, что вам нужна «глубокая нулевая проверка» для логики принятия решения.

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

Обновление: начиная с Visual Studio 2015 компилятор C # (язык версии 6) теперь распознает ?. оператора, что делает «глубокую нулевую проверку» легким. Подробнее см. В этом ответе .

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

 try { var x = cake.frosting.berries.loader; ... } catch (NullReferenceException ex) { // either one of cake, frosting, or berries was null ... } 

Я лично не сделал бы этого по следующим причинам:

  • Это не выглядит красиво.
  • Он использует обработку исключений, которая должна ориентироваться на исключительные ситуации, а не на то, что вы ожидаете часто в ходе обычного курса.
  • NullReferenceException вероятно, никогда не должно быть обнаружено явно. (См. Этот вопрос .)

Так можно ли использовать какой-либо метод расширения или это будет функция языка, […]

Это почти наверняка должно быть языковой функцией (которая доступна в C # 6 в виде операторов .? И ?[] ), Если у C # уже была более сложная ленивая оценка или если вы не хотите использовать reflection (что, вероятно, также не является хорошей идеей по причинам производительности и безопасности типов).

Поскольку невозможно просто передать cake.frosting.berries.loader функции (она будет оценена и cake.frosting.berries.loader исключение с нулевой ссылкой), вам придется реализовать общий метод поиска следующим образом: он принимает объекты и имена свойств для поиска:

 static object LookupProperty( object startingPoint, params string[] lookupChain ) { // 1. if 'startingPoint' is null, return null, or throw an exception. // 2. recursively look up one property/field after the other from 'lookupChain', // using reflection. // 3. if one lookup is not possible, return null, or throw an exception. // 3. return the last property/field's value. } ... var x = LookupProperty( cake, "frosting", "berries", "loader" ); 

(Примечание: отредактирован код.)

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

[…], или это просто плохая идея?

Я бы либо остался с:

 if (cake != null && cake.frosting != null && ...) ... 

или пойти с вышеупомянутым ответом Мехрдада Афшари.


PS: Когда я написал этот ответ, я, очевидно, не рассматривал деревья выражений для lambda-функций; см., например, @driis ‘ответ для решения в этом направлении. Это также основано на каком-то отражении и, следовательно, может не работать так же хорошо, как и более простое решение ( if (… != null & … != null) … ), но это может быть оценено лучше с точки зрения синтаксиса ,

В то время как ответ driis интересен, я думаю, что это слишком дорогая производительность. Вместо того, чтобы компилировать много делегатов, я бы предпочел скомпилировать один lambda для каждого пути свойства, кешировать его, а затем повторно вывести его из нескольких типов.

NullCoalesce ниже делает именно это, он возвращает новое lambda-выражение с нулевыми проверками и возвратом значения по умолчанию (TResult), если любой путь равен NULL.

Пример:

 NullCoalesce((Process p) => p.StartInfo.FileName) 

Вернет выражение

 (Process p) => (p != null && p.StartInfo != null ? p.StartInfo.FileName : default(string)); 

Код:

  static void Main(string[] args) { var converted = NullCoalesce((MethodInfo p) => p.DeclaringType.Assembly.Evidence.Locked); var converted2 = NullCoalesce((string[] s) => s.Length); } private static Expression> NullCoalesce(Expression> lambdaExpression) { var test = GetTest(lambdaExpression.Body); if (test != null) { return Expression.Lambda>( Expression.Condition( test, lambdaExpression.Body, Expression.Default( typeof(TResult) ) ), lambdaExpression.Parameters ); } return lambdaExpression; } private static Expression GetTest(Expression expression) { Expression container; switch (expression.NodeType) { case ExpressionType.ArrayLength: container = ((UnaryExpression)expression).Operand; break; case ExpressionType.MemberAccess: if ((container = ((MemberExpression)expression).Expression) == null) { return null; } break; default: return null; } var baseTest = GetTest(container); if (!container.Type.IsValueType) { var containerNotNull = Expression.NotEqual( container, Expression.Default( container.Type ) ); return (baseTest == null ? containerNotNull : Expression.AndAlso( baseTest, containerNotNull ) ); } return baseTest; } 

Один из вариантов заключается в использовании Null Object Patten, поэтому вместо того, чтобы иметь нулевое значение, когда у вас нет торта, у вас есть NullCake, который возвращает NullFosting и т. Д. Извините, я не очень хорошо объясняю это, но другие люди, см.

  • Пример использования Null Object Patten
  • Википедай пишет на Null Object Patten

Я тоже часто желал более простого синтаксиса! Это становится особенно уродливым, если у вас есть значения метода-возврата, которые могут быть нулевыми, потому что тогда вам нужны дополнительные переменные (например: cake.frosting.flavors.FirstOrDefault().loader )

Однако, вот довольно приличная альтернатива, которую я использую: создайте вспомогательный метод Null-Safe-Chain. Я понимаю, что это очень похоже на ответ Джона Джон (с использованием метода расширения Coal ), но я нахожу его более простым и менее типичным. Вот как это выглядит:

 var loader = NullSafe.Chain(cake, c=>c.frosting, f=>f.berries, b=>b.loader); 

Вот реализация:

 public static TResult Chain(TA a, Func b, Func c, Func r) where TA:class where TB:class where TC:class { if (a == null) return default(TResult); var B = b(a); if (B == null) return default(TResult); var C = c(B); if (C == null) return default(TResult); return r(C); } 

Я также создал несколько перегрузок (с параметрами от 2 до 6), а также перегрузки, которые позволяют цепочке заканчиваться значением типа или по умолчанию. Это очень хорошо работает для меня!

Может быть, проект codeplex, который реализует Maybe или IfNotNull, используя lambdas для глубоких выражений в C #

Пример использования:

 int? CityId= employee.Maybe(e=>e.Person.Address.City); 

Ссылка была предложена в аналогичном вопросе. Как проверить нули в глубоком lambda-выражении?

Как было предложено в ответе Джона Лейдегрена , один подход к обходу заключается в использовании методов расширения и делегатов. Использование их может выглядеть примерно так:

 int? numberOfBerries = cake .NullOr(c => c.Frosting) .NullOr(f => f.Berries) .NullOr(b => b.Count()); 

Реализация грязная, потому что вам нужно заставить ее работать для типов значений, ссылочных типов и типов значений с нулевым значением. Вы можете найти полную реализацию ответа Timwi на « Что такое правильный способ проверки нулевых значений? ,

Или вы можете использовать reflection 🙂

Функция отражения:

 public Object GetPropValue(String name, Object obj) { foreach (String part in name.Split('.')) { if (obj == null) { return null; } Type type = obj.GetType(); PropertyInfo info = type.GetProperty(part); if (info == null) { return null; } obj = info.GetValue(obj, null); } return obj; } 

Применение:

 object test1 = GetPropValue("PropertyA.PropertyB.PropertyC",obj); 

Мой случай (возврат DBNull.Value вместо нуля в функции отражения):

 cmd.Parameters.AddWithValue("CustomerContactEmail", GetPropValue("AccountingCustomerParty.Party.Contact.ElectronicMail.Value", eInvoiceType)); 

Попробуйте этот код:

  ///  /// check deep property ///  /// instance /// deep property not include instance name example "ABCDE" /// if null return true else return false public static bool IsNull(this object obj, string property) { if (string.IsNullOrEmpty(property) || string.IsNullOrEmpty(property.Trim())) throw new Exception("Parameter : property is empty"); if (obj != null) { string[] deep = property.Split('.'); object instance = obj; Type objType = instance.GetType(); PropertyInfo propertyInfo; foreach (string p in deep) { propertyInfo = objType.GetProperty(p); if (propertyInfo == null) throw new Exception("No property : " + p); instance = propertyInfo.GetValue(instance, null); if (instance != null) objType = instance.GetType(); else return true; } return false; } else return true; } 

Мне очень нравится эта версия от Дмитрия Нестера: Цепочки с нулевыми проверками и Модификация Maybe

Я отправил эту последнюю ночь, а затем друг указал мне на этот вопрос. Надеюсь, поможет. Затем вы можете сделать что-то вроде этого:

 var color = Dis.OrDat(() => cake.frosting.berries.color, "blue"); using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Linq.Expressions; namespace DeepNullCoalescence { public static class Dis { public static T OrDat(Expression> expr, T dat) { try { var func = expr.Compile(); var result = func.Invoke(); return result ?? dat; //now we can coalesce } catch (NullReferenceException) { return dat; } } } } 

Читайте полный блог здесь .

Тот же друг также предложил вам посмотреть это .

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

 public static class GetValueOrDefaultExtension { public static TResult GetValueOrDefault(this TSource source, Func selector) { try { return selector(source); } catch { return default(TResult); } } } 

И да, это, вероятно, не оптимальное решение из-за последствий попытки / улова, но оно работает:>

Применение:

 var val = cake.GetValueOrDefault(x => x.frosting.berries.loader); 

Где вам нужно это сделать, сделайте следующее:

Применение

 Color color = someOrder.ComplexGet(x => x.Customer.LastOrder.Product.Color); 

или

 Color color = Complex.Get(() => someOrder.Customer.LastOrder.Product.Color); 

Выполнение classа помощника

 public static class Complex { public static T1 ComplexGet(this T2 root, Func func) { return Get(() => func(root)); } public static T Get(Func func) { try { return func(); } catch (Exception) { return default(T); } } } 

Мне нравится подход, предпринятый Objective-C:

«Язык Objective-C использует другой подход к этой проблеме и не вызывает методы на nil, но вместо этого возвращает nil для всех таких вызовов».

 if (cake.frosting.berries != null) { var str = cake.frosting.berries...; } 
  • TouchJSON, работающий с NSNull
  • getActionView () моего MenuItem возвращает null
  • Как вы строите std :: string со встроенным нулем?
  • Что является предпочтительным: Nullable . HasValue или Nullable ! = Null?
  • Как сделать тип значения nullable с помощью .NET XmlSerializer?
  • Java null проверить, почему use == вместо .equals ()
  • Нуль для примитивных типов данных
  • IllegalArgumentException или NullPointerException для нулевого параметра?
  • Переопределение NULL
  • Тестирование указателей на достоверность (C / C ++)
  • Внешний ключ MySQL для использования NULL?
  • Давайте будем гением компьютера.