Как узнать, что лямбда-выражение имеет значение NULL

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

Пример. Функция:

 public static bool HasNull<T, Y>(this T someType, Expression<Func<T, Y>> input)
    {
      //Determine if expression has a null property
    }  

Использовать:

person.HasNull(d=>d.addressdetails.Street)
person.HasNull(d=>d.addressdetails[1].Street)
person.HasNull(d=>d.addressdetails.FirstOrDefault().Street)
person.HasNull(d=>d.InvoiceList.FirstOrDefault().Product.Name)

В любом из примеров addressdetails или street, или invoicelist, или product, или name могут иметь значение null. Код выдаст исключение, если я попытаюсь вызвать функцию, а какое-то вложенное свойство имеет значение null.

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

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

связанное сообщение: Не останавливайте отладчик при ЭТОМ исключение, когда он брошен и пойман


person MichaelD    schedule 24.12.2009    source источник
comment
получить отражение свойств и значений объекта и foreach на нем с помощью метода расширения? Если вы получили нуль, обрабатывайте его как хотите. Вы можете менять объект сколько угодно, и он всегда будет работать. Не выделять код, но отражение не рекомендуется для производства ... кроме случаев крайней необходимости. Для легкого использования это нормально, но если вы собираетесь отражать тысячи и тысячи раз, вам нужно переосмыслить свои модели.   -  person Piotr Kula    schedule 12.03.2014


Ответы (6)


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

public static class Dereferencer
{
    private static readonly MethodInfo safeDereferenceMethodInfo 
        = typeof (Dereferencer).GetMethod("SafeDereferenceHelper", BindingFlags.NonPublic| BindingFlags.Static);


    private static TMember SafeDereferenceHelper<TTarget, TMember>(TTarget target,
                                                            Func<TTarget, TMember> walker)
    {
        return target == null ? default(TMember) : walker(target);
    }

    public static TMember SafeDereference<TTarget, TMember>(this TTarget target, Expression<Func<TTarget, TMember>> expression)
    {
        var lambdaExpression = expression as LambdaExpression;
        if (lambdaExpression == null)
            return default(TMember);

        var methodCalls = new Queue<MethodCallExpression>();
        VisitExpression(expression.Body, methodCalls);
        var callChain = methodCalls.Count == 0 ? expression.Body : CombineMethodCalls(methodCalls);
        var exp = Expression.Lambda(typeof (Func<TTarget, TMember>), callChain, lambdaExpression.Parameters);
        var safeEvaluator = (Func<TTarget, TMember>) exp.Compile();

        return safeEvaluator(target);
    }

    private static Expression CombineMethodCalls(Queue<MethodCallExpression> methodCallExpressions)
    {
        var callChain = methodCallExpressions.Dequeue();
        if (methodCallExpressions.Count == 0)
            return callChain;

        return Expression.Call(callChain.Method, 
                               CombineMethodCalls(methodCallExpressions), 
                               callChain.Arguments[1]);
    }

    private static MethodCallExpression GenerateSafeDereferenceCall(Type targetType,
                                                                    Type memberType,
                                                                    Expression target,
                                                                    Func<ParameterExpression, Expression> bodyBuilder)
    {
        var methodInfo = safeDereferenceMethodInfo.MakeGenericMethod(targetType, memberType);
        var lambdaType = typeof (Func<,>).MakeGenericType(targetType, memberType);
        var lambdaParameterName = targetType.Name.ToLower();
        var lambdaParameter = Expression.Parameter(targetType, lambdaParameterName);
        var lambda = Expression.Lambda(lambdaType, bodyBuilder(lambdaParameter), lambdaParameter);
        return Expression.Call(methodInfo, target, lambda);
    }

    private static void VisitExpression(Expression expression, 
                                        Queue<MethodCallExpression> methodCallsQueue)
    {
        switch (expression.NodeType)
        {
            case ExpressionType.MemberAccess:
                VisitMemberExpression((MemberExpression) expression, methodCallsQueue);
                break;
            case ExpressionType.Call:
                VisitMethodCallExpression((MethodCallExpression) expression, methodCallsQueue);
                break;
        }
    }

