Подтвердить что ты не робот

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

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

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

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

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

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

4b9b3361

Ответ 1

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

cake?.frosting?.berries?.loader

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

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

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

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

Ответ 2

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

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

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<TArg,TResult>(this TArg arg, Expression<Func<TArg,TResult>> expression)
        {
            if (expression == null)
                throw new ArgumentNullException("expression");

            if (ReferenceEquals(arg, null))
                return default(TResult);

            var stack = new Stack<MemberExpression>();
            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. Рассмотрите это как код доказательной концепции, и имейте в виду, что при его использовании будет штраф за производительность (что, вероятно, не будет иметь большого значения во многих случаях, но не использует его в замкнутом цикле:-))

Ответ 3

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

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

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

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

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

... или небольшое изменение выше:

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

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

Ответ 4

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

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

Ответ 5

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

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

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

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

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

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

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

Так как нет возможности просто передать 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 value.
}

...

var x = LookupProperty( cake, "frosting", "berries", "loader" );

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

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

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

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

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

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


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

Ответ 6

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

NullCoalesce ниже делает именно это, он возвращает новое лямбда-выражение с нулевыми проверками и возвратом значения по умолчанию (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<Func<TSource, TResult>> NullCoalesce<TSource, TResult>(Expression<Func<TSource, TResult>> lambdaExpression)
    {
        var test = GetTest(lambdaExpression.Body);
        if (test != null)
        {
            return Expression.Lambda<Func<TSource, TResult>>(
                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;
    }

Ответ 7

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

Ответ 9

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

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

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

Здесь реализация:

public static TResult Chain<TA,TB,TC,TResult>(TA a, Func<TA,TB> b, Func<TB,TC> c, Func<TC,TResult> 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), а также перегрузки, которые позволяют цепочке заканчиваться значением типа или по умолчанию. Это действительно хорошо для меня!

Ответ 10

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

    /// <summary>
    /// check deep property
    /// </summary>
    /// <param name="obj">instance</param>
    /// <param name="property">deep property not include instance name example "A.B.C.D.E"</param>
    /// <returns>if null return true else return false</returns>
    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;
    }

Ответ 12

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

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

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

Ответ 14

Или вы можете использовать отражение:)

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

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);

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

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

Ответ 15

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

var color = Dis.OrDat<string>(() => 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<T>(Expression<Func><T>> expr, T dat)
    {
      try
      {
        var func = expr.Compile();
        var result = func.Invoke();
        return result ?? dat; //now we can coalesce
      }
      catch (NullReferenceException)
      {
        return dat;
      }
    }
  }
}

Подробнее читайте полный пост в блоге.

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

Ответ 16

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

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

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

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

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

Ответ 17

где вам нужно это сделать, сделайте это.

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

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

или

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

Реализация класса помощника

public static class Complex
{
    public static T1 ComplexGet<T1, T2>(this T2 root, Func<T2, T1> func)
    {
        return Get(() => func(root));
    }

    public static T Get<T>(Func<T> func)
    {
        try
        {
            return func();
        }
        catch (Exception)
        {
            return default(T);
        }
    }
}

Ответ 18

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

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

if (cake.frosting.berries != null) 
{
var str = cake.frosting.berries...;
}