    private static void VisitMemberExpression(MemberExpression expression, 
                                              Queue<MethodCallExpression> methodCallsQueue)
    {
        var call = GenerateSafeDereferenceCall(expression.Expression.Type,
                                               expression.Type,
                                               expression.Expression,
                                               p => Expression.PropertyOrField(p, expression.Member.Name));

        methodCallsQueue.Enqueue(call);
        VisitExpression(expression.Expression, methodCallsQueue);
    }

    private static void VisitMethodCallExpression(MethodCallExpression expression, 
                                                  Queue<MethodCallExpression> methodCallsQueue)
    {
        var call = GenerateSafeDereferenceCall(expression.Object.Type,
                                               expression.Type,
                                               expression.Object,
                                               p => Expression.Call(p, expression.Method, expression.Arguments));

        methodCallsQueue.Enqueue(call);
        VisitExpression(expression.Object, methodCallsQueue);
    }
}

Вы можете использовать это так:

var street = person.SafeDereference(d=>d.addressdetails.Street);
street = person.SafeDereference(d=>d.addressdetails[1].Street);
street = person.SafeDereference(d=>d.addressdetails.FirstOrDefault().Street);
var name = person.SafeDereference(d=>d.InvoiceList.FirstOrDefault().Product.Name);

Предупреждение: это не полностью протестировано, оно должно работать с методами и свойствами, но, вероятно, не с методами расширения внутри выражения.

Изменить: Хорошо, на данный момент он не может обрабатывать методы расширения (например, FirstOrDefault), но все еще можно настроить решение.

person Romain Verdier    schedule 24.12.2009
comment
Вау, если это действительно работает, это должно быть здорово, но лично я бы не хотел отлаживать этот метод;) - person Dmitry Tashkinov; 24.12.2009
comment
только что протестировал, работает, но как вы говорите не методами расширения. но это хорошее начало - person MichaelD; 24.12.2009

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

Вы уверены, что это меньше работы, чем просто добавить в код явную нулевую охрану?

person Jon Skeet    schedule 24.12.2009
comment
да, я надеялся, что код уже кем-то написан. - person MichaelD; 24.12.2009

Нам определенно нужен нулевой безопасный оператор разыменования в C #, но до тех пор просмотрите этот вопрос, который предлагает несколько иное, но тоже изящное решение той же проблемы.

person jeroenh    schedule 27.12.2009

По какой причине вы не могли просто сделать следующее?

bool result;
result = addressdetails != null && addressdetails.Street != null;
result = addressdetails != null && addressdetails.Count > 1 && addressdetails[1].Street != null;
result = addressdetails != null && addressdetails.FirstOrDefault() != null && addressdetails.FirstOrDefault().Street != null;
result = addressdetails != null && addressdetails.FirstOrDefault() != null && addressdetails.FirstOrDefault().Product != null && addressdetails.FirstOrDefault().Product.Name != null;

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

person Sam Salisbury    schedule 24.12.2009
comment
Потому что это более подвержено ошибкам и медленнее записывается. Мне нравится чистый код. Или он имеет нуль, или его нет. в .net также есть getValueOrDefault для извлечения значения из обнуляемого int для более быстрого и чистого кода - person MichaelD; 24.12.2009

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

public static bool HasNull<T, Y>(this T someType, IEnumerable<string> propertyNames)

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

Вместо того, чтобы писать

person.HasNull(d=>d.addressdetails.FirstOrDefault().Street)

тебе придется написать

person.HasNull(new string[] { "addressdetails", "0", "Street" })
person Dmitry Tashkinov    schedule 24.12.2009

Я писал об этом в блоге, он работает в VB.NET (я перевел на C #, но не тестировал версию C #, проверьте это).

Оформить заказ: Как бы вы проверили car.Finish.Style.Year.Model.Vendor.Contacts.FirstOrDefault (). FullName для null? :-)

Dim x = person.TryGetValue(
    Function(p) p.addressdetail(1).FirstOrDefault().Product.Name)
person Shimmy Weitzhandler    schedule 11.08.2